GitHub

Hydration

Hydration in Pulse is automatic. Specs with mutations, actions, or persist are hydrated automatically — Pulse binds events to the server-rendered HTML without re-rendering it. Purely server-rendered specs get zero JavaScript sent to the browser.

How hydration works

Pass spec files as URL objects to createServer. Pulse detects whether each spec needs client-side interactivity and, if so, derives the browser-importable path automatically — no hydrate field needed in the spec:

// server.js
await createServer(
  [
    new URL('./src/pages/counter.js', import.meta.url),  // has mutations → auto-hydrated
    new URL('./src/pages/about.js',   import.meta.url),  // no mutations  → zero JS
  ],
  { port: 3000, root: new URL('.', import.meta.url) }
)

// src/pages/counter.js — no hydrate field needed
export default {
  route: '/counter',
  state: { count: 0 },
  mutations: {
    increment: (state) => ({ count: state.count + 1 }),
    decrement: (state) => ({ count: state.count - 1 }),
  },
  view: (state) => `
    <div>
      <button data-event="decrement">-</button>
      <span>${state.count}</span>
      <button data-event="increment">+</button>
    </div>
  `,
}

Development bootstrap

In development Pulse emits an inline bootstrap script that imports the spec and runtime source files directly — no build step required:

<script type="module">
  import spec from '/src/pages/counter.js'
  import { mount } from '/src/runtime/index.js'
  import { initNavigation } from '/src/runtime/navigate.js'
  mount(spec, root, window.__PULSE_SERVER__ || {}, { ssr: true })
  initNavigation(root, mount)
</script>

This imports the spec and runtime source files directly — no build step required for development.

Production bundles

Run npm run build to generate production bundles. This creates content-hashed files in public/dist/ and a manifest.json mapping spec hydrate paths to bundle paths.

# Generated by npm run build
public/dist/
  runtime-abc123.js          # shared runtime (~3.8 kB brotli)
  counter.boot-def456.js     # per-page spec bundle (~0.4–0.9 kB brotli)
  manifest.json              # { '/src/pages/counter.js': '/dist/counter.boot-def456.js' }

When Pulse detects a manifest (via staticDir auto-detection or explicit manifest option), it resolves the auto-derived hydrate path to the bundle path and emits a single <script src> tag instead of the inline bootstrap:

<script type="module" src="/dist/counter.boot-def456.js"></script>

The { ssr: true } option

The bootstrap script calls mount(spec, root, serverState, { ssr: true }). This tells the runtime to skip the initial re-render and bind event listeners to the existing DOM only.

This is what keeps LCP fast. The server-painted HTML is the LCP element. The JavaScript binds events without touching the DOM — no flash, no layout shift, no JS-rendered replacement.

Never set { ssr: false } on a server-rendered page. It re-renders the entire DOM on mount, replacing the server-painted HTML — causing a visible flash and pushing LCP to 400–600ms.

mount()

The mount function attaches the Pulse runtime to a DOM element:

import { mount } from '@invisibleloop/pulse/runtime'

mount(
  spec,          // the page spec
  rootEl,        // the DOM element to mount into
  serverState,   // window.__PULSE_SERVER__ (server data from SSR)
  { ssr: true }  // skip re-render on first mount
)

After mount, all data-event and data-action attributes in the DOM are wired to the spec's mutations and actions. State updates trigger a full view re-render via innerHTML replacement.

Pages without hydration

Specs with no mutations, actions, or persist automatically get zero JavaScript — no runtime overhead, no hydration cost. This is the correct default for:

  • Documentation pages
  • Marketing/landing pages
  • Blog posts and articles
  • Any page with no client-side interactivity
Many pages that appear to need JavaScript can be handled server-side with routing and server data. Keep specs free of mutations and actions wherever possible.

Passing server state to the client

Server data fetched via server.data() is serialised into the page HTML as window.__PULSE_SERVER__. The client runtime reads this on mount, making server data available to the view during client re-renders without an additional network request.

// Emitted in the page HTML
<script id="__PULSE_SERVER__" type="application/json">
  {"product":{"id":1,"name":"Widget","price":9.99}}
</script>