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.
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
- The page loads only the main bundle. The chart code is not downloaded yet.
- The user clicks "Show report". import() starts downloading the chart chunk and returns a pending promise.
- The chunk arrives and runs once. The promise resolves to its module namespace.
- 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?
A module with top-level await is not finished until the await settles, and its importers wait for it. So one slow top-level await delays every module that depends on it. It is tempting to think await only pauses its own file.
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.”
Follow-up What does dynamic import() give you that static import does not?
Follow-up How do you avoid the wait when the user clicks?
Follow-up What is an import map?
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()andpreload()at once call the loader only once. - After a failed load, the next
get()calls the loader again.
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
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 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
<script type="importmap">, maps bare names to URLs; Chrome 89, Firefox 108, Safari 16.4 Source: MDN: importmap.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?