StencilJS Lazy Loading: Shipping Web Component Chunks on Demand
StencilJS uses compiler-driven lazy loading to ship Web Component chunks only when needed. See how the registry and loader reduce initial bundle size—and where FOUC or request overhead can bite.
23 Aug 2025, 22:43 UTC

Shipping a StencilJS component library often starts innocently: a handful of components, one bundle, everything works. Then the library grows. A monolithic bundle that once loaded instantly now blocks the main thread, delays time-to-interactive, and makes every page pay for components it never renders. StencilJS addresses this with compiler-driven lazy loading, but the mechanism has enough moving parts that it pays to understand exactly what the compiler emits and what the browser does with it.
How StencilJS Splits Components Into Chunks
Instead of emitting one large JavaScript file containing every component definition, the Stencil compiler analyzes your components and splits them into separate chunks. At runtime, a small loader script acts as a central registry. It knows which custom element name maps to which chunk URL, but it does not download those chunks immediately.
When the browser parses HTML and encounters a custom element—say <my-button>—the browser's CustomElementsRegistry recognizes the tag name but has no definition for it yet. Stencil's loader intercepts this moment. It triggers a dynamic import for the chunk associated with my-button, fetches the JavaScript, executes it, and then registers the component definition with the browser. Only after that definition runs does the browser upgrade the element from an unknown HTMLElement to your fully functional component.
The practical effect is that the initial payload contains only the loader and the components actually present in the initial HTML. Everything else waits until the DOM asks for it.
Bootstrapping: hydrateApplication and the Loader Script
How the lazy system starts depends on your build target. In a typical www output, Stencil expects you to call hydrateApplication during app boot. This function wires up the lazy registry so that the application can start without loading every component definition upfront. In a dist build intended for distribution, the compiler usually injects a loader script into the generated index.html; that script performs the same registry role automatically.
Because the bootstrapping differs by output target, it is worth confirming which mode your project uses before debugging missing chunks. If you are distributing components, look for the loader script reference in the built HTML. If you are prerendering an application, verify that hydration is actually initializing the loader.
A Practical Verification Workflow
You can confirm that lazy loading is working without any special tooling—just browser developer tools.
- Build your project. Run your normal Stencil build command (for example,
npm run build) and serve the output. - Open the Network tab. In your browser's developer tools, select the Network panel and filter by JavaScript. Reload the page and note the initial requests. You should see the loader and any components present in the starting HTML, but not the full set of component chunks.
- Trigger a component. Add a component to the DOM that was not in the initial markup—through user interaction, routing, or by typing the tag into the console. Watch for a new JavaScript request corresponding to that component's chunk.
- Inspect the built HTML. Open the generated
index.htmland confirm the presence of the Stencil loader script. Its absence usually means the lazy bootstrap did not run.
To gauge the impact on bundle size, compare a dist build containing a large number of components against one with only a few. The initial JavaScript payload should be noticeably smaller in the larger project, even though the total number of chunks grows.
Trade-offs and Limitations
Lazy loading is not free. The most visible risk is a Flash of Unstyled Content (FOUC). Because the browser upgrades the element only after the chunk executes, there is a window where the custom element exists in the DOM but has no styles or shadow DOM content. If network latency is high or the chunk is large, users may see an unstyled placeholder. Mitigating this usually means reserving space with CSS or ensuring critical components are included in the initial payload.
A second limitation is request granularity. If you split components too finely—essentially one chunk per tiny component—a page that renders many different components will fire numerous HTTP requests. On high-latency connections, the round-trip overhead can erode the savings from a smaller initial bundle. The compiler's automatic splitting is a reasonable default, but extremely large libraries may benefit from auditing which components are truly independent.
Finally, behavior is build-target dependent. The lazy mechanism in a dist output is not identical to a hydrated www application. Loader configuration, prerendering settings, and the presence of hydrateApplication all change what gets fetched and when. Always verify against the specific output you are shipping.
What To Do Next
If you maintain a StencilJS library, treat lazy loading as a measurable property rather than an assumption. Build your project, open the Network tab, and confirm that chunks appear only when their tags enter the DOM. Check the generated HTML for the loader script, and compare initial payload sizes as your component count grows. If you see FOUC or an explosion of requests, adjust which components are bundled eagerly and reserve layout space for late-arriving elements. The compiler gives you the split; the Network tab tells you whether it is actually helping.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.