completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Dynamic import & top-level await

Load JavaScript modules on demand, understand dynamic import promises, code splitting, top-level await, import.meta, import.meta.resolve, and current module-loading proposals.

By the end, you can
  • 01
    Load code when you need itUse import() to load a module later, read its exports after the promise is ready, build the file name in code, and catch a failed load.
  • 02
    Know who waitsExplain why a module runs only once, how code splitting keeps the first download small, and which modules wait for top-level await.
  • 03
    Find where a module livesUse import.meta.url and import.meta.resolve(), and recognize import defer and import source as proposals that are not ready yet.

Load a module only when you need it

In the last lessons, you wrote import at the top of a file. That is a static import. "Static" means fixed: the browser loads those files before your code runs. That is right for code you always need.

Some code is not always needed. Think of a chart screen, a text editor, an admin page, or a second language. Many visitors never open them. If the page loads them at the start, the first screen is slower for everyone.

Dynamic import solves this. You write import(specifier), like a function call. Here is how it works:

  • It runs only when your code reaches that line.
  • The specifier is the string that names the file, like "./charts.js". Your code can build that string while the program runs.
  • It returns a Promise. When the file is ready, the promise gives you the module's exports.
  • It works in normal scripts and in modules.
Real-life analogyCook every dish ahead, or cook it when someone orders

Picture a restaurant that cooks every dish on the menu before it opens the door. Food comes fast, but the kitchen cooks a lot of food that nobody orders. A smarter kitchen waits for an order. The guest gets an order number, and the dish comes out when it is ready.

In real life: The kitchen cooks every dish before it opens
In JavaScript: Static imports load every module before your code runs
In real life: A guest orders one dish only when they want it
In JavaScript: import() starts loading only when that line runs
In real life: The guest gets an order number while the dish cooks
In JavaScript: import() gives back a promise while the module loads
In real life: The dish arrives, ready to eat
In JavaScript: The promise gives you the module's exports

This lesson covers these tools, in this order:

  • import(), which loads a module when you ask.
  • Code splitting, which keeps extra code out of the first download.
  • Top-level await, which lets a module wait before it is ready.
  • import.meta and import.meta.resolve(), which tell a module where it lives.
  • import defer and import source, two new forms that are not ready for everyday use yet.

import() gives you a promise first

INTERACTIVE

Read the code first. import() does not hand you the exports straight away. It hands you a promise, and await waits for it. The promise then gives you the module namespace object. That is a read-only object with one property for each export of the module.

Read a default export and a named exportPop out in the code editor (opens in a new tab)JavaScript
const menu = await import("./menu.js");console.log(menu.default);console.log(menu.price);

In plain words, line by line:

  1. Line 1 loads menu.js and waits. Then it saves the module namespace object in menu.
  2. Line 2 reads menu.default. A default export is just a property named default. It prints tea.
  3. Line 3 reads the named export price. It prints 3.
import of ./menu.js gives a Promise card right away. An arrow marked await leads to a cart called menu, which holds two cards: default and price.import("./menu.js")Promiseright awayawaitthe exportsdefaultpricemenu
import("./menu.js") gives you a promise right away. After await, you get the module's exports in one object: menu.default is "tea" and menu.price is 3.
import() lab: the promise, the exports, the cache, and a failure
Import, read, import again, then failPop out in the code editor (opens in a new tab)JavaScript
const pending = import("counter");console.log(pending instanceof Promise);const counter = await pending;console.log(counter.next());const again = await import("counter");console.log(again === counter);await import("broken");
Logreal modules

Click a button to import a module.

Try it yourself

Each button runs real module code inside a small hidden page (an iframe). The modules are written into the page itself, so nothing is downloaded. The lesson shows a message only if it comes from that exact iframe.

The iframe is locked down with sandbox. It runs only the code shown here, never code you type, and it makes no network requests.

The lab shows four things to remember:

  1. The promise exists right away, before the module is ready. So you can show a "Loading" message while you wait, like with any promise from the Promises lesson.
  2. The promise gives you the namespace object, not the default export. To read a default export, write module.default.
  3. JavaScript keeps each module after it runs once. This is the module cache. A second import of the same URL gives you the same module, and its code does not run again. So its variables are shared, whether you load it with import or with import().
  4. If the file is missing, has a syntax error, or throws an error while it runs, the promise rejects, which means it fails. Put await import(...) inside try/catch to handle that.
You can build the file name in code

JavaScript allows import("./locales/" + language + ".js"). But a bundler, the build tool that joins and splits your files before the site goes online, needs to find every possible file ahead of time. So it may want the files in one known folder. Also, never turn text that a user typed into a file path. Pick from a list of known files instead.

Choose a language file from a fixed listPop out in the code editor (opens in a new tab)JavaScript
const loadLocale = {  en: () => import("./locales/en.js"),  hi: () => import("./locales/hi.js"),};const language = "en";const locale = await loadLocale[language]();console.log(locale.greeting);

In plain words, line by line:

  1. Lines 1 to 4 make an object called loadLocale. It lists every language file the app may load. Each value is a small function that imports one file.
  2. Line 5 sets language to "en".
  3. Line 6 calls only the English function, so only ./locales/en.js is loaded.
  4. Line 7 prints Hello.

Each file path is written out in full. So a bundler can find both files and make a separate file for each one.

Catch a module that fails to loadPop out in the code editor (opens in a new tab)JavaScript
try {  await import("./broken.js");} catch (error) {  console.log(error.message);}

broken.js throws an error while it runs. So the import on line 2 rejects. The catch block on line 3 catches the error, and line 4 prints feature failed. Without try/catch, nothing would handle the error.

Code splitting: load extra code later

STEP THROUGH

import() is part of the JavaScript language. Code splitting is what a bundler does with it. The bundler puts each dynamically imported module into its own file, called a chunk. Then the browser loads:

  • the small main part of the app, called the shell, right away;
  • each feature chunk later, when someone opens that feature.

This site works this way. Each lesson page is its own chunk. So opening this lesson does not load the code for every other lesson.

Real-life analogyPack a small bag for today

Carrying every bag everywhere slows you down. If you pack only what you need today, your first walk is light. On beach day, you go and get the beach bag.

In real life: Carry clothes for a whole month everywhere
In JavaScript: One huge file that loads before anything runs
In real life: Pack today's clothes in a small bag
In JavaScript: The shell and the current page load first
In real life: Fetch the beach bag on beach day
In JavaScript: A feature chunk loads when the user opens that feature
A page window holds the shell, 42 KB, which loads first. Two separate boxes sit outside: charts, 120 KB, which arrives on click, and editor, 95 KB, which is not loaded yet.pageshell 42 KBloads firstseparate chunks120 KBchartson click95 KBeditornot yet
The page downloads only the 42 KB shell at first. The 120 KB charts file comes when the user opens Charts. The editor file stays on the server until someone needs it.
Code splitting: what loads now, and what loads later
Add a feature's bytes only oncePop out in the code editor (opens in a new tab)JavaScript
const loaded = new Set(["shell"]);let bytes = 42; async function openFeature(name) {  if (!loaded.has(name)) {    bytes = bytes + featureBytes[name];    loaded.add(name);  }  return name + " ready";}
Bytes downloaded42 KB
Shell42 KB now
Charts0 KB so far
Editor0 KB so far
Try it yourself

The shell downloads first. Charts and Editor are separate chunks, so their bytes download only if you open them.

This is a model with made-up sizes. A real bundler picks the chunk names and sizes.
Load charts only after a clickPop out in the code editor (opens in a new tab)JavaScript
async function openCharts(wasClicked) {  if (!wasClicked) return "waiting";  const charts = await import("./charts.js");  return charts.open();} console.log(await openCharts(false));console.log(await openCharts(true));

In plain words, line by line:

  1. Line 7 calls openCharts(false), which means "no click yet". Line 2 returns "waiting", and nothing is imported.
  2. Line 8 calls openCharts(true), which acts like a click.
  3. Line 3 loads charts.js and waits for it.
  4. Line 4 calls its open() export, so line 8 prints chart opened.

A real button would show a "Loading" message while line 3 waits.

Ask the browser to fetch a file earlyHTML
<link rel="modulepreload" href="./charts.js"><script type="module" src="./app.js"></script>

A modulepreload link asks the browser to download a module early and get it ready. It does not run the module. import() still decides when the module runs. Use it only when the user will probably open that feature soon. If they don't, you downloaded bytes for nothing.

The next replay shows two choices, step by step. Try each one:

  • Load on click downloads nothing until the user clicks.
  • Preload on hover starts the download when the pointer moves over the button. It costs bytes earlier, but the click may feel instant.
Replay: load the charts later
Step 0 of 5Ready
Your turn: follow the blue line

Pick when the charts file loads, then step through to see when its bytes are downloaded.

Running in
  1. script
Next: line 1
button.addEventListener("click", async () => {  const charts = await import("./charts.js");  charts.renderDashboard();});
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Choose when the Charts feature starts loading.

Changing the choice starts the replay again. Guess first: at which step is the 120 KB charts file downloaded?

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Top-level await: a module can wait before it is ready

INTERACTIVE

Normally you can use await only inside an async function. Top-level await means writing await directly in a module, outside any function. It works only in modules, not in normal scripts.

When a module uses it, the module pauses at that line. Then:

  • Modules that import it wait until it finishes.
  • Modules that don't import it keep going.
A module waits for its settingsJavaScript
const settings = await fetch("./settings.json").then((response) => response.json());export const theme = settings.theme;console.log(theme);

Line 1 fetches settings.json and waits for the answer. Line 2 exports theme from those settings. So a module that imports theme always gets a ready value. If the fetch fails, that import fails instead. This example needs a real settings.json file, so here you can only read it.

Real-life analogyA shop that opens only when it is ready

A shop owner fills the shelves before unlocking the door. People who came for that shop wait outside. The bakery next door doesn't wait for anyone. It opens on time.

In real life: The shop sets up before it unlocks the door
In JavaScript: The module waits for its setup before its exports are ready
In real life: Customers of that shop wait in line
In JavaScript: Modules that import it wait
In real life: The bakery next door opens on time
In JavaScript: Modules that don't import it keep going
Left: config.js holds an await card and pauses for 350 ms. An arrow marked then leads to app.js, which imports config.js and waits for it. Right: sibling.js imports nothing and runs right away.app.js imports config.jsawaitconfig.jspauses 350 msthenapp.jswaits for configno importssibling.jsruns right away
config.js pauses at its await. app.js imports it, so it waits. sibling.js imports nothing from it, so it runs right away.
Top-level await lab: who waits?
Three modules: config, app, and siblingJavaScript
// config.jsconst started = performance.now();await new Promise((resolve) => setTimeout(resolve, 350));export const config = "ready after " + Math.round(performance.now() - started) + "ms"; // app.jsimport { config } from "config";post("app can run only after config: " + config); // sibling.jspost("sibling module ran without waiting for config");
In orderreal modules
    Try it yourself

    app.js imports config.js, so it waits for config.js to finish. sibling.js doesn't import config.js, so it runs before the timer ends.

    The modules run in a small hidden page (an iframe). The lesson shows a message only if it comes from that iframe.

    Use top-level await with care:

    • It is a good fit for one-time setup that every export of the module needs.
    • It is a bad fit for a small helper file that every page imports. A slow wait there holds up every page that uses it.

    import.meta and import.meta.resolve()

    INTERACTIVE

    Sometimes a module needs to know where its own file is. import.meta is an object with facts about the current module. The browser or Node fills it in. Its most useful property is import.meta.url: the full URL of the current module file.

    Say an image sits in the same folder as your module. Write new URL("./tea.svg", import.meta.url) to get its full URL. new URL(name, base) joins a file name to a base address. The URL objects lesson covers it in depth.

    Find a file next to this moduleJavaScript
    const iconUrl = new URL("./tea.svg", import.meta.url);console.log(iconUrl.href.endsWith("/tea.svg"));

    Line 1 uses this module's URL as the base. So the result stays right even when the page URL changes. Line 2 prints true, because the full URL ends in /tea.svg. The same trick works for workers and other files near the module.

    A card ./tea.svg, the nearby file, plus a card import.meta.url, this module's URL, make a card .../tea.svg, the full URL../tea.svgnearby file+import.meta.urlthis module's URL.../tea.svgfull URL
    new URL() joins the file name ./tea.svg to this module's own address, import.meta.url. The result is a full URL that ends in /tea.svg.

    import.meta.resolve(specifier) answers one question: "What full URL would this name point to from this module?" It only works out the URL. It does not load the file.

    Real-life analogyimport.meta is the module's name badge

    Say a module needs a helper file, an image, or a worker script next to it. Guessing from the page's address can give the wrong place. Asking from the module's own badge gives the right one.

    In real life: A badge at an event says who you are and where you belong
    In JavaScript: import.meta.url says the URL of this module
    In real life: You ask staff, "Where is Room B from here?"
    In JavaScript: import.meta.resolve("./helper.js") asks where a name points from this module
    import.meta lab: a module's name badge
    Where does this module live?JavaScript
    console.log(import.meta.url);console.log(import.meta.resolve("./helper.js"));console.log(import.meta.resolve("missing-package"));
    Module reportreal browser
      Try it yourself

      The module prints its own URL. Then it works out the full URL of a nearby file. Last, it tries a bare package name that nothing points to, and that throws an error.

      import.meta.resolve() returns right away in current browsers and current Node. It only works out the URL; it doesn't load the file.
      resolve() answers right away

      In current browsers and current Node, import.meta.resolve() returns a string right away, not a promise. It can still throw an error. A bare name is a name with no ./ or / in front, like missing-package. It throws when nothing tells the browser where that package lives.

      import defer and import source are proposals

      TC39 is the group that decides on new JavaScript features. A proposal is an idea for a new feature. Each proposal moves through stages from 0 to 4. At Stage 4, it becomes part of JavaScript.

      The TC39 list puts two module proposals at Stage 3: Deferring Module Evaluation and Source Phase Imports. Stage 3 means the design is nearly done and browsers are trying it out. It does not mean every browser runs it today. So the code below is for reading only.

      Proposal syntax: read it, don't run itJavaScript
      // Display-only proposals, not runnable lesson codeimport defer * as charts from "./heavy-charts.js";import source parser from "./parser.wasm"; // Today: use import() or your bundler's documented features instead.

      What each line is for:

      • Line 2, import defer, loads the module and links it now. But it waits to run the module until your code first reads something from charts.
      • Line 3, import source, gives you the module before it runs, such as a WebAssembly module. WebAssembly is a fast, compiled kind of code that browsers can run.

      Normal browsers may not run either line without extra tools. Today, the tool that works everywhere is still import().

      Sort the need: static import, import(), or top-level await?

      INTERACTIVE

      The table compares the three tools. Read each row across: when the tool does its work, what you get, and when to use it.

      Three tools that look alike
      When you writeWhen does it do its work?What do you get?When should you use it?
      Static importIt loads the file before your module runs.You get the exported names, ready to use.Use it for code the module always needs.
      Dynamic import()It loads the file when that line runs.You get a promise for the module namespace object.Use it for extra features, pages, or a file name built in code.
      Top-level awaitIt pauses the module while the module runs.You get no new value by itself. The module just waits.Use it for one-time setup that the exports need.

      Then try the sorter. Use these rules:

      • The first screen can't work without it? Use a static import.
      • A click or a setting chooses it later? Use import().
      • The module's own exports are not usable until setup finishes? Think about top-level await.
      Choose the loading tool
      • You always need formatPrice before the page can show
      • Load an admin panel only after a click on the Admin button
      • Pick ./locales/${language}.js from a setting
      • Read a settings file before this module's exports can be used
      • Share fixed values that almost every module uses
      • Start a WebAssembly helper before exporting compress
      Try it yourself
      0 of 6 correct

      Put each need under the tool that fits it best.

      Choose a category for every card. You can change an answer at any time; Reset clears them all.

      Where you'll use this

      You'll use import() wherever a feature is useful but not always needed. Common cases:

      • Pages: settings pages, admin tools, and dashboards should not slow down the home page.
      • Heavy widgets: charts, text editors, maps, code highlighters, and video tools can load after a click.
      • Language files: load only the language the user picked.
      An editor that loads on clickPop out in the code editor (opens in a new tab)JavaScript
      button.addEventListener("click", async () => {  button.disabled = true;  const { openEditor } = await import("./editor-panel.js");  openEditor(document.querySelector("#draft"));}); async function loadLocale(language) {  return import("./locales/" + language + ".js");}

      In plain words, line by line:

      1. Line 2 turns the button off, so more clicks do nothing while the editor loads.
      2. Line 3 imports editor-panel.js, a fixed file name. The braces take the openEditor export out of the namespace object.
      3. Line 4 calls openEditor with the draft section.
      4. Line 8 builds the file name from a language setting.

      In a project with a bundler, keep all language files in one known folder. Then the bundler can make a chunk for each one.

      Common misconceptions

      Here is the most common mistake. Line 2 points out the bug, and lines 3 and 4 show the fix.

      A common bug: using the promise as the modulePop out in the code editor (opens in a new tab)JavaScript
      const chartPromise = import("./charts.js");// chartPromise.render() is a bug: await the promise first.const charts = await chartPromise;charts.render();

      "import() gives me the exports right away."

      It gives you a promise right away. Use await or .then() before you read the exports.

      "Every import() makes a fresh copy of the module."

      No. JavaScript keeps each module after it runs once. Importing the same URL again gives you the same module.

      "Top-level await freezes the whole page."

      No. Only modules that import the waiting module wait. Other modules keep going.

      "A bundler can split code at any function call."

      No. A bundler needs a module boundary to make a separate chunk. Most often, that boundary is an import().

      "Stage 3 syntax works in every browser."

      No. Stage 3 means the design is nearly done, not that every browser supports it. Check your tools before you use it.

      Practice: load code on purpose

      5 EXERCISES
      Exercise 1 · Warm-upPredict the first log

      What does the first console.log print?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const pending = import("data:text/javascript,export const answer = 32 + 10");
      console.log(pending instanceof Promise);
      pending.then((module) => console.log(module.answer));

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 2 · PracticeProve the module cache

        Run the program in a modern browser console or a Node module. What are the three log lines?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const source = "export let count = 0; export function next(){ count += 1; return count; }";
        const url = "data:text/javascript," + encodeURIComponent(source);
        const first = await import(url);
        console.log(first.next());
        const second = await import(url);
        console.log(second.count);
        console.log(first === second);

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 3 · PracticeFind the bug

          Look at the bug example above. Why is chartPromise.render() wrong? Then type the tool you should use to load an optional feature.

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 4 · ChallengePlan a dashboard split

            Picture a dashboard with a menu, charts, a map, and a text editor. Write down which parts you would load at the start, and which parts you would import later.

              Exercise 5 · ChallengeFind a file next to a module

              Write two lines that start a module worker from editor-worker.js. That file sits next to the current module.

                Quiz: dynamic imports and waiting modules

                7 QUESTIONS
                Lesson quiz · 7 questionsScore: first tries count
                1. Question 1 of 7What does import() give back right away?

                  Choose an answer to see the explanation.

                2. Question 2 of 7What does this code log first?

                  Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                  const pending = import("data:text/javascript,export const answer = 32 + 10");
                  console.log(pending instanceof Promise);
                  pending.then((module) => console.log(module.answer));

                  Choose an answer to see the explanation.

                3. Question 3 of 7What happens when you import the same module URL twice?

                  Choose an answer to see the explanation.

                4. Question 4 of 7Which modules wait for a module that uses top-level await?

                  Choose an answer to see the explanation.

                5. Question 5 of 7What is import.meta.url?

                  Choose an answer to see the explanation.

                6. Question 6 of 7What does import.meta.resolve() return in current browsers and Node?

                  Choose an answer to see the explanation.

                7. Question 7 of 7How should you treat this lesson's import defer and import source examples?

                  Choose an answer to see the explanation.

                Key takeaways

                • import() returns a promise. The promise gives you the module namespace object, which holds the exports.
                • You can build the file name in code, but bundlers work best with a fixed list or folder.
                • A module runs only once. Importing the same URL again gives you the same module.
                • Code splitting keeps the code for extra features out of the first download.
                • Top-level await works only in modules. It makes the modules that import it wait, and no others.
                • import.meta.url is the module's own URL. import.meta.resolve() turns a name into a full URL and returns a string right away.
                • import defer and import source are proposals. Learn to recognize them, but don't expect every browser to run them yet.

                Remember the one-liner.
                Static imports load what a module always needs. import() loads extra code when you ask for it. Top-level await lets a module finish its setup before others use it.

                Up next: Module resolution & import maps.

                CompleteFrontend Clear concepts. Working examples.