Choosing Between Client‑Only, SSR, and Prerendering in StencilJS
A decision guide that compares StencilJS client‑only, SSR with hydration, and static prerendering, shows trade‑offs, and provides a concrete Express‑server implementation.
23 Mar 2026, 06:18 UTC

Decision and constraints
You need to decide whether to keep StencilJS components client‑only, enable server‑side rendering (SSR) with hydration, or use static prerendering. The decision is constrained by:
- Fast time‑to‑interactive (TTI) is a priority.
- Server resources are limited; you prefer minimal extra workload.
- The existing build pipeline must stay largely unchanged (no new tooling beyond a plugin).
Supported options
| Option | Description | Build‑time impact | Runtime cost |
|---|---|---|---|
| Client‑only (default) | Components render in the browser after the JavaScript bundle loads. | No extra build step. | Minimal server load; higher first contentful paint (FCP). |
| SSR with hydration | Render HTML on a Node/Edge server, then hydrate on the client. | Requires @stencil/ssr plugin and a server‑side bundle. | Slightly larger server workload; lower FCP. |
| Prerendering (static) | Generate HTML at build time for known routes. | Adds a build step; limited to routes known ahead of time. | Zero server runtime cost. |
Trade‑offs
Client‑only is the simplest to set up and keeps the dev loop fast, but it hurts SEO and can delay visible content because the browser must download and execute JavaScript before anything appears.
SSR improves SEO and reduces FCP because the server sends fully rendered markup. It does, however, require a Node.js runtime (≥14) and adds complexity to deployment (you need to run a server or edge function). If hydration is slow, TTI can suffer.
Prerendering gives the SEO benefits of SSR without any server cost, but it only works for routes that are static at build time. Frequently changing content would become stale unless you rebuild the site.
Concrete implementation – enabling SSR with hydration
- Install the SSR plugin as a dev dependency:
npm install --save-dev @stencil/ssr - Update
stencil.config.tsto output the www folder and enable the hydrate server:import { Config } from '@stencil/core'; import { ssr } from '@stencil/ssr'; export const config: Config = { namespace: 'my-app', outputTargets: [ { type: 'www', // enables the SSR entry point serviceWorker: null, // disable if not needed }, { type: 'dist', }, { type: 'dist-hydrate-script', }, ], plugins: [ ssr({ hydrateServer: true, // optional: specify the entry point for the server bundle // default is 'src/ssr.ts' }), ], }; - Create a simple Express server (file
server.js):const express = require('express'); const { renderToString } = require('@stencil/ssr'); const app = express(); const PORT = process.env.PORT || 3000; // Serve static assets from the www folder app.use(express.static('www')); app.get('*', async (req, res) => { try { const { html } = await renderToString({ url: req.originalUrl, // the directory where the www build lives dir: './www', }); res.send(html); } catch (e) { console.error(e); res.status(500).send('Server error'); } }); app.listen(PORT, () => { console.log(`Server listening on http://localhost:${PORT}`); }); - Build the project:
This producesnpm run buildwww/buildwith the client bundle and a server entry (ssr.js) used byrenderToString. - Start the server:
node server.js
Validation steps
- Check the build output: after
npm run buildverify thatwww/buildcontains bothesm(oresm) client chunks and a file namedssr.js. - Use curl to fetch a route and grep for expected text, for example:
If the command returns a line, the server rendered HTML includes that text.curl -s http://localhost:3000 | grep -i "welcome to stencil" - Open the page in a browser, open DevTools → Console, and look for the message:
This indicates that the client‑side hydration ran after the server‑sent HTML.[stencil] hydrate - In the Network tab, confirm that the initial HTML response (status 200) arrived before the JavaScript bundle (
main.jsor similar) was downloaded.
Limitations and practical checks
- Server requirement: SSR needs a Node.js runtime (≥14). If your host only serves static files, consider a serverless edge function (e.g., Vercel, Cloudflare Workers) that can run the
renderToStringcall. - Hydration mismatches: Avoid accessing
windowordocumentdirectly in component render methods. Guard such code withif (typeof window !== 'undefined') { … }or use Stencil’s@Propand state lifecycle. - Build‑time impact: Adding the SSR plugin increases bundle size slightly because the server entry is included. Measure the size of
www/buildbefore and after adding the plugin to ensure it stays within your budget. - Prerendering alternative: If your routes are mostly static, you can replace the Express server with the built‑in preroller:
This generates static HTML files innpm run build -- --prerenderwwwwith zero server cost.
By following the steps above you can decide which rendering mode fits your constraints, implement SSR with hydration, and verify that both server‑rendered markup and client‑side hydration are working as expected.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.