tooling.report

feature

Multiple entry points per page

Can multiple entry points be used without duplicating modules?

Person in shorts with blue hair walking left

Introduction

Although it's common to have a single entry point per page, other models loosen this association. For instance, you might have one entry point for analytics loaded alongside the main entry point, allowing for each to be loaded at a different priority. Or, you may end up with one entry point per independently-loadable component.

Depending on the tool and configuration, modules relied on by multiple entry bundles can be extracted into common bundles used across multiple pages. It is also sometimes the case that modules are used on too few pages to justify being extract into a shared bundle, so a bundler may inline the module into each entry bundle.

Whether extracted or inlined, it's important that a module never be instantiated multiple times - both ECMAScript Modules and CommonJS Modules specify that a module must only be instantiated once per JavaScript context. This guarantee allows a module's top-level scope to be used for global state, shared across all usages of that module. Many libraries rely on this assumption in order to implement important cross-cutting concerns like memoization, queues and plugin registries. Breaking this assumption and instantiating a module multiple times will introduce bugs or inefficiencies in technically correct code.

The Test

This test checks to see if a module used by two different entry bundles will be instantiated only one time. The shared module exports an object with a numeric count property, and each entry module imports the object and increments that property:

index.html

<!DOCTYPE html>
<script src="component-1.js"></script>
<script src="component-2.js"></script>

component-1.js

import obj from './obj.js';
obj.count++;
console.log('component-1', obj.count);

component-2.js

import obj from './obj.js';
obj.count++;
console.log('component-2', obj.count);

obj.js

export default { count: 0 };

If modules are correctly instantiated only once, both entry modules receive a reference to the same object exported by the shared module, and the .count property of that object is incremented twice, resulting in its value being 2. However, if the common dependency module is instantiated separately by each entry bundle, each will increment its own .count property and the result will be two objects each with a count value of 1.

To pass this test, the logged output should be:

component-1 1
component-2 2

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

Parcel inlines obj.js twice, which makes the code behave differently compared to when using native ES modules.

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

When you opt-in to code splitting, Webpack may duplicate modules between chunks depending on heuristics. If this happens, then you can end up with multiple instances of the same module, each with their own state that can easily get out of sync.

However, you can set optimization.runtimeChunk to "single". This moves Webpack's runtime module loader into its own bundle rather than being inlined into each entry, creating a global registry that allows code-splitted modules to be shared between entries. This doesn't prevent Webpack from copying module code between entry points, but it prevents it creating two instances of the same module at runtime, while reducing the number of HTTP requests needed to load modules for a given page.

This test is a "partial" pass, since runtimeChunk:"single" is required to ensure correct module instantiation, but it is disabled by default and not documented in Webpack's code splitting guide.

The optimization.splitChunks.minSize option can be used to change the size threshold for creating a chunk, which defaults to 30k.