tooling.report

feature

One entry point per page

Are common entry dependencies extracted into a shared bundle?

Person in shorts with blue hair walking left

Introduction

The use of multiple entry points in bundlers is often synonymous with multiple pages. As an example, imagine an "index" page and a "profile" page, each with their own script. Both scripts happen to make use of a large common library. Instead of being bundled into each page's script, the library should be split out into a bundle shared by both.

Splitting common code out of entry bundles reduces a site's total JavaScript size, and enables the common code to be loaded from the cache when moving between pages.

The Test

This test simulates a two page website by bundling two entry modules, index.js and profile.js. Both entry modules depend on a utils.js module.

index.js

import { logCaps } from './utils.js';
import { exclaim } from './exclaim.js';
logCaps(exclaim('This is index'));

profile.js

import { logCaps } from './utils.js';
logCaps('This is profile');

utils.js

export function logCaps(msg) {
  console.log(msg.toUpperCase());
}

exclaim.js

export function exclaim(msg) {
  return msg + '!';
}

The result should be three scripts: One for the "index" page, one for the "profile" page, and one containing the logCaps() function they both need.

Some bundlers prefer to duplicate modules to avoid creating an extra chunk unless some condition is met, such as a size threshold. That is acceptable, since these entry points are running in different JavaScript realms (pages, in this case). However, to pass the test, the bundler must at some point perform code splitting, either through configuration, or passing some size threshold.

Conclusion

browserify

Browserify's factor-bundle plugin supports extracting shared modules into a common bundle, alongside the bundles for each entry module.

There are two important caveats to consider when using factor-bundle: only a single "common" bundle can be generated for all entry bundles, and it can't be used with tinyify/browser-pack-flat. This means Browserify users must currently choose between having optimized "scope-collapsed" bundles with duplication, or multiple "scope-preserved" bundles without duplication.

parcel

When multiple HTML files are passed to Parcel for processing, the scripts in each page have their common dependency modules extracted into shared bundles if the shared module is greater than 30kb.

If the shared module is less than 30kb, it's duplicated in each entry point. There's no documented way to configure this threshold.

Issues

rollup

Rollup accepts multiple entry modules specified as an Array or Object, and produces a corresponding output bundle for each, splitting common code into other chunks by default.

Rollup aims to create as few chunks as possible, but it will never duplicate module definitions in a single build.

webpack

Webpack does not perform automatic code splitting by default.

Setting the optimization.splitChunks.chunks configuration option to "all" enables the generation of shared bundles for common dependencies. While this works in many applications, it's important to understand that splitChunks does not automatically create bundles for all shared dependencies by default. Instead, a set of threshold conditions are used to determine when common dependencies should be extracted into a shared bundle. The optimization.splitChunks.minSize option can be used to change the size threshold, which defaults to 30k.

If the code splits, Webpack will not load dependencies by default. Webpack currently assumes entry bundles have no dependencies, which means our shared bundles would need to be explicitly loaded on a page. There are many ecosystem tools available to generate the necessary <script> tags, including HtmlWebpackPlugin, which is used in this test.

Another solution involves runtimeChunk. Webpack relies on a runtime module loader and registry, and the optimization.runtimeChunk option provides a way to control where that code should be generated. Setting it to "single" produces a runtime.js bundle that, when added to the page before the entry bundle, will load any shared bundles on which it depends.