How-to library · Developer docs

How-to library

Task-shaped pages: a title that starts with a verb, the situation in a few lines, the steps, the working code in both engines, and why this way rather than another. Found from a search, done, and left.

Cut a bundle in half with dynamic imports

Find the heavy library that most visits never use, load it only when it is needed, and measure what every page saved.

Context

Context. The site-wide bundle has grown past its budget, and most of it is one or two libraries that only some pages, or only some clicks, need: a lightbox, a date picker, a map, a phone-number field. Every visitor downloads and runs them on every page. A dynamic import() moves each into a separate file that the browser fetches the first time the code is used. On a bundle where one library is half the weight, that halves what every page carries.

Steps

  1. Measure the bundle as it travels: gzip -c dist/app.js | wc -c, and note the number.
  2. Find what is heavy and rarely used. A bundle analyser shows each module's share; the browser's Coverage panel shows how much of the file a typical page actually runs.
  3. Replace the static import of that library with a dynamic one, inside the code path that needs it, triggered by the element's presence or by the first interaction.
  4. Name the chunks and set where they are served from, so the browser can find them and they cache well.
  5. Remove every other static import of the same module, including ones used only for a type, or the split does nothing.
  6. Build, measure again, and check the page that uses the library still works with the Network panel open: the chunk should load only there.

Code

Before, the lightbox library is in every page's bundle:

JavaScript
// src/js/gallery.js
import Lightbox from 'some-lightbox';
document.querySelectorAll('[data-gallery]').forEach(el => new Lightbox(el));

After, it is fetched only when a gallery is on the page, and only once:

JavaScript
// src/js/gallery.js
const galleries = document.querySelectorAll('[data-gallery]');
if (galleries.length) {
  import(/* webpackChunkName: "lightbox" */ 'some-lightbox')
    .then(({ default: Lightbox }) => galleries.forEach(el => new Lightbox(el)));
}
JavaScript
// webpack.config.js (production)
output: {
  path: path.resolve(__dirname, 'dist'),
  publicPath: '/dist/',                               // where the site serves the chunks
  filename: '[name].js',
  chunkFilename: 'chunks/[name].[contenthash:8].js'   // a new name whenever the content changes
}

The page template loads the entry exactly as before; the chunk is fetched by the entry from the same origin, so it needs no tag of its own:

Template code
<script nonce="{$_inlineScriptNonce}" src="/dist/app.js?v={$_currentSite->getLastVcsHash()}" defer></script>
<script nonce="{{ _inlineScriptNonce }}" src="/dist/app.js?v={{ _currentSite.getLastVcsHash() }}" defer></script>

Commit the chunk files with the build: the platform builds nothing (Build before you push).

Why this and not that

A dynamic import is worth it for code that is large and used by a minority of visits; for a few kilobytes used everywhere it adds a request and saves nothing. A separate entry per page type is the better tool when whole page types need different code, such as the checkout (entry splitting); a dynamic import is for the code inside one page type that only some visits need. The hashed chunk name means a changed chunk gets a new URL, so no browser runs an old chunk against a new entry. The chunk loads under the site's content security policy because it comes from the site's own origin and is loaded by a script that carries the nonce (Dynamic imports).

Go deeper

Was this page helpful?

Your answer goes to the docs team.