Topic 12.4
Bundlers and Dev Servers
In one line
A bundler follows your imports from one entry file and combines the modules into a few optimised files, transpiling new syntax for older browsers, removing unused code and minifying; a dev server such as Vite serves your code instantly while you work and updates the page as you save.
Think of it like this
Packing for a trip. At home, your things are spread across many drawers and shelves (your source files). Before you travel, you pack only what you'll use into one or two suitcases (the bundle), leave behind what you won't need (tree shaking), roll clothes tightly to save space (minifying), and label each bag so it isn't mixed up with an old one (content hashes). At home you don't pack anything; you just open the drawer you need (the dev server).
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Bundler
- A build tool that follows your imports and combines many modules into a few optimised files.
- Entry point
- The first file the bundler starts from, usually
src/main.jsorindex.html. - Tree shaking
- Removing exported code that no module imports, so it isn't shipped.
- Minification
- Making code smaller without changing what it does: removing spaces and comments, shortening names.
- Transpiler
- A tool that rewrites code into another version of the language, such as modern JavaScript into older JavaScript.
- Polyfill
- Extra code that adds a missing built-in feature (like a method) to older browsers.
- Dev server
- A local web server for development that serves your code and reloads changes as you save.
- HMR
- Hot Module Replacement: updating one changed module in a running page without reloading everything.
- Source map
- A file that maps built, minified code back to your original source for debugging.
- Content hash
- A short fingerprint of a file's contents put in its name, so a changed file gets a new name.
Step by step
01Create a Vite project
npm create vite@latest runs the create-vite package, which asks a few questions (or takes them as options) and writes a starter project. The vanilla template means plain JavaScript, no framework.
02Look at the structure
index.html sits at the project root and is the entry point: Vite reads its <script type="module" src="/src/main.js"> and follows the imports. public/ holds files copied as-is. package.json has the dev, build and preview scripts.
my-app/
├── index.html <- entry: <script type="module" src="/src/main.js">
├── package.json <- scripts: dev, build, preview
├── public/
│ └── vite.svg <- copied to dist/ unchanged
└── src/
├── main.js
├── counter.js
└── style.css <- imported from main.js03Run the dev server
npm run dev starts Vite. Open the address it prints. Edit src/counter.js and save: the change appears at once without a full reload (HMR). Imports like import "./style.css" work too: Vite turns CSS into a <style> in development.
04Build for production
npm run build bundles, tree-shakes, transpiles to the target, minifies, hashes file names and writes dist/. This folder is what you upload to a host. npm run preview serves it locally to check it.
05Development versus production
The two modes optimise for different things: development for instant feedback, production for the smallest, most cacheable files.
06Configure only what you need
Vite works without a config file. When you need one, vite.config.js sets options such as the browser target, the public base path (for sites served from a subfolder) and source maps.
import { defineConfig } from "vite";
export default defineConfig({
base: "/shop/", // the site lives at example.com/shop/
build: {
target: "es2020", // syntax newer than this is transpiled
sourcemap: true, // write .map files for debugging
},
});Try it yourself
- 1
Use a tree-shaken function
In the first example, add
import { formatDate } from "./format.js";tomain.js(as a new string in its array). Predict: which name disappears from the "Tree-shaken away" list, and does the order change? - 2
Check the edge cases
In the second example, add
{ profile: null }and{ profile: { name: 0 } }toinputs. Predict both results for each function before you run it. - 3
Change only a comment
In the third example, make
v2change only the comment text instead of the number. Predict: does the hash change? Why is that good for caching?
Code & diagrams
Real bundlers parse code properly instead of reading lines, rename variables when two modules use the same name ("scope hoisting"), and only drop code they can prove has no side effects. The idea is the same: follow imports, put dependencies first, leave out what nobody uses.
Preview
Console
Nothing logged yet.Expected console output
Order: [ './format.js', './prices.js', './main.js' ]
Tree-shaken away: [ 'formatDate', 'debugPrices' ]
Running the bundle:
Rs 118.00Same results, much more code: that's the price of supporting old browsers. `void 0` is a short, safe way to write `undefined`. Notice the empty name stays `""`: `??` only replaces `null` and `undefined`, and the transpiled version keeps that exact rule.
Preview
Console
Nothing logged yet.Expected console output
"guest" "guest"
"guest" "guest"
"" ""
"Asha" "Asha"Changing one number gives a completely different file name, so a browser that cached version 1 for a year still fetches version 2 at once. Unchanged files keep their names and stay cached. Real builds use stronger hashes (such as SHA-256) and real minifiers also shorten `price` and `rate` to one letter.
Preview
Console
Nothing logged yet.Expected console output
version 1: 99 -> 70 chars, file index-45fc5ef3.js
version 2: 99 -> 70 chars, file index-e042c929.jsBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Open the built page from disk
Run npm run build, then double-click dist/index.html to open it as a file.
Break #2
Read an environment variable without the prefix
Put API_URL=https://api.example.com in a .env file and use import.meta.env.API_URL in src/main.js.
Myth vs fact
Myth
You can't write modern JavaScript without a bundler.
Fact
Browsers run ES modules natively. Small sites and prototypes can skip the build step. Bundlers earn their place with npm packages, many modules and production optimisations.
Myth
Transpiling makes new features like toSorted() work in old browsers.
Fact
Transpilers rewrite syntax. A missing built-in method needs a polyfill (for example from core-js). The two are often configured together, but they are different jobs.
Myth
Minification protects your code from being copied.
Fact
Minified code is harder to read, not secret. Tools can reformat it, and source maps (if published) restore it. Anything sent to the browser is public.
Myth
The dev server is just a slower version of production.
Fact
It's a different setup: unbundled native modules, on-demand transforms and HMR. That's why you should always test the production build with vite preview before deploying.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Tree shaking depends on side-effect knowledge: a module that runs code at import time (adds a global, registers a plugin, imports CSS) can't be dropped even if none of its exports are used. Library authors mark safe files with
"sideEffects": falseinpackage.json(or list the files that do have side effects). CommonJS packages tree-shake poorly becauserequireandmodule.exportsare dynamic. - ▸
Vite 7's default production target is
"baseline-widely-available": browsers that have supported features for at least 30 months under the Baseline project (Chrome and Edge 107, Firefox 104, Safari 16). Newer syntax is transpiled down to that level by esbuild/Oxc; polyfills are not added unless you use@vitejs/plugin-legacy. - ▸
Hashing strategy matters for caching: with long-lived
Cache-Control: max-age=31536000, immutableon/assets/*and a short cache onindex.html, users get new code immediately after a deploy while unchanged chunks stay cached. Splitting rarely changing vendor code into its own chunk keeps its hash stable between releases. - ▸
Much modern tooling is written in Rust or Go for speed: esbuild (Go), SWC, Oxc, Rolldown, Rspack and Turbopack (Rust). They are often 10 to 100 times faster than JavaScript-based predecessors like Babel and webpack, which is why rebuilds now take milliseconds.
Remember this
- 1
A bundler is a build tool that starts at your entry file (often
src/main.js), follows everyimportto build the module graph (Topic 12.1), and writes a small number of output files, usually into adistfolder. Why not ship hundreds of separate modules? Each file is a request, and the browser only discovers a module's imports after downloading it, which causes waterfalls. Bundlers also resolve bare specifiers like"date-fns"fromnode_modules(Topic 12.3), which browsers can't do on their own, and handle non-JavaScript imports such as CSS, images and JSON. - 2
On the way, the bundler optimises. Tree shaking removes exports nobody imports; it works because ES module imports are static. Minification shortens the code without changing what it does: it removes spaces and comments and renames local variables (
totalPricebecomest). Code splitting creates chunks at eachimport()(Topic 12.2). Content hashing puts a fingerprint of the file's contents in its name (index-B7xQ2LmA.js), so browsers can cache it for a year and a new version gets a new name. (Hectal's/system-designcourse covers HTTP caching in depth.) - 3
Transpiling means translating code into a different version of the same language: for example, rewriting
user?.name ?? "guest"(ES2020) into older syntax that a 2019 browser understands, or removing TypeScript types (Topic 12.6). The tool is a transpiler (Babel, SWC, esbuild, TypeScript'stsc). You choose the target: which browsers must be supported, often written as a browserslist query such as"> 0.5%, last 2 versions, not dead". Note that transpiling changes syntax only; a missing function such asArray.prototype.toSortedneeds a polyfill (extra code that adds it), not a transpiler. - 4
A dev server is a local web server for development. Vite (French for "quick", said "veet") is today's most common choice for new projects. In development it does not bundle your code: it serves each source file as a native ES module and transforms it the moment the browser asks for it, so start-up is near-instant even in large apps. It pre-bundles npm packages once, because some packages contain hundreds of small files. HMR (Hot Module Replacement) swaps an edited module into the running page without a full reload, so you keep your place and state.
- 5
For production,
vite buildruns a full bundler (Rollup in Vite 7, its faster Rust-based successor Rolldown in newer versions) to produce optimised files indist/, andvite previewserves that folder locally so you can test the real output. Other tools you'll meet: webpack (older, very configurable, still in many company codebases), esbuild and Rollup (used directly, often for libraries), Parcel, Rspack and Turbopack (used by Next.js). The ideas in this topic apply to all of them. - 6
Source maps connect the minified output back to your original files. The bundler writes
index-B7xQ2LmA.js.map, and DevTools uses it to show your real code, variable names and line numbers in errors and breakpoints (Phase 13). Without them, a production error points to "line 1, column 48211" of one giant line. Whether to publish source maps is a choice: they make debugging easier but reveal your original source.
Explain it without notes
Why do we bundle JavaScript for production if browsers support ES modules?
What is the difference between transpiling and polyfilling?
How does Vite's dev server differ from its production build?
What is tree shaking, and why does it need ES modules?
Practice
Write minifyNumbers(code) that removes every space except those inside double-quoted strings. Test it on const total = 1 + 2; const s = "a b";.
Write the package.json scripts and the vite.config.js for a site deployed to https://example.com/blog/, with source maps turned on.
Trade-offs
- ↔
A build step gives smaller, faster, cached production files and lets you use npm packages and TypeScript, but adds configuration, a slower feedback loop for some changes, and differences between dev and production to test for.
- ↔
Targeting very old browsers means more transpiled code and polyfills for everyone unless you ship two builds; a modern baseline target keeps bundles small but drops some users.
- ↔
Publishing source maps makes production errors easy to read; keeping them private (uploaded only to your error-tracking service) keeps your original source less exposed.
Done when you can
I can explain why bundling helps: requests, waterfalls, bare specifiers, optimisations.
I can create a Vite project and use
dev,buildandpreview.I can explain tree shaking, minification, code splitting and content hashes.
I know transpiling changes syntax, polyfills add features, and how a browser target is chosen.
I know what source maps are for, and that
VITE_variables end up public.