←All posts

import defer Reaches Chrome: Load Modules Now, Pay for Them Later

import defer lets the browser fetch and link a module up front but postpone running it until first use. Here is how it works, where it beats dynamic import, and where it quietly does nothing.

import defer Reaches Chrome: Load Modules Now, Pay for Them Later

Most startup JavaScript is not slow because of how it downloads. It is slow because of what it does the moment it arrives: top-level code that builds lookup tables, registers handlers, parses configuration, and initializes features the user may never touch. Bundle splitting helps with transfer size, but once a module is in the graph, it runs.

import defer is a new import form that separates two things we have always treated as one: getting a module and running it. Chrome's intent to ship targeted Chrome 155, and web.dev's September platform roundup lists it among the features in the Chrome 156 beta. Either way, it is landing now, and it is worth understanding before it shows up in your toolchain.

What it does

The syntax is a namespace import with a defer keyword:

import defer * as squares from "./squares.js";

According to MDN, the module and its dependencies are still fetched, parsed, and linked up front. What is postponed is evaluation, the execution of the module's top-level code, until something touches a property of the namespace.

Here is the example from MDN, slightly condensed:

// squares.js
console.log("Initializing squares");
const squares = Array.from({ length: 10000 }, (_, i) => i ** 2);
export function getSquare(i) { return squares[i]; }
// main.js
import defer * as squares from "./squares.js";

console.log("Ready");
export function showSquare(i) {
  console.log(squares.getSquare(i));
}
showSquare(3); // logs "Initializing squares", then 9
showSquare(4); // logs 16; initialization is not repeated

squares.js is on the wire and linked before main.js runs, but its 10,000-element array is not built until showSquare(3) is called. If that function never runs, the work never happens. The TC39 proposal describes the deferred module as a new top-level execution node, "just like a dynamic import does, except executing synchronously."

Why not just use dynamic import?

import() already lets you postpone work, but it changes the shape of your code. It returns a promise, so every caller becomes async, loading states creep into the call chain, and a function that used to be synchronous no longer can be.

import defer keeps the call site synchronous. The first property access does the evaluation inline, and subsequent accesses are ordinary reads. That makes it a good fit for code paths that are rarely used but must stay sync: a formatter, a validator, a parser for an uncommon file type.

The trade-off is that the bytes are not deferred. Because the module is fetched up front, you save main-thread execution time, not network time. The blink-dev message itself only claims a qualitative benefit, to "reduce the startup impact of modules", and gives no figures. Measure before you promise anyone a number.

What triggers evaluation

This is where deferral gets quietly defeated. MDN lists the operations that evaluate the module: property reads, in checks, Object.keys and other key enumeration, Object.getOwnPropertyDescriptor, and delete. Destructuring at the top level counts too:

import defer * as report from "./report.js";

const { createReport } = report; // evaluates immediately, deferral wasted

Reading then does not trigger evaluation, which keeps the namespace from being mistaken for a thenable. Passing the namespace around, assigning it, or comparing it also does nothing. So you can hand the deferred namespace to another function and let that function decide whether to pay.

If you want to keep the deferral, access members at the point of use, inside the function that needs them, as in showSquare above.

Behaviors to know before you adopt it

Top-level await is not deferred. A property read is synchronous, so it cannot wait on async work. MDN notes that modules using top-level await are evaluated eagerly, along with the dependencies needed to run them. If you import such a module with defer, you get no benefit.

Errors are loud and sticky. Loading, parsing, and linking errors still fail up front. Errors thrown during deferred evaluation surface at the first access, synchronously, so try...catch works around the access. The error is cached, and later accesses rethrow it without re-running the module. A failure that used to crash at startup can now appear in the middle of a user interaction, so cover those paths in tests.

Side effects move. The MDN page warns against deferring modules whose side effects must happen first, such as polyfill installers. Anything that registers global state or patches a prototype should stay as a regular import.

Only namespace imports. import defer { x } from "./mod.js" is not valid syntax. The proposal states that the form only ever returns a namespace object, and deferred re-exports are not in the current version.

It is not Baseline yet. MDN marks the feature as experimental with limited availability. The blink-dev message lists Gecko as positive and WebKit as shipped or shipping, but you should check current support before relying on it without a build step.

How to try it safely

Start with an audit rather than a rewrite. Open the Performance panel, record a cold load, and look for long tasks in module evaluation that belong to features behind a click or a route. Good candidates share three traits: heavy top-level work, no top-level await, and a small number of call sites.

Then convert one module:

  1. Change import * as x from "./x.js" to import defer * as x from "./x.js".
  2. Make sure nothing destructures or enumerates the namespace at module scope.
  3. Confirm that ./x.js has no side effects the rest of the app depends on.
  4. Re-record the trace and compare the startup task before and after.

If your code is compiled, check how your bundler and TypeScript setup treat the new syntax before shipping it; this post does not assume either handles it, and a toolchain that rewrites or rejects the keyword would change the picture.

Takeaways

  • import defer splits "fetch and link" from "evaluate", and runs evaluation on first property access.
  • It keeps call sites synchronous, which is its advantage over import(). It does not reduce download size.
  • Top-level destructuring, key enumeration, and top-level await all cancel the benefit.
  • Errors and side effects move to first use, so test those paths and leave polyfills alone.
  • Treat it as a progressive improvement: profile first, convert modules with heavy, rarely used initialization, and measure again.

Sources: Chrome intent to ship (blink-dev), MDN: import defer, TC39 proposal-defer-import-eval, web.dev: New to the web platform in September.

←Back to all posts
Next step

Need help shipping software?

Tell us what you're trying to build. A discovery call, a one-page summary within 48 hours, a proposal within a week.

Response · 48h·NDA on request·US contracts only