logo

Babylon.js Market

By Lawrence

6 minutes

Building for Production

TL;DR — One vite command does two opposite jobs. In dev, esbuildThe fast Go-based transpiler Vite uses in dev to strip TypeScript types and serve files the instant the browser asks for them. serves your modules unbundled so HMRHot Module Replacement, the dev feature that swaps the one module you changed into the running page without a full reload. is instant. For production, RollupThe bundler Vite runs for the production build, stitching your modules into a few optimized files. bundles, tree-shakes, and content-hashes a lean dist/. We also cover how a published package ships its .js next to the .d.ts that powers your editor's autocompleteThe editor feature that suggests method and property names as you type, driven by the types it can see.. Skip if you already know why dev serves unbundled modules while npm run build bundles and tree-shakes, and how .d.ts files feed autocomplete.

Last lesson you read the framework's real source — the 640-slot event pool. You saw why it never allocates a single object in a frame. That was the last big idea the course owed you. What's left is the machinery that carries that source to everyone else. Back in The Dev Loop, npm run dev updated the page the moment you saved. That was impressive, since you didn't yet know what a moduleA single source file that imports what it needs and exports what others use; your app is a graph of them. was. That same vite command also packages your code for a stranger's phone. Those are two very different jobs, and two different tools handle them. The tools pull in opposite directions. One question has also quietly waited the whole course. How did a package you npm installed teach your editor to autocomplete code you never wrote?

How HMR keeps the loop fast

Your project isn't one giant file. It's a pile of small files — modules — and each one imports what it needs from the others. When you save, Vite doesn't rebuild the whole app. It finds the one module that changed and sends only that module to the browser. The browser swaps it in place. The page updates in milliseconds, and you keep your scroll position and any text you'd typed. This is Hot Module Replacement, or HMR.

npm run dev

#  VITE ready in 180 ms
#  ➜  Local:   http://localhost:5173/
#
#  (you save Player.ts)
#  hmr update /src/Player.ts   ← just that one module, re-sent

That loop between "I wonder what this does" and "oh, that's what it does" was only a couple hundred milliseconds wide. That speed is what let you read so much real source so fast.

The production build: a different job, a different tool

In dev, Vite serves your source files mostly as-is. It uses the browser's native ES module system, and a fast transpiler called esbuild strips the TypeScript types on the fly. Vite tunes dev for one thing: your feedback loop. Files transform only when the browser asks for them, so startup is instant and HMR is instant.

But hundreds of tiny files is wrong for a real user. They shouldn't have to download hundreds of files, or any code the app never runs. So npm run build hands the bundling to Rollup. Rollup's job is the opposite of esbuild's: a fast download and run for the end user. The two builds are tuned for two different people.

Dev (npm run dev)Production (npm run build)
Optimizes foryour feedback loopthe user's download and run
ToolesbuildRollup
Moduleshundreds, served unbundledbundled into a handful of files
Transformson demand, per requestall up front, once
Unused codestill serveddropped (tree-shaken)
File namessource pathscontent-hashed (index-a3f9c1.js)

What Rollup does that esbuild doesn't

Three jobs turn your pile of modules into something small enough to ship.

Bundling stitches your modules into a few files. Now the browser makes a handful of requests instead of hundreds. Tree-shaking follows your imports and drops any code nothing imports. Use three of a library's fifty functions, and the other forty-seven never reach your bundleThe output of stitching many source modules together into one file (or a few) the browser downloads at once.. The name comes from shaking dead leaves off a tree. Code-splitting breaks the result into separate chunks. Each chunkOne of the separate files code-splitting produces, loaded on demand rather than all at startup. loads only when a page needs it. Code-splitting also stamps each file's name with a content hashA short fingerprint of a file's contents stamped into its name, so the name changes only when the contents do. of what's inside it. When the contents change, the name changes. So a browser re-downloads a file only when it's actually different.

npm run build

#  vite v7  building for production...
#  ✓ 142 modules transformed.
#  dist/index.html                 0.4 kB
#  dist/assets/index-a3f9c1.js    48.2 kB │ gzip: 15.1 kB   ← bundled, tree-shaken, hashed
#  dist/assets/vendor-7b2e08.js   91.0 kB │ gzip: 30.4 kB   ← a split-off chunk
#  ✓ built in 1.2s

The output lands in dist/, a folder you can read line by line now. The second, smaller number on each line — gzip: 15.1 kB — is how small the file gets after it's compressed for download. The vendor chunk is the outside library code, split away from your own. It's split here because it's loaded through a dynamic import(). Now a browser can cache it on its own and skip re-downloading it when only your code changes. The hashed names, the split chunks, and the smaller sizes are all Rollup deciding what a stranger should download. Two tools, on purpose: esbuild for a fast dev loop, Rollup for a lean ship.

Split-panel dark-navy neon-wireframe diagram: left panel labeled DEV showing esbuild transpiling many small unbundled native-ESM module boxes wired straight to a browser with a glowing instant-HMR arrow; right panel labeled PROD showing Rollup funneling those modules into a few bundled chunks, faint greyed-out boxes falling away labeled tree-shaking, output files stamped with hashed names

How a typed library ships: .js plus .d.ts

A published package like @babylonjsmarket/ecs ships a dist/ too. But it's not the bundled, tree-shaken, hashed kind an app build produces. A library leaves bundling to the app that installs it. So it ships its modules unbundled, one .js per source file, paired with a description of their types. Two kinds of files sit side by side.

FileWhat it carriesJob
.jsthe actual transpiled coderuns in the browser
.d.tsa pure description of the shapes — what classes exist, their methods, arguments, return typestells your editor the types

The .d.ts files carry no logic. They're the shape of the library written down: the same interface and class signatures you've read all course, with the implementation peeled off. When you npm install @babylonjsmarket/ecs and import { World }, you pull in the built JavaScript to run. The declaration files are why your editor autocompleted method names and underlined mistakes before you ran anything.

// You write this:
import { World } from "@babylonjsmarket/ecs";

const world = new World();
world.//  ← your editor lists every method here, from ecs's .d.ts files

That autocomplete was never magic. Every red squiggle in Lesson 2, and every method name the editor offered as you typed world., came from a .d.ts file. The framework's authors shipped those files so your editor could see the shapes without ever running the code.

This course edits those packages a third way. BabylonJS Market is a monorepoOne repository holding several packages that can be linked and edited together instead of installed separately., one repository that holds the framework packages together. So instead of installing each package's built dist/, the workspace links the live source. A fix in @babylonjsmarket/ecs shows up in a game the same second. You don't need that yet. "Install the built dist, get the .d.ts for free" is the whole story for now.

Now go read the source

Open the @babylonjsmarket/ecs source and read it. These are the files that dist/ was built from. Read them not to memorize, but to recognize. The type aliases, the abstract base class, the generic emit, the Map<string, Set<EventCallback>>, the 640-slot pool of pre-allocated reusable objects: you've already met every shape on the page. You came in not knowing what a module was. Now you can open a production game framework and follow it. You'll see not just what it says but why it's built the way it is. Most people never reach that part.

When you're ready to build on it, Up to Speed with Arcade ECS turns this same source into real, playable games — entities, components, systems, the World, and scenes as data. This course gave you the language and the runtime. That one gives you the architecture that sits on top.

References

  • Vite — Building for Production — the Rollup production build, tree-shakingDropping any code nothing imports from the final bundle, named for shaking dead leaves off a tree., and code-splittingBreaking the bundle into separate chunks that load only when the page actually needs them..
  • Rollup — the production bundler.
  • MDN — JavaScript modules — the ES modules both builds rely on.
  • npm — package.json — how a published package describes its files and types.

Was this page helpful?

We read every note — tell us what's working and what isn't.

↑↓ NavigateEnter SelectEsc CloseCtrl+K Open Search