| | |

Parcel 07 ๐Ÿ“ฆ Differential Bundling โ€” Modern & legacy bundle generation

โฑ๏ธ Estimated read: 10 min | |

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

FieldPurpose
targetsNamed output configurations
targets.<name>.engines.browsersBrowser query set for that target
targets.<name>.distDirOutput directory override
Top-level <name>Exact filename for single-entry target

Default output directories

TargetOutput
moderndist/modern/
legacydist/legacy/

The differential serving mechanism

ElementRole
<script type="module">Triggers the modern bundle
<script nomodule>Fallback for legacy browsers
browserslistEnables transpilation for the legacy target
targetsNames the outputs and defines their engines

Browser behavior

BrowserModule scriptNomodule script
Modern (supports ESM)Downloads and runsIgnores (no download)
Legacy (no ESM)IgnoresDownloads 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

PitfallWhy It HappensFix
No nomodule fallback generatedSource HTML uses classic script tagUse <script type="module">
Legacy bundle is identical to modernbrowserslist missing or targets identicalDeclare browserslist and distinct targets
Modern browsers download the legacy bundleThe nomodule attribute is missingCheck the output HTML; ensure type="module" is in the source
Build fails on IE 11 syntaxbrowserslist does not include the legacy browserAdd it to the browserslist field
Outputs overwrite each otherSingle target or distDir collisionUse 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

ItemValue
Trigger<script type="module"> in the HTML
Fallback<script nomodule> injected automatically
Targets fieldtargets in package.json
Default directoriesdist/modern, dist/legacy
Browser selectionModern: module; Legacy: nomodule
Requirementbrowserslist for the legacy transpilation
BenefitSmaller 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 module and nomodule attributes .
  • The <script type="module"> tag is the trigger. Parcel detects it in the HTML and generates the nomodule fallback automatically, based on the browserslist targets. A classic <script> tag does not trigger differential serving .
  • The targets configuration names the outputs and defines their engines. A modern target and a legacy target are the standard pair. Each target’s engines.browsers determines the syntax that the output preserves or downlevels .
  • browserslist and targets serve different purposes. browserslist enables transpilation for the project. targets decides which transpilation applies to which output. Without browserslist, the legacy target has no transpilation to apply.
  • The modern browser never downloads the legacy bundle. The nomodule attribute 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/modern and dist/legacy keep the two bundles apart. The distDir field 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.

Was this helpful?


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!