# Code splitting with dynamic import() in a PWA

> How dynamic import() splits a bundle so startup loads less JavaScript, what that does to INP and LCP, and where it cannot be used in a PWA.

**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()`

`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

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

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

`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

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

- [ ] 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.