Engrain by ByteRush

JS.10.03 Mid-level· 10 minCore — asked often

Dynamic import, top-level await and code splitting

Load code only when it is needed with import(). How bundlers split routes, why top-level await delays every importer, and what import maps and import attributes do.

Comes up in: JavaScript, Web fundamentals, Frontend system design

Learn#

Kiran’s dashboard takes 6 seconds to start on a cheap phone. A 310 KB chart library is in the main bundle, but only one user in ten opens the report tab.

A static import at the top of a file always loads. A dynamic import, import("./chart.js"), loads a module when that line runs. It returns a promise of the module’s namespace object:

console.log("page ready");
const showReport = async () => {
  const chart = await import("data:text/javascript,console.log('chart module runs'); export default (d) => d.join(' | ');");
  console.log(chart.default([3, 5, 8]));
};
await showReport();
await showReport();

Output (verified)

page ready
chart module runs
3 | 5 | 8
3 | 5 | 8

The module ran once; the second call reused it.

Bundlers see each import() and put that module in its own file, a “chunk”. This is code splitting. Routes, modals, editors and charts are the usual places to split.

Loading the chart only when it is neededStep 1 of 4

Network

  • GET main.js (40 KB)

Console

    empty

    The page loads only the main bundle. The chart code is not downloaded yet.

    Read the steps as text
    1. The page loads only the main bundle. The chart code is not downloaded yet.
    2. The user clicks "Show report". import() starts downloading the chart chunk and returns a pending promise.
    3. The chunk arrives and runs once. The promise resolves to its module namespace.
    4. A second click reuses the loaded module. No new request.

    Predict#

    Predict first

    config.js uses top-level await to fetch settings, which takes 2 seconds. app.js imports config.js. When does app.js start running?

    Interview angle#

    Say it like this — about 30 seconds

    “import() loads a module at runtime and returns a promise of its namespace, so I can load code only when it is needed. Bundlers turn each import() into a separate chunk: that is code splitting, used for routes, modals and heavy libraries. I preload on hover or idle time to hide the delay, and I handle load failures. Top-level await is useful, but it blocks every importer until it settles, so I keep it out of shared modules.”

    Stays on your device. Nothing is uploaded.
    Follow-up What does dynamic import() give you that static import does not?
    Loading on demand, a computed module path, and use from classic scripts and CommonJS code.
    Follow-up How do you avoid the wait when the user clicks?
    Start the import earlier: on hover, on focus, when idle, or with a modulepreload link.
    Follow-up What is an import map?
    A JSON block in the page that maps bare names such as “react” to URLs, so browsers can load them without a bundler.

    Practice#

    On your own

    Write lazyModule, the helper behind “load the chart on first click, preload on hover”.

    Done when

    • get() resolves to the default export, or to the module itself if it has none.
    • Many calls to get() and preload() at once call the loader only once.
    • After a failed load, the next get() calls the loader again.
    Your solution
    Ctrl/⌘ + Enter

    Remember#

    Try to answer each card before you open it. Then add them to your reviews.

    What does import("./x.js") return?

    A promise that resolves to the module's namespace object.

    What is code splitting?

    Putting dynamically imported modules in separate chunks that load on demand.

    What happens to importers of a module that uses top-level await?

    They wait until that await settles before they run.

    What does an import map do?

    Maps bare module names, such as react, to URLs in the browser.

    Go deeper

    Four layers. Stop at the one your interview needs; each ends with a quick check.

    The core ideaEvery round

    import() works in any code, including classic scripts. The path can be computed, such as import(`./locales/${lang}.js`), but a bundler needs a fixed folder and pattern in the path to know which files to split; a fully computed path cannot be bundled.

    Check yourself: Where can you use import()?

    Anywhere: in functions, classic scripts and CommonJS, not only in modules.

    How it works underneathSenior rounds

    Top-level await makes a module asynchronous. Its parents in the graph wait until it settles, while unrelated modules can still run. Safari 15–26 supported it only partly; full support came in Safari 27.
    Use it for one-time setup in entry files, not in shared utilities.

    Check yourself: Why does one top-level await slow the whole page?

    Every module that imports it, directly or indirectly, waits for it to settle.

    Edge cases and trapsWhere people slip
    Import attributes tell the engine what a file is: import data from "./data.json" with { type: "json" }. Without the attribute, browsers refuse to load JSON as a module, for safety. Support: Chrome 123, Firefox 138, Safari 17.2, Node.js 20.10.0. The older assert syntax is deprecated.
    Check yourself: Which syntax loads JSON as a module?

    import data from './data.json' with { type: 'json' }

    In productionStaff-level answers

    An import map, <script type="importmap">, maps bare names to URLs; Chrome 89, Firefox 108, Safari 16.4 Source: MDN: importmap.
    In React, lazy() with Suspense wraps import() for components. Add <link rel="modulepreload"> for chunks you know users will need Source: MDN: import().

    Check yourself: What is a modulepreload link for?

    Downloading a module and its dependencies early, before the import runs.

    Finished the five steps? Mark it, and its cards join your reviews.

    Last checked 2026-10-31. Spot a mistake? Use “Report an error” below — fixes are listed on the corrections page.

    Interview questions this answers

    • What does dynamic import() give you?