Code splitting with dynamic import() in a PWA
Published
In one line: Code splitting breaks a JavaScript bundle into pieces so the
browser only downloads, parses, and executes what a page needs at startup;
import() — a dynamic, promise-returning expression distinct from the static
import statement — is the mechanism that loads the rest on demand.
Dynamic import()
Section titled “Dynamic import()”import() is not a function call even though it looks like one — per MDN,
“import itself is a keyword, not a function,” so it cannot be aliased like
const myImport = import. It returns a promise that, on success, “fulfills to
a module namespace object: an object containing all exports from moduleName.”
Unlike a static import declaration, which “is static and will always result
in the imported module being evaluated at load time,” a dynamic import()
call can appear anywhere in code and load a module conditionally or on demand
— for example, only after a user submits a form.
Why splitting the bundle matters
Section titled “Why splitting the bundle matters”Web.dev frames the goal directly: “Code splitting is a technique that seeks to minimize startup time” by shipping less JavaScript upfront — “split your bundle into multiple pieces and only send what’s necessary at the very beginning.” Code that is dynamically imported “does not get included into the initial bundle and is now lazy loaded,” so it is fetched only when actually needed.
Smaller startup bundles mean less main-thread parse/compile/execution work, which “will contribute to better…INP times” by freeing the main thread to respond to input sooner. For client-side-rendered pages, trimming the JavaScript responsible for rendering markup can also improve LCP, particularly when the main thread is too busy to render the largest content element promptly.
Route, component, and vendor splitting
Section titled “Route, component, and vendor splitting”Beyond manually deferring individual functions, web.dev points to
higher-level strategies: “Splitting on the route or component level when
using a client-side framework is a simpler approach to lazy loading different
parts of your application,” and frameworks built on bundlers such as webpack
often provide built-in abstractions for this. Third-party dependencies are
usually handled differently — “split into a separate vendor bundle that can be
cached since they don’t update as often” (for example with webpack’s
SplitChunksPlugin) — rather than lazy-loaded on demand like route or
component code. Bundlers that support dynamic import() for this purpose
include webpack, Parcel, and Rollup.
Where dynamic import() cannot run
Section titled “Where dynamic import() cannot run”import() “can be used in the main thread, a shared worker, or a dedicated
worker, but will throw if called within a service worker or a worklet.” Plan
splitting around code that runs on the main thread or in dedicated/shared
workers — it is not available for lazy-loading logic inside a service worker.
Caching behavior
Section titled “Caching behavior”Each normalized module specifier generally resolves to the same cached module
namespace object — MDN notes this is “generally true,” with one documented
exception: if the imported module exports a function named then, that
function is invoked as part of the dynamic import’s promise resolution, which
MDN warns against relying on. “This aggressive caching ensures that a piece of
JavaScript code is never executed more than once, even if it is imported
multiple times. Future imports don’t even result in HTTP requests or disk
access.” That caching applies once a module has loaded and linked
successfully; “only evaluation failures are cached,” so a module that fails to
load or link may be retried on the next import.
Practical checklist
Section titled “Practical checklist”- Ship only what a route or view needs at startup; defer the rest behind
import(). - Split at the route or component boundary when using a client-side framework rather than hand-splitting individual functions.
- Route third-party dependencies into a separate, long-cacheable vendor bundle instead of lazy-loading them per interaction.
- Do not call dynamic
import()inside a service worker or worklet — it throws there. - Preload chunks you know you’ll need soon so the split doesn’t trade startup cost for a visible delay later.