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:
- Change
import * as x from "./x.js"toimport defer * as x from "./x.js". - Make sure nothing destructures or enumerates the namespace at module scope.
- Confirm that
./x.jshas no side effects the rest of the app depends on. - 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 defersplits "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
awaitall 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.