Architectural note: Lazy loading and hydration in StencilJS for smaller bundles and faster TTI
Learn how to split StencilJS components into lazy chunks, keep server‑rendered HTML interactive with hydration, and verify the design with build checks and browser devtools.
15 Feb 2026, 13:13 UTC

Problem: Large initial JavaScript bundle delays interactivity
When a StencilJS application bundles all components into a single file, the first paint may show server‑rendered HTML but the page stays unresponsive until the whole bundle downloads, parses and executes. This increases Time to Interactive (TTI) and hurts metrics on slower connections.
Takeaway
Enable Stencil’s compiler‑driven lazy loading and hydration so each component is shipped as its own chunk, loaded only when its custom element appears in the DOM, while server‑rendered markup becomes interactive without a full re‑render.
Requirements
- StencilJS v2.x or later (compiler supports lazy loading).
- Server‑side rendering (SSR) enabled to deliver initial HTML.
- Build pipeline that can emit multiple chunks (default Stencil build).
- Ability to edit
stencil.config.tsand run the build command.
Smallest suitable design
- Mark each component that is not needed on the initial view with
@Component({ lazy: true }). - Keep only the components required for the above‑the‑fold UI in the eager bundle (either omit
lazyor setlazy: false). - Ensure the component’s tag is present in the server‑rendered HTML so the browser can instantiate it later.
- Let Stencil’s compiler generate a separate chunk file for each lazy component (named
[component-name].jsby default).
Trust / data boundaries
Stencil uses Shadow DOM to encapsulate each component’s style and markup, preventing CSS from leaking out or global styles from unintentionally affecting the component’s internal DOM. This creates a clear trust boundary:
- Component’s internal DOM and styles are private.
- Communication with the parent happens only through public properties, methods, or events declared in the component’s API.
- Global theme values can be passed in via CSS Custom Properties, which cross the shadow boundary deliberately.
Operational checks
- After building, open the
distfolder and verify that alongside the main bundle (main.js) there are separate files such asmy-lazy-component.js. - Run the application (
npm startorstencil dev --serve) and open Chrome DevTools → Network. Filter by JS and interact with the page to trigger the lazy component; you should see a request for its chunk appear only after the interaction. - Disable JavaScript in the browser, reload the page, and confirm that the server‑rendered HTML is still visible (no blank screen). Re‑enable JS and observe the
hydratedclass being added to the host element, indicating hydration completed. - Inspect the host element in the Elements pane; after hydration you should see a class attribute containing
hydrated(added by Stencil) and no duplicate markup.
Failure modes and conditions that would change the design
- Flash of Unstyled Content (FOUC) – If the component’s CSS is not inlined or delivered with the initial HTML, the element may appear unstyled before its chunk loads. Mitigation: include critical CSS in the SSR output or set
devTools: falseand rely on Stencil’s automatic CSS injection. - Layout shift – Hydration can modify the DOM size if server‑rendered markup differs from the client‑generated tree (e.g., due to conditional rendering based on browser‑only APIs). Keep server and client render identical for lazy components, or use
hydrateSlotto preserve existing nodes. - Excessive HTTP requests – Splitting too many tiny components can increase round‑trip overhead, especially on HTTP/1.1. Monitor the number of chunk requests in the Network tab; if > 20‑30 requests noticeably delay TTI, consider bundling a group of related lazy components using
stencil-build’sdevToolsflag or a custom rollup config. - Shadow DOM theming – Global variables defined outside the shadow root do not pierce the boundary. If a design system relies on cascading variables, expose them through CSS Custom Properties on the host (
:host { --theme-color: var(--global-primary); }) and consume them inside the component. - Build:
npm run build(requires read/write access to project directory). - Check output:
ls dist/**/*.jsshould listmain.jsplus one file per lazy component. - Run dev server:
npm start(orstencil dev --serve). - Open Chrome DevTools → Network → JS, enable ‘Disable cache’, reload, then interact to trigger lazy component; verify a new chunk request appears.
- Disable JS, reload, confirm server‑rendered content visible.
- Re‑enable JS, inspect host element for
hydratedclass.
Practical verification checklist
If any of these checks fail, revisit the lazy flag, ensure the component is actually used in the SSR template, and verify that Stencil’s compiler version supports lazy loading (v2.0+).
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.