Organizing programs

How to dynamically import JavaScript modules

Learn how to dynamically import a JavaScript module when the path is built at runtime, using the await import() syntax instead of a static import statement.

8 minute lesson

~~~

Static imports are resolved when the module graph is built. Their path cannot be assembled at runtime:

import test from `${folder}/test.js`

Use the import() expression when loading depends on a runtime decision:

const module = await import('./charts.js')
module.renderChart(data)

import() returns a promise for the module namespace object. A default export is available as module.default; named exports are properties with their exported names.

A practical use is loading an expensive feature only after the user asks for it:

button.addEventListener('click', async () => {
  button.disabled = true

  try {
    const { openEditor } = await import('./editor.js')
    openEditor()
  } catch (error) {
    showMessage('The editor could not be loaded')
  } finally {
    button.disabled = false
  }
})

Bundlers can split a dynamic import into a separate chunk. This reduces initial JavaScript only if the feature is not immediately needed; it also introduces a network request and a failure path later in the interaction.

Avoid arbitrary user-controlled paths. Bundlers must know which files may exist, and unrestricted server-side imports can load code you never intended to expose. Map choices to known import functions:

const loaders = {
  bar: () => import('./charts/bar.js'),
  line: () => import('./charts/line.js')
}

Try it: dynamically load one module on a button click, inspect the Network panel, then simulate offline mode and verify the error state.

Lesson completed