StencilJS Lazy‑Loaded Component Architecture: Requirements, Minimal Design, and Operational Checks
An architecture note that outlines the requirements for lazy‑loading StencilJS components, the minimal design needed, trust/data boundaries, operational verification steps, failure modes, and conditions that would trigger a redesign.
20 Aug 2026, 16:24 UTC

Requirements
To benefit from StencilJS’s built‑in lazy‑loading, a component must meet the following conditions:
- It is declared with
@Component({ tag: 'my-cmp', lazy: true })(or wrapped with thestencil-lazy-loadpolyfill). - No static reference to the component’s tag appears in the initial HTML served to the browser; any static usage forces early download and defeats code‑splitting.
- The project uses StencilJS v2.x or later, where the compiler emits a separate ES module chunk for each lazy component.
These requirements ensure that the browser only downloads the component’s code when the element is first inserted into the DOM.
Smallest Suitable Design
The minimal design consists of three parts generated by the Stencil compiler:
- Main bundle – contains the application bootstrap, shared utilities, and the lazy‑load runtime.
- Lazy chunk – a separate file named after the component (e.g.,
my-cmp.js) that holds the component’s class, styles, and render logic. - Loader script – a small inline function that, when the component’s selector is encountered, dynamically imports the lazy chunk and registers the custom element.
At runtime, the loader script creates a promise that resolves when the chunk finishes loading, then defines the custom element and upgrades any matching DOM nodes.
Trust and Data Boundaries
Lazy‑loaded components retain StencilJS’s encapsulation guarantees:
- Each component renders into its own shadow DOM, preventing accidental style or DOM leakage.
- Data flows only through declared
@Prop()inputs and@Event()outputs; the host page cannot directly access the component’s internal state or methods. - Events bubble out of the shadow root and can be listened to on the host element, preserving the standard custom‑element communication model.
Because the lazy chunk is evaluated in the same global scope as the main bundle, there is no additional trust boundary introduced by the loading mechanism itself.
Operational Checks
Build‑time verification
Run the production build and inspect the output:
# Run from the project root
npm run build
# After completion, list the generated chunks
ls -l www/build/
You should see a file matching the component’s tag, e.g., my-cmp.js, alongside the main bundle (main.js). No other manual bundler configuration is required.
Runtime verification (network)
- Serve the build with a static server (e.g.,
npx serve www). - Open Chrome DevTools → Network, enable throttling (e.g., “Slow 3G”).
- Ensure the page initially loads only the main bundle and any shared chunks.
- Trigger the component’s insertion (e.g., click a button that adds
<my-cmp>to the DOM). - Observe a separate request for
my-cmp.jsappear after the DOM mutation.
If the request occurs before the insertion, check for accidental static usage of the tag in the initial HTML.
Fallback UI verification
Implement a simple placeholder while the chunk loads:
<div>
<button id="show">Load component</button>
<div id="placeholder" style="display:none;">Loading…</div>
<div id="target"></div>
</div>
Verify that the “Loading…” text appears immediately after the button click and disappears once componentOnReady resolves.
Error handling verification
Introduce a deliberate error inside the lazy component’s render() method, rebuild, and reload the page. Then:
- Trigger the component’s insertion as described above.
- Open the Console tab; you should see an error logged from the chunk.
- Confirm that the host page remains functional (e.g., other buttons still work) and that the error is also catchable via
window.onerroror atry/catcharoundcomponentOnReady.
This demonstrates that a failure in the lazy chunk does not bring down the entire application.
Failure Modes
- Static usage – If the component’s tag appears in the initial HTML, the loader script runs during the first paint and the chunk is fetched eagerly, negating the code‑splitting benefit.
- SSR/Prerendering mismatch – In server‑side rendering or prerendering modes, the lazy‑load mechanism is disabled by default. If the server renders a placeholder but the client attempts to hydrate the real component, a hydration mismatch can occur unless you enable
hydrateServerSideor provide an SSR‑compatible fallback. - Network failure – If the lazy chunk fails to load (e.g., 404 or timeout), the component remains undefined. Without error handling, subsequent attempts to create the element will throw.
- Version skew** – Loading a lazy chunk built with a different Stencil version than the main bundle can cause runtime incompatibilities (e.g., changed lifecycle signatures).
Conditions That Would Change the Design
Reconsider the lazy‑loading approach when any of the following become true:
- You need the component to be available immediately on first paint (e.g., above‑the‑fold UI). In that case, declare it without
lazy: trueor usestencil-lazy-loadwith a preload hint. - Your application must support browsers that lack native ES module dynamic import support and you cannot rely on the polyfill’s size overhead.
- You intend to share heavy state or singleton services across lazy components; you may prefer a shared chunk rather than per‑component splitting to avoid duplication.
- You are building a library for consumption by other projects and want to avoid exposing the lazy‑load runtime as a public dependency.
In each case, the smallest suitable design would shift toward either eager loading, a shared chunk, or a custom loading strategy that aligns with the new constraints.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.