GitHub

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:

MetricTargetHow
LCPFastStreaming SSR sends HTML before server data resolves. Actual LCP depends on server location, CDN, and network conditions.
CLS0.00Always set width and height on images; framework never shifts layout
Lighthouse Performance100Compression, immutable caching, no render-blocking resources
Lighthouse Accessibility100Semantic HTML, proper alt text, sufficient contrast
Lighthouse SEO100Meta tags, structured data, canonical links
Lighthouse Best Practices100HTTPS, security headers, no deprecated APIs
Run 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:

EncodingPriorityTypical savings
Brotli (br)First choice20–30% smaller than gzip for text content
gzipFallbackWidely supported, good compression
NoneLast resortNo 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.

Static assets in 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 sizeWhat the boot file containsSize (brotli)
Single pageYour spec + the full Pulse runtime bundled together~3.5 kB
Multiple pagesYour 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 visitruntime-[hash].js (~3.1 kB) + home.boot-[hash].js (~0.35 kB)
  • Navigate to another pagecontact.boot-[hash].js (~0.47 kB) only. Runtime already cached.
  • Return visit — nothing. Both files served from cache with immutable headers.
The runtime hash only changes when the Pulse runtime itself is updated — not when your app changes. Deploying new pages or mutations does not bust the runtime cache for returning visitors.

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:

HeaderValue
X-Content-Type-Optionsnosniff
X-Frame-OptionsDENY
Referrer-Policystrict-origin-when-cross-origin
Permissions-Policycamera=(), microphone=(), geolocation=()
Cross-Origin-Opener-Policysame-origin
Cross-Origin-Resource-Policysame-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:

ConstraintChromeFirefoxSafariEdgeSince
?. optional chaining (JS)807413.180Feb – Mar 2020
gap on flexbox (CSS)846314.184Aug 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.

No explicit 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 width and height on images (use the img() helper)
  • Never inject content above existing content after page load
  • Use aspect-ratio CSS for embeds (videos, iframes)
  • Avoid loading web fonts that cause FOUT — use font-display: swap or system fonts

Optimising LCP

LCP (Largest Contentful Paint) is typically your hero image or largest heading. Tips:

  • Use priority: true on the LCP image — sets fetchpriority="high" and loading="eager"
  • Avoid blocking server fetches — use stream so 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
Never render your LCP element (hero image, main heading) client-side. If it requires a client JS import or a dynamic import, it will not paint until JS executes — pushing LCP from <100ms to >500ms.