JavaScript Dynamic Imports
By Flavio Copes
Learn how JavaScript dynamic imports overcome the limits of static imports, letting you load a module conditionally at runtime with the await import() syntax.
Dynamic imports let you load an ES module at runtime, using the import() syntax. Unlike static imports, they can run anywhere in your code, inside conditions, and with a module name computed on the fly.
I first used them to power code splitting in a Next.js application, and I had to do a bit of research because they are slightly different from static imports.
Static imports and their limits
A static import of an ES Module default export looks like this:
import moment from 'moment'
You can use object destructuring to get a named export:
import { format } from 'date-fns'
Static imports have some limits:
- they are limited to the top level of the file
- they can’t be loaded conditionally (inside an
if) - the name of the package can’t be determined at execution time
Dynamic imports can do all those things!
How do you use import()?
import() returns a promise, which resolves to the module object. Combined with await, it reads almost like a static import:
const module = await import('module')
To get the default export, you access the default property of the module object.
Example using moment:
const moment = (await import('moment')).default
Named imports on the other hand work as expected, with destructuring:
const { format } = await import('date-fns')
Since the module name is just a string, you can build it at runtime. This is impossible with static imports:
const messages = await import(`./locales/${language}.js`)
When would you use this?
The classic case is loading a heavy library only when it’s needed. Say you fire confetti when the user clicks a button:
button.addEventListener('click', async () => {
const { default: confetti } = await import('canvas-confetti')
confetti()
})
The library is not part of the initial page load. It’s downloaded the first time someone clicks. Bundlers like the one in Next.js turn this into a separate chunk automatically, which is exactly the code splitting I mentioned at the start.
Watch out for the default export
The most common mistake is forgetting .default. If you write:
const moment = await import('moment')
moment() //TypeError: moment is not a function
moment here is the whole module object, not the function you wanted. Access .default (or destructure it as { default: moment }) and it works.
Also note that await at the top level only works inside ES modules. In other contexts, use the promise directly with .then(), or wrap the code in an async function.
Can you use them today? Yes! The browser support is excellent, Node.js supports them too, and for very old toolchains there’s a Babel plugin.
Related posts about js: