Performance
Performance in Pulse is structural, not optional. Streaming SSR, immutable asset caching, brotli compression, and zero client JS by default are the baseline — not optimisations applied after the fact. A page that uses the framework correctly cannot score poorly.
Performance targets
Every page served by Pulse should meet these targets on localhost with no throttling:
| Metric | Target | How |
|---|---|---|
| LCP | Fast | Streaming SSR sends HTML before server data resolves. Actual LCP depends on server location, CDN, and network conditions. |
| CLS | 0.00 | Always set width and height on images; framework never shifts layout |
| Lighthouse Performance | 100 | Compression, immutable caching, no render-blocking resources |
| Lighthouse Accessibility | 100 | Semantic HTML, proper alt text, sufficient contrast |
| Lighthouse SEO | 100 | Meta tags, structured data, canonical links |
| Lighthouse Best Practices | 100 | HTTPS, security headers, no deprecated APIs |
mcp__chrome-devtools__lighthouse_audit after every new page to verify all four scores are 100. Fix any failures before considering the task done.Streaming SSR
Pulse uses Node.js streams for SSR. The server sends the <head> and page shell immediately, before any async data resolves. Browsers start downloading CSS and fonts while the server fetches data — so the user sees a styled shell within milliseconds.
export default {
route: '/feed',
// Shell renders instantly — hero, nav, layout
// Deferred segments wait for data then stream in
stream: {
shell: ['header', 'hero'],
deferred: ['feed', 'sidebar'],
},
server: {
feed: async () => db.posts.getLatest(20), // slow
sidebar: async () => db.tags.getPopular(), // slow
},
// view is a keyed object matching stream segments
view: {
header: (state) => `<header>...</header>`,
hero: (state) => `<section class="hero">...</section>`,
feed: (state, server) => server.feed.map(renderPost).join(''),
sidebar: (state, server) => renderSidebar(server.sidebar),
},
}
Automatic compression
All responses are compressed automatically. Pulse negotiates the best available encoding from the Accept-Encoding header:
| Encoding | Priority | Typical savings |
|---|---|---|
Brotli (br) | First choice | 20–30% smaller than gzip for text content |
| gzip | Fallback | Widely supported, good compression |
| None | Last resort | No compression (rare) |
HTML, CSS, JavaScript, JSON, XML, and SVG responses are all compressed. Binary formats (images, fonts) are served as-is.
Immutable asset caching
Production JS bundles include a content hash in their filename (/dist/counter.boot-a1b2c3d4.js). The server sends Cache-Control: public, max-age=31536000, immutable for these files — browsers cache them forever and never re-request them unless the hash changes.
When you deploy a new version, the hash changes, and users automatically get the new file. No cache-busting tricks needed.
public/ get max-age=3600. For rarely-updated images, consider versioning them by filename.Zero client JS by default
Pages with no mutations, actions, or persist send no JavaScript at all — the HTML is entirely self-contained. The Pulse CLI detects this automatically; you never need to opt out. This is appropriate for static content pages: marketing pages, blog posts, documentation.
// No mutations, actions, or persist → zero JS sent to browser
export default {
route: '/about',
meta: { title: 'About', styles: ['/app.css'] },
state: {},
view: () => `<main><h1>About us</h1></main>`,
}
JS bundle splitting and caching
When you open the network tab you will see a single [page].boot-[hash].js file loading. What is inside that file depends on how many pages your app has:
| App size | What the boot file contains | Size (brotli) |
|---|---|---|
| Single page | Your spec + the full Pulse runtime bundled together | ~3.5 kB |
| Multiple pages | Your spec only — runtime is in a separate runtime-[hash].js chunk | ~0.35–0.5 kB |
With multiple pages, esbuild's code splitting extracts the Pulse runtime into a shared chunk because every page imports it. The browser downloads it once and caches it — subsequent page navigations only fetch the small per-page boot file.
What you see in the network tab across navigations:
- First page visit —
runtime-[hash].js(~3.1 kB) +home.boot-[hash].js(~0.35 kB) - Navigate to another page —
contact.boot-[hash].js(~0.47 kB) only. Runtime already cached. - Return visit — nothing. Both files served from cache with
immutableheaders.
Security headers
Every response — including 404 and 500 errors — carries a full set of security headers automatically. There is no configuration step and no way to accidentally omit them:
| Header | Value |
|---|---|
X-Content-Type-Options | nosniff |
X-Frame-Options | DENY |
Referrer-Policy | strict-origin-when-cross-origin |
Permissions-Policy | camera=(), microphone=(), geolocation=() |
Cross-Origin-Opener-Policy | same-origin |
Cross-Origin-Resource-Policy | same-origin |
These headers are applied automatically — no configuration needed.
Browser support
Pulse ships modern JavaScript and CSS without transpilation or polyfills. The effective minimum is set by two features:
| Constraint | Chrome | Firefox | Safari | Edge | Since |
|---|---|---|---|---|---|
?. optional chaining (JS) | 80 | 74 | 13.1 | 80 | Feb – Mar 2020 |
gap on flexbox (CSS) | 84 | 63 | 14.1 | 84 | Aug 2020 – Apr 2021 |
In practice, Safari 14.1 (April 2021) is the combined floor — browsers released after that date support everything Pulse uses. This covers roughly 95%+ of global traffic.
target is set in the esbuild config, so syntax ships as written. If you need to support older browsers, set target in scripts/build.js and esbuild will downcompile optional chaining and other modern syntax automatically.Preventing layout shift (CLS)
Pulse targets 0.00 CLS. Layout shift is caused by elements that change size or position after the initial paint. These rules prevent it:
- Always set
widthandheighton images (use theimg()helper) - Never inject content above existing content after page load
- Use
aspect-ratioCSS for embeds (videos, iframes) - Avoid loading web fonts that cause FOUT — use
font-display: swapor system fonts
Optimising LCP
LCP (Largest Contentful Paint) is typically your hero image or largest heading. Tips:
- Use
priority: trueon the LCP image — setsfetchpriority="high"andloading="eager" - Avoid blocking server fetches — use
streamso the shell renders without waiting for data - Keep your hero HTML inline (SSR) — never rely on client JS to render the LCP element
- Use modern image formats (AVIF, WebP) via the
picture()helper