Modules, packages, and the toolchain · Lesson 64 of 77 · concept
Bundlers, transpilers, and runtimes
About 50 minutes.
By the end of this lesson you can
- Say what a runtime is and name three different ones your code might meet.
- Explain what a transpiler changes and what it deliberately does not.
- Say what a bundler does with your import graph, and what tree shaking needs from it.
- Tell the difference between transpiling syntax and polyfilling a missing capability.
Read, not run. The sandbox on this site has no modules, the DOM, the network and the filesystem, so the examples in this lesson are shown rather than executed. Where an example cannot run, it says so and explains why instead of leaving you to find out.
First, the only thing that actually runs your code
Explanation
A runtime is the program that executes JavaScript. A browser has one. Node has one. This page has a third: a small engine compiled to WebAssembly, which is why your exercises run without a server. Same language, three different environments.
The distinction that matters is between the language and its surroundings. The language gives you values, functions, objects, classes - everything modules 1 to 9 covered - and it is the same everywhere. The surroundings are whatever else that particular runtime chooses to provide: a browser adds document and fetch, Node adds a file system and process, and the engine on this page adds almost nothing, which is exactly why it is safe to run a stranger's code in.
So "does JavaScript have X" is usually the wrong question. The right one is "does the runtime I am targeting have X". The second worked example below asks this page's runtime that question directly, and the answers are worth looking at: JSON and Promise exist because they are part of the language; setTimeout, fetch and document do not exist at all, because no host is providing them here.
Everything else is a tool, and no tool runs your code
A mental model
Around the runtime sits a toolchain: a formatter, a linter, a type checker, a test runner, a transpiler, a bundler, a minifier. MDN's overview of client-side tooling groups them usefully into a safety net you use while writing, transformation tools that change your code before it ships, and post-development tools for testing and deployment.
Here is the mental model that makes the whole category tractable: with one exception, none of these tools changes what your program MEANS. They take source in and give source out, and the output is supposed to behave identically. A formatter changes whitespace. A minifier changes names and whitespace. A transpiler changes syntax. If any of them changes behaviour, that is a bug in the tool, not a feature.
The exception is the polyfill, which genuinely does add something that was not there. Keeping that one clearly separate from the others is most of what this lesson is for.
MDN — Overview of modern web tooling
Check there: Client-side tools are grouped into safety-net, transformation and post-development categories; transformation covers writing modern syntax that gets converted for older environments, and bundlers optimise output by tree shaking and minifying.
A transpiler rewrites syntax, not capability
Explanation
A transpiler - Babel is the best-known, and TypeScript and SWC do the same job among others - reads modern JavaScript and writes older JavaScript that behaves the same. Arrow functions become function expressions. Template literals become string concatenation. Class syntax becomes functions and prototypes. The point is that you get to write today's language and still run on an environment that only understands an older one.
The first worked example shows the transformation by hand: the modern version and the transpiled-looking version sit side by side, and the last line checks that they produce the same answer. That is the whole promise of a transpiler in one line of output.
What a transpiler cannot do is invent a capability the runtime does not have. There is no older syntax that means fetch, because fetch is not syntax - it is a function the environment either provides or does not. Rewriting your arrow functions will not conjure it. This is the single most useful distinction in this lesson, and the next section is about the tool that does address it.
A polyfill supplies a missing thing; a transpiler cannot
A common mistake
A polyfill is ordinary JavaScript that implements a missing feature using whatever the runtime does have, and installs it under the standard name so the rest of your code cannot tell the difference. If an old browser lacks a method, a polyfill defines one that behaves the same way.
The mistake to avoid is expecting your build setup to handle both cases with one switch. Say your code uses a syntax feature and a newer built-in method. The transpiler rewrites the syntax and, quite correctly, leaves the method call exactly as it was - it is a normal function call, and there is nothing to rewrite. On an old runtime the syntax now works and the method is still undefined. The symptom is a build that succeeds and a page that throws "is not a function" on a device you do not own.
The rule to carry: syntax is a transpiler's job, missing capability is a polyfill's job, and knowing which of the two you are relying on is your job. A polyfill also has a cost the transpiler does not - it is real code, shipped to every user, whether they needed it or not.
A bundler follows your imports and reduces the pile
Explanation
You have split your program into two hundred files, which is good for you and bad for a browser, which would have to request them all. A bundler - Vite, webpack, Rollup, esbuild - starts at your entry file, follows every import to build the graph of what depends on what, and writes out a smaller number of files containing the same code.
Along the way it does the optimisations MDN's overview mentions. Tree shaking removes exported code that nothing imports, so a library with fifty functions costs you only the ones you used. Minification strips whitespace and shortens local names, which changes nothing about behaviour and a great deal about download size. Source maps are how the shortened result can still be debugged: a side file that maps the built code back to the lines you wrote.
Now the payoff for lesson one's rule about imports living at the top level and naming a literal string. That restriction is what lets the bundler read the graph WITHOUT running your program. MDN's definition of tree shaking says it relies on the import and export statements to work out what is used. Hide an import behind a variable or a condition and you have hidden it from the tool as well.
Check there: Tree shaking is defined as dead-code removal that relies on the import and export statements to detect which modules are used.
How much of this do you need on day one?
Why it matters
Less than the industry's noise level suggests. A small project served to modern browsers can use ES modules directly, with no build step at all. Every browser in current use understands import and export.
The tools earn their place when a specific problem appears: you need to support an environment older than your syntax (transpiler), your page makes too many requests or ships too much unused library code (bundler), or your users are on runtimes missing something you depend on (polyfill). Adding a toolchain before you have the problem it solves is how projects end up with configuration nobody understands.
What you should be able to do after this lesson is read a project's setup and say what each piece is for - which is the module's stated goal, and the difference between being able to work in an unfamiliar repository and being locked out of it.
Recap
Recap
The runtime executes your code and decides what exists beyond the language itself - browser, Node and this page's engine all differ. Everything else in the toolchain transforms source into equivalent source: a transpiler rewrites modern syntax into older syntax, a minifier shortens, a bundler follows the import graph and emits fewer files while tree shaking away exports nobody imports. None of them adds a capability; only a polyfill does that, and it costs real bytes. Tree shaking works because imports are static, which is why they must sit at the top level and name a literal string.
Worked examples
Every output below was produced by actually running the code in the same sandbox your exercises run in — it is not a prediction.
What a transpiler does, done by hand
The top half is modern syntax; the bottom half is what a transpiler targeting an older environment might emit. Nothing about the meaning changes, and the last line proves it - which is exactly the guarantee a transpiler is supposed to give you.
1const names = ["ada", "grace"];2const shouted = names.map((name) => name.toUpperCase() + "!");3console.log(shouted.join(" "));4 5var namesOld = ["ada", "grace"];6var shoutedOld = namesOld.map(function (name) {7 return name.toUpperCase() + "!";8});9console.log(shoutedOld.join(" "));10 11console.log(shouted.join(" ") === shoutedOld.join(" "));- line 2
An arrow function - syntax from 2015 that some very old environments cannot parse.
- line 6
The same callback as a function expression. A transpiler makes this substitution mechanically, for every arrow in your project.
- line 5
const and let are also syntax, and older targets get var instead - which is why transpiled output looks dated even when your source does not.
- line 11
Identical results. A transformation that changed this line to false would be a bug in the tool, not a trade-off you accepted.
What it prints
ADA! GRACE!ADA! GRACE!true
Asking this page's runtime what it has
Not a thought experiment: this runs in the engine grading your exercises, and the answers are its actual capabilities. Two of these are part of the JavaScript language, so they are here. Four are things a host environment provides, and this host provides none of them.
1console.log(typeof JSON.parse);2console.log(typeof Promise);3console.log(typeof setTimeout);4console.log(typeof fetch);5console.log(typeof document);6console.log(typeof require);- line 1
JSON is part of the language, so every runtime has it. Nothing needed to be added for this to work.
- line 3
setTimeout is NOT part of the language - browsers and Node each provide their own. Here there are no timers at all, which is why this course's async module has to explain rather than demonstrate.
- line 4
No fetch, because there is no network. No transpiler could give you this; only a host or a polyfill could, and neither exists here.
- line 6
No require either, which is the same story from the CommonJS side: module loading is a host capability, not a language one.
What it prints
functionfunctionundefinedundefinedundefinedundefined
What a bundler emits
A sketch rather than real output, so you can see the shape of the transformation: three files with imports between them become one file with the imports resolved away and the unused export dropped. Real bundler output is minified and wrapped, but this is what it is doing underneath.
1// ----- before: three files -----2// src/format.js3export function capitalise(text) { /* ... */ }4export function shout(text) { /* ... */ } // nobody imports this5 6// src/label.js7import { capitalise } from "./format.js";8export function labelFor(text) { return capitalise(text.trim()); }9 10// src/main.js11import { labelFor } from "./label.js";12console.log(labelFor(" release notes "));13 14// ----- after: one bundled file (sketch) -----15function capitalise(text) { /* ... */ }16function labelFor(text) { return capitalise(text.trim()); }17console.log(labelFor(" release notes "));- line 4
Exported but never imported anywhere in the graph. This is what tree shaking removes - and it can only see that because the imports are statically readable.
- line 7
An import between two of your own files. In the bundled output it is gone: both functions now sit in one file, so there is nothing to resolve at load time.
- line 15
Note what did NOT change: the bodies. A bundler rearranges and removes; it does not rewrite what your functions do.
- line 16
shout is absent. A browser downloads less code, and nothing about the program's behaviour differs.
This one does not run hereBoth halves are module syntax spread across several files, and the sandbox has no module loader. It is also a sketch of a bundler's job rather than real output - real bundles are minified and wrapped in scope-isolating machinery.
Check yourself
Not scored, not stored. Getting one wrong is the useful part.
Your code uses arrow functions and calls fetch. You transpile it for a very old browser that has neither. What happens?
The second function is the first one after a minifier has shortened every name it safely could. Predict all three lines.
function totalOf(list) { let sum = 0; for (const value of list) { sum = sum + value; } return sum;}function t(l) { let s = 0; for (const v of l) { s = s + v; } return s;}console.log(totalOf([2, 3, 4]));console.log(t([2, 3, 4]));console.log(totalOf([2, 3, 4]) === t([2, 3, 4]));
Why can a bundler tell that an exported function is unused without running your program?
No exercise in this lesson
The subject is the tools around the runtime, and the sandbox has none of them: no bundler, no transpiler, no minifier and no file system for them to read. What can be shown in running code is here as worked examples - the same program before and after a transformation, behaviour unchanged.
Worth remembering
- A transpiler and a polyfill - which one gives you a missing fetch?
The polyfill. A transpiler only rewrites syntax into older syntax; a missing function is a capability, and no syntax change can supply it.
- What does tree shaking depend on?
Statically readable import and export statements. The bundler builds the dependency graph from the source text without running the program.
- Is setTimeout part of JavaScript?
No. It is provided by the host - browsers and Node each supply their own. The runtime, not the language, decides what exists beyond the core.
Check this lesson against the source
We wrote the explanation; we did not invent the facts. This is the page that backs them.
MDN — Overview of modern web tooling
Check there: The three tool categories, the description of transformation tools that let you write modern code for older environments, and bundlers as the tools that tree shake and minify code for production.
Check there: Tree shaking relies on the static import and export statements to detect unused code.
Check there: Minification removes unnecessary characters without changing what the code does.
Prefer to read the source in a structured breakdown? Turn this doc into a breakdown