Choosing Your StencilJS Output Target: Lazy-Loading vs. Bundled Custom Elements
Deciding between StencilJS's lazy-loaded 'dist' target and the 'dist-custom-elements' bundle is a trade-off between initial load speed and integration simplicity.
10 Jul 2026, 20:26 UTC

The Payload Dilemma: To Chunk or To Bundle?
When shipping a component library with StencilJS, you face a critical architectural decision: do you want your users to download every single component in your library upfront, or should they only download the code for the components actually present on the page? This isn't just a build configuration detail; it fundamentally changes how your library impacts the Core Web Vitals of the consuming application.
The solution lies in Stencil's outputTargets. By configuring different targets, you can ship the same source code as a high-performance lazy-loaded system or a simple, single-file bundle. The right choice depends entirely on who is consuming your components and how much control they have over their script loading strategy.
The Lazy-Loaded Approach (dist)
The default dist target is designed for performance. It produces a tiny entry script (roughly 2 KB gzipped) and a series of separate JavaScript chunks for each component. When the browser encounters a custom element tag—like <my-button>—the Stencil runtime detects it and dynamically imports the specific chunk needed to render that component.
- Best for: Large libraries with dozens of components where a single page only uses a few.
- Trade-off: It introduces a network round-trip the first time a component is used, which can cause a brief "flash of unstyled content" (FOUC) if not managed with CSS.
The Bundled Approach (dist-custom-elements)
The dist-custom-elements target ignores lazy-loading. It bundles every component in your project into one large JavaScript file that calls defineCustomElements() immediately upon execution. This transforms your library into a standard Web Component package that behaves like a traditional JS library.
- Best for: Small libraries, environments where network requests are restricted, or consumers who want a simple
<script src="..."></script>integration without managing a loader. - Trade-off: No tree-shaking. Even if the user only uses one component, they download the entire library.
Configuration Example: Dual-Targeting
Many teams choose to provide both options to support different consumer needs. Below is a stencil.config.ts configuration demonstrating how to implement both targets. This should be run in your project root with the permissions of the user owning the Node.js environment.
import { Config } from '@stencil/core';
export const config: Config = {
namespace: 'my-component-library',
outputTargets: [
// Target 1: The high-performance lazy-loaded version
{
type: 'dist',
esmLoader: true,
},
// Target 2: The "drop-in" single bundle version
{
type: 'dist-custom-elements',
},
// Target 3: Framework wrappers for React users
{
type: 'react-output-target',
componentCorePackage: '@my-org/my-component-library',
proxiesFile: './react-components/proxies.ts',
}
],
};
Verification: After running npm run build, check your project root. You will see a /dist folder containing many small .js files (the chunks) and a /dist-custom-elements folder containing a single consolidated bundle.
Critical Limitations and Risks
While dual-targeting is powerful, there are two major pitfalls to avoid:
- Duplicate Definitions: Never load both the
distloader and thedist-custom-elementsbundle on the same page. This will cause the browser to throw errors because you cannot define the same custom element tag twice. - SSR Guarding: If you use the
dist-hydrate-scripttarget for Server-Side Rendering (SSR), remember thatrender()runs on the server. Any reference towindow,document, orlocalStoragewill crash the server process. These must be wrapped in checks or moved to thecomponentDidLoad()lifecycle method.
Decision Matrix
| Requirement | Use dist (Lazy) |
Use dist-custom-elements |
|---|---|---|
| Minimum Initial Payload | ✅ Yes | ❌ No |
| Zero-Config Integration | ❌ No | ✅ Yes |
| Fastest First-Paint (Small Lib) | ❌ No | ✅ Yes |
| Scalability (100+ Components) | ✅ Yes | ❌ No |
Actionable Summary
If you are building a corporate design system intended for use across multiple diverse applications, stick with the dist target to keep bundle sizes lean. If you are building a small, specialized widget intended for third-party websites via a CDN, the dist-custom-elements target is the most reliable choice. To verify your choice, open the Network tab in Chrome DevTools: if you see individual component files loading as you navigate the page, lazy-loading is active.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.