Parcel 07 ๐ฆ Differential Bundling โ Modern & legacy bundle generation
Differential bundling is Parcel’s mechanism for shipping two versions of the same application from one build. A modern bundle contains the source syntax, unmodified for browsers that support it. A legacy bundle contains the downleveled syntax and polyfills for browsers that do not. The browser chooses which bundle to load using the type="module" and nomodule attributes on the script tag. The modern browser parses the module script and ignores the nomodule fallback; the legacy browser ignores the module script and loads the fallback.
This chapter covers the targets configuration that produces the two bundles, the automatic nomodule fallback that Parcel injects when a <script type="module"> is used, and the directory structure that separates the outputs. It also covers the relationship between differential bundling and the browserslist field from the previous chapter: browserslist enables transpilation, and the targets configuration decides which transpilation applies to which output.
Key point: Differential bundling requires two things: a <script type="module"> in the HTML, and a browserslist field that defines the legacy target. Parcel generates the modern bundle from the source and the legacy bundle from the transpiled source, and injects the nomodule fallback automatically.
Why differential bundling exists
The transpilation cost problem. Transpiling modern JavaScript to ES5 syntax produces larger, slower output. Arrow functions become function expressions. async/await becomes a state machine. Classes become prototype chains. A browser that supports the modern syntax does not need any of this, but a single bundle that targets the oldest supported browser forces every user to pay the cost.
The parsing cost problem. Even if the transpiled code runs correctly in a modern browser, the browser must parse the larger output. The nomodule attribute solves this: a browser that supports ES modules ignores any script with the nomodule attribute, so it never downloads or parses the legacy bundle. The modern browser downloads only the modern bundle, and the legacy browser downloads only the legacy bundle .
The zero-configuration problem. Parcel detects the <script type="module"> tag in the HTML and generates the nomodule fallback automatically, based on the browserslist targets . The developer does not write a second script tag or a conditional loader. The bundler does the work.
a. The targets configuration
The targets field in package.json defines the named outputs. Each target has a name and an engines object that specifies the environment it compiles for. A modern target and a legacy target are the standard pair .
{
"targets": {
"modern": {
"engines": {
"browsers": ["last 2 Chrome versions", "last 2 Firefox versions"]
}
},
"legacy": {
"engines": {
"browsers": ["IE 11"]
}
}
}
}
The modern target compiles for the most recent versions of Chrome and Firefox, which support the source syntax. The legacy target compiles for IE 11, which requires downleveling. Parcel builds both from the same source, in parallel .
The outputs are written to dist/${targetName} by default. A project with a modern and a legacy target produces dist/modern and dist/legacy . The distDir field inside each target overrides the directory name if the project has a different convention.
If a target has only a single entry, an exact filename can be specified using a top-level field that matches the target name. A modern target can write to dist/modern.js, and a legacy target can write to dist/legacy.js .
b. The automatic nomodule fallback
The differential serving mechanism is triggered by the HTML, not by the targets configuration alone. When Parcel sees a <script type="module"> in the HTML, it knows the project uses ES modules. It then checks the browserslist targets. If the targets include browsers that do not support ES modules, Parcel generates a legacy bundle and injects a <script nomodule> fallback into the HTML .
<!-- Source HTML -->
<script type="module" src="./index.js"></script>
<!-- Output HTML (approximately) -->
<script type="module" src="/dist/modern/index.js"></script>
<script nomodule src="/dist/legacy/index.js"></script>
The modern browser parses the type="module" script and executes it. It sees the nomodule attribute on the second script and skips it entirely, without downloading the legacy bundle. The legacy browser does not understand type="module", so it skips the first script and executes the nomodule script, which contains the transpiled code .
This is why the browserslist field from the previous chapter is required. Without it, Parcel has no target to compile the legacy bundle for, and no reason to generate one. The targets configuration names the outputs, but browserslist defines the browser set that triggers the fallback.
Complete Example Session
// ============================================
// PART 1: THE TARGETS CONFIGURATION
// ============================================
{
"targets": {
"modern": {
"engines": {
"browsers": ["last 2 Chrome versions"]
}
},
"legacy": {
"engines": {
"browsers": ["IE 11"]
}
}
}
}
// Two named outputs.
// modern: source syntax, no downleveling.
// legacy: downleveled for IE 11.
<!-- ============================================
PART 2: THE SOURCE HTML
============================================ -->
<script type="module" src="./index.js"></script>
<!-- The module script is the trigger.
Parcel reads the type="module" attribute
and generates the nomodule fallback. -->
// ============================================
// PART 3: THE BROWSERSLIST REQUIREMENT
// ============================================
{
"browserslist": ["last 2 Chrome versions", "IE 11"],
"targets": {
"modern": { "engines": { "browsers": ["last 2 Chrome versions"] } },
"legacy": { "engines": { "browsers": ["IE 11"] } }
}
}
// browserslist enables transpilation.
// targets decides which transpilation applies to which output.
# ============================================
# PART 4: THE BUILD
# ============================================
parcel build index.html
# Output:
# dist/modern/index.js (modern syntax)
# dist/legacy/index.js (transpiled for IE 11)
# dist/index.html (with nomodule fallback)
# ============================================
# PART 5: THE OUTPUT HTML
# ============================================
cat dist/index.html
# <script type="module" src="/dist/modern/index.js"></script>
# <script nomodule src="/dist/legacy/index.js"></script>
// ============================================
// PART 6: THE MODERN BUNDLE
// ============================================
// const greet = (name) => `Hello, ${name}`;
// Modern browsers parse and run this as written.
// No downleveling.
// ============================================
// PART 7: THE LEGACY BUNDLE
// ============================================
// var greet = function greet(name) {
// return "Hello, ".concat(name);
// };
// IE 11 parses and runs this.
// The nomodule attribute ensures it is not
// downloaded by modern browsers.
// ============================================
// PART 8: EXACT FILENAMES
// ============================================
{
"modern": "dist/modern.js",
"legacy": "dist/legacy.js",
"targets": {
"modern": { "engines": { "browsers": ["last 2 Chrome versions"] } },
"legacy": { "engines": { "browsers": ["IE 11"] } }
}
}
// Single-entry targets can write to exact filenames.
// ============================================
// PART 9: THE DISTDIR OVERRIDE
// ============================================
{
"targets": {
"modern": {
"distDir": "dist/esm",
"engines": { "browsers": ["last 2 Chrome versions"] }
},
"legacy": {
"distDir": "dist/es5",
"engines": { "browsers": ["IE 11"] }
}
}
}
// Custom directory names.
# ============================================
# PART 10: THE DIFFERENTIAL SERVING RESULT
# ============================================
# Modern browser:
# loads dist/modern/index.js
# ignores dist/legacy/index.js
#
# Legacy browser:
# ignores dist/modern/index.js
# loads dist/legacy/index.js
#
# Both users get a bundle sized for their browser.
The ten parts covered the targets configuration, the source HTML, the browserslist requirement, the build output, the nomodule fallback, the modern bundle, the legacy bundle, exact filenames, the distDir override, and the differential serving result.
Quick Reference
Targets configuration
| Field | Purpose |
|---|---|
targets | Named output configurations |
targets.<name>.engines.browsers | Browser query set for that target |
targets.<name>.distDir | Output directory override |
Top-level <name> | Exact filename for single-entry target |
Default output directories
| Target | Output |
|---|---|
modern | dist/modern/ |
legacy | dist/legacy/ |
The differential serving mechanism
| Element | Role |
|---|---|
<script type="module"> | Triggers the modern bundle |
<script nomodule> | Fallback for legacy browsers |
browserslist | Enables transpilation for the legacy target |
targets | Names the outputs and defines their engines |
Browser behavior
| Browser | Module script | Nomodule script |
|---|---|---|
| Modern (supports ESM) | Downloads and runs | Ignores (no download) |
| Legacy (no ESM) | Ignores | Downloads and runs |
Best Practices
โ Do This:
// Define both targets explicitly
"targets": {
"modern": { "engines": { "browsers": ["last 2 Chrome versions"] } },
"legacy": { "engines": { "browsers": ["IE 11"] } }
} // โ
<!-- Use type="module" in the source HTML -->
<script type="module" src="./index.js"></script> // โ
// Declare browserslist for the legacy transpilation
"browserslist": ["last 2 Chrome versions", "IE 11"] // โ
โ Don’t Do This:
<!-- Use a classic script tag -->
<script src="./index.js"></script> // โ (no differential serving)
// Define targets without browserslist
"targets": { "legacy": { "engines": { "browsers": ["IE 11"] } } } // โ (transpilation not enabled)
// Use the same browsers query for both targets
"modern": { "engines": { "browsers": ["IE 11"] } },
"legacy": { "engines": { "browsers": ["IE 11"] } } // โ (identical bundles)
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
No nomodule fallback generated | Source HTML uses classic script tag | Use <script type="module"> |
| Legacy bundle is identical to modern | browserslist missing or targets identical | Declare browserslist and distinct targets |
| Modern browsers download the legacy bundle | The nomodule attribute is missing | Check the output HTML; ensure type="module" is in the source |
| Build fails on IE 11 syntax | browserslist does not include the legacy browser | Add it to the browserslist field |
| Outputs overwrite each other | Single target or distDir collision | Use distinct target names or distDir values |
Real-World Examples
1. Modern and legacy targets
{
"targets": {
"modern": { "engines": { "browsers": ["last 2 Chrome versions"] } },
"legacy": { "engines": { "browsers": ["IE 11"] } }
}
}
2. The source HTML
<script type="module" src="./index.js"></script>
3. The output HTML
<script type="module" src="/dist/modern/index.js"></script>
<script nomodule src="/dist/legacy/index.js"></script>
4. Browserslist
{ "browserslist": ["last 2 Chrome versions", "IE 11"] }
5. Exact filenames
{ "modern": "dist/modern.js", "legacy": "dist/legacy.js" }
6. Custom distDir
{ "modern": { "distDir": "dist/esm" }, "legacy": { "distDir": "dist/es5" } }
7. Build command
parcel build index.html
8. Verify output
ls dist/modern dist/legacy
9. Check the HTML
grep nomodule dist/index.html
10. The InfoQ summary
"Parcel 2 automatically generates both native ES modules for modern browsers
and fallback classic scripts for old browsers."
Visual
The differential serving flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ SOURCE โ
โ <script type="module" src="./index.js"></script> โ
โ โ
โ Parcel build โ
โ โ โ
โ โโโ modern target โโโบ dist/modern/index.js โ
โ โ (source syntax) โ
โ โ โ
โ โโโ legacy target โโโบ dist/legacy/index.js โ
โ (transpiled) โ
โ โ
โ Output HTML โ
โ <script type="module" src="/dist/modern/index.js"> โ
โ <script nomodule src="/dist/legacy/index.js"> โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The browser decision
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ MODERN BROWSER โ
โ type="module" โโโบ supported โโโบ download modern โ
โ nomodule โโโบ ignored (no download) โ
โ โ
โ LEGACY BROWSER โ
โ type="module" โโโบ not supported โโโบ ignore โ
โ nomodule โโโบ supported โโโบ download legacy โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The target directories
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ dist/ โ
โ โโโ modern/ โ
โ โ โโโ index.js โ
โ โโโ legacy/ โ
โ โ โโโ index.js โ
โ โโโ index.html โ
โ (contains both script tags) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The browserslist and targets relationship
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ browserslist โ
โ โโโ enables transpilation โ
โ โ
โ targets โ
โ โโโ modern โโโบ engines.browsers โโโบ source syntax โ
โ โโโ legacy โโโบ engines.browsers โโโบ transpiled syntax โ
โ โ
โ Both are required. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| Trigger | <script type="module"> in the HTML |
| Fallback | <script nomodule> injected automatically |
| Targets field | targets in package.json |
| Default directories | dist/modern, dist/legacy |
| Browser selection | Modern: module; Legacy: nomodule |
| Requirement | browserslist for the legacy transpilation |
| Benefit | Smaller bundles for modern browsers |
Key takeaways:
- Differential bundling produces two outputs from one source. The modern bundle contains the source syntax. The legacy bundle contains the downleveled syntax. The browser chooses which to load using the
moduleandnomoduleattributes . - The
<script type="module">tag is the trigger. Parcel detects it in the HTML and generates thenomodulefallback automatically, based on thebrowserslisttargets. A classic<script>tag does not trigger differential serving . - The
targetsconfiguration names the outputs and defines their engines. Amoderntarget and alegacytarget are the standard pair. Each target’sengines.browsersdetermines the syntax that the output preserves or downlevels . browserslistandtargetsserve different purposes.browserslistenables transpilation for the project.targetsdecides which transpilation applies to which output. Withoutbrowserslist, the legacy target has no transpilation to apply.- The modern browser never downloads the legacy bundle. The
nomoduleattribute tells the browser to skip the script entirely, so the larger transpiled output is not parsed or executed by browsers that do not need it . - The output directories are separated by default.
dist/modernanddist/legacykeep the two bundles apart. ThedistDirfield or an exact filename can override the default structure .
Remember: Differential bundling is the mechanism that lets a single codebase serve both a modern browser and a legacy browser without compromise. The modern browser gets the source syntax, which is smaller and faster. The legacy browser gets the transpiled syntax, which runs where the source would fail. The <script type="module"> tag is the signal, the browserslist field is the enabler, and the targets configuration is the definition of the two outputs. Parcel handles the fallback injection, so the developer writes one script tag and gets two bundles.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!