ZYVOPMulti-Platform Sync
SeriesAI NewsWhy ZyVOPJoin Discord
LoginGet Started
ZYVOP
The Developer Publishing Hub
SeriesAI NewsPreview My BlogPrivacyTermsGuidelinesDMCACommunity
© 2026 ZyVOP
Homeimport defer: Lazy Modules Without Going Async

import defer: Lazy Modules Without Going Async

September 22, 2026•
3 min read
Danny Holloran
Danny Holloran
Originally published ondanholloran.me
Senior Developer·
import defer: Lazy Modules Without Going Async
#tooling#JavaScript#node#performance

Every codebase has that one import. A syntax highlighter, a PDF renderer, a date-formatting library with a locale table the size of a small novel. It sits at the top of a file, it gets evaluated the moment the module graph runs, and it is needed on exactly one code path that most users never hit.

The usual fix is await import(). It works, but it charges rent: the function that needed the module becomes async, and so does its caller, and its caller's caller. You wanted to move some work later in time, and instead you rewrote a call stack. import defer is the piece that was missing, and it is riding along with ES2026.

Linked up front, evaluated on first touch

The syntax is a single keyword in one position:

import defer * as ts from "typescript";

function compileFile(path) {
  // The typescript module graph evaluates here, on first property access.
  return ts.createProgram([path], {});
}

Nothing from typescript runs at startup. The module and its dependencies are still resolved, fetched, parsed, and linked, so a missing file or a bad named import still blows up immediately rather than lurking. What is deferred is evaluation: the top-level code only runs the first time you read a property off ts. If compileFile is never called, that graph never executes.

That distinction is the whole value proposition. import() defers everything, which is why it has to be async. import defer defers only the synchronous part that can safely be moved, so property access stays synchronous and nothing above it needs to change colour.

Only the namespace form works. There is no import defer { createProgram } from "typescript" and no default form, because the namespace object is the thing doing the work — evaluation is triggered by a property read, and a destructured binding has nothing left to intercept.

The parts that will surprise you

The deferred namespace is effectively a proxy, and the list of operations that trip evaluation is broader than "read a property". Reading a key triggers it, but so does "value" in ns, Object.keys(ns), a for...in, Object.getOwnPropertyDescriptor, and even delete ns.value or Object.defineProperty on it — the operation fails, since the namespace is sealed, but evaluation happens anyway. Object.isSealed() and Object.isFrozen() enumerate keys, so those count too.

Which also means this defeats the whole thing:

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

const { getSquare } = squares; // evaluates immediately — you read a property

Three behaviours are worth committing to memory:

Top-level await opts out. Reading a property is synchronous, so it cannot wait on an async module. A module containing top-level await is evaluated eagerly, along with whatever its evaluation requires. The rest of the graph can still stay deferred, but do not assume defer bought you anything if the target awaits at the top level.

Side effects move. If a module installs a polyfill, registers a custom element, or patches a global, deferring it means that side effect now happens at an unpredictable moment, or never. Those modules should stay on a plain import.

Evaluation errors are cached and synchronous. If the deferred module throws while initializing, the throw comes out of whatever expression touched the namespace, catchable with a normal try...catch. Every later access rethrows the same error rather than retrying.

One more detail that saves a debugging session: the namespace deliberately does not expose an export named then, even after evaluation. Without that carve-out, handing the namespace to Promise.resolve() would look like a thenable and force evaluation as a side effect of promise resolution.

Where you can actually use it

Tooling got there first, as usual. TypeScript has understood the syntax since 5.9, and Babel, webpack, and Prettier all handle it. Bun and Deno ship it enabled by default. In browsers, V8 has had it behind a flag with an Intent to Ship filed in September 2026, WebKit has it implemented, and Gecko has signalled support — so treat it as landing rather than landed, and check the compatibility table before you rely on it in production.

The honest use case today is server-side and CLI code, where Bun, Deno, and a bundler give you a straight path. Go find the heaviest module on your cold-start path that is only needed conditionally, swap the import, and measure. If it turns out the module has top-level await, or installs something on import, you have learned something useful about your dependency either way.

Comments (0)

Join the discussion by logging into your account.

No comments yet. Be the first to comment!

Danny Holloran
Danny Holloran

Senior Developer

Senior Frontend & Fullstack Developer with 14+ years building performant, scalable web applications. Passionate about architecture, mentorship, and finding the right tool for the job.

Subscribe to Danny Holloran's Newsletter

Direct email dispatches when new stories are published. Zero algorithms.

Danny Holloran
Like
Love
Clap
Fire
Party
Wow

More from Danny Holloran

View profile

Uint8Array toBase64 and toHex: Stop Round-Tripping Bytes Through btoa

JavaScript can finally turn bytes into base64 and hex (and back) without the String.fromCharCode and btoa dance. Here's how Uint8Array's new encoding methods work and where they replace the helpers you've been copy-pasting.

4 minSep 25

Turbopack's Persistent Build Cache Only Helps If Your CI Keeps It

Next.js 16.3 turned on Turbopack's persistent build cache by default and published numbers as high as 5.5x. Most CI pipelines will see exactly zero of that, because the cache is a directory and containers start empty.

4 minSep 21

CSS reading-flow: Fix the Tab Order Your Layout Broke

Grid and flexbox let you rearrange a layout without touching the DOM, which quietly breaks keyboard navigation. The reading-flow and reading-order properties finally let CSS tell the browser which order actually counts.

3 minSep 19

WebNN: The Only Web API That Can Reach Your NPU

Almost every laptop shipped in the last two years has a neural processing unit sitting idle. WebNN is the only web standard that can actually talk to it, and it just hit an updated Candidate Recommendation.

4 minSep 15

GraphQL @oneOf: Exactly One Input, Enforced by the Schema

OneOf Input Objects landed in the September 2025 GraphQL spec, which means the exactly-one-of-these-arguments rule you've been enforcing in resolver code is now something the type system can do for you.

3 minSep 14