Guide 03 · Developer docs

The theme repo and the asset pipeline

Everything a developer changes on a Core dna site lives in its theme repository. This guide covers the repository and its build, printing records in templates, stylesheets and client-side code, and the parts of a site around the main content.

L1–L2
Unit 04 · Client-side code

Dynamic imports

A dynamic import() turns heavy code into a chunk that downloads only when it is needed. Splitting out a phone-number library and a payment provider's code in production repositories, the measurement that justifies a split (and the one that showed a split saved 2 KB), and what silently undoes one.

Who this is for. The developer whose bundle carries a heavy library that only some pages, or some visitors, use. Afterwards you can split it into an on-demand chunk, prove the split is worth it before you make it, and keep it split.

How it works

A static import puts a module into the bundle. A call to import() makes webpack write the module, and whatever only it uses, into a separate chunk, and returns a promise that downloads and runs the chunk the first time the call is made. Give chunks a stable, content-hashed name so they cache well and change name when their content does:

JavaScript webpack.common.js (extract)
output: {
  chunkFilename: 'chunks/[name].[contenthash:8].js'
}

Chunks are loaded by the bundle that asked for them, from the output folder, so they need no template change and no script tag. Under a policy with 'strict-dynamic', a chunk loaded by a script that carries the nonce is trusted in turn; under the default policy it is allowed because it comes from the site itself.

Example: a library only some pages need

A production Twig repository loads its international phone-number field only on pages that have a telephone input:

JavaScript src/js/helpers/international-phone-inputs.ts (shortened)
export const initInternationalPhoneInputs = () => {
  const inputs = document.querySelectorAll('input[type=tel]');
  if (!inputs.length) {
    return;
  }

  import('intl-tel-input').then(({ default: intlTelInput }) =>
    inputs.forEach((input) => intlTelInput(input, {
      utilsScript: '/dist/chunks/intl-tel-input/utils.js'
    }))
  );
};

The library's formatting utilities, which it fetches itself once initialised, are 56 KB gzipped. Pages without a phone field download neither.

Example: a payment provider, on the payment step

Two production commerce sites load each payment method's code only when the visitor chooses that method:

JavaScript src/static/js/features/checkout.js (extract)
function loadProcessor(type) {
  switch (type?.toLowerCase()) {
    case 'stripe':   return import(/* webpackChunkName: "stripe-pay" */ './payment-methods/stripe.js');
    case 'mock-pay': return import(/* webpackChunkName: "mock-pay" */ './payment-methods/mock-pay.js');
    default:         return Promise.reject(new Error(`Unsupported payment type: ${type}`));
  }
}

Measured, the saving on one of them is small: its Stripe chunk is about 2 KB gzipped, because the @stripe/stripe-js package is a loader, and Stripe's library itself is fetched from Stripe's servers when loadStripe() is called. The split that matters there is calling loadStripe() only on the payment step, so the provider's own script is not downloaded on every page of the checkout. The provider's host also has to be in the site's Content Security Policy: The CSP nonce.

Measure before you split

The obvious candidate is often not the heavy one. Before splitting:

  1. Build for production and record the gzipped size of the bundle (Measuring a candidate).
  2. Find what the module really weighs: webpack's --json stats, or webpack-bundle-analyzer, show each module's share.
  3. Split only if the module is a large share of a bundle that many pages load and few use. A split of a few kilobytes adds a request and a delay at the moment the feature is used, for no gain.

What silently undoes a split

  • A static import of the same module anywhere in the bundle, even one used only for a constant, pulls it back into the bundle; the dynamic import then loads nothing new. In TypeScript, import types with import type.
  • A shared dependency imported statically elsewhere stays in the bundle; only what the split module alone uses moves out.
  • A missing chunk on the server, because the output folder was not committed in full, fails only when the feature is used. Check the Network panel on the page that triggers it.
Go deeper

Was this page helpful?

Your answer goes to the docs team.