Skip to content

SSR & Hydration

When using SSR (Next.js, Remix, Astro), the server renders HTML with mangled class names (z, y, x). The client must use the same mangle map to hydrate correctly. CSSzyx uses SHA-256 checksums to verify this.

Without protection, a stale CDN cache could serve HTML with an old mangle map while the browser downloads a new JS bundle with a different map. The result: class z means p-4 on the server but bg-blue-500 on the client. Your app silently renders with broken styles.

CSSzyx detects this and aborts hydration before the mismatch causes damage.

Step 1 — Build

Compute a SHA-256 checksum of the full mangle map at build time.

Step 2 — Inject

Embed the checksum in the <html data-sz-checksum="..."> attribute.

Step 3 — Verify

Client validates the checksum before the first React hydration.

<!-- Server output includes checksum and mangle map -->
<html data-sz-checksum="a3f9c2...">
<script id="__CSSZYX_MANGLE_MAP__" type="application/json">
{
"class:p-4": "z",
"class:bg-blue-500": "y",
"class:hover:bg-blue-600": "x"
}
</script>
</html>

The client runtime calls verifyMangleChecksum(expectedChecksum) before hydration. If the expected checksum doesn’t match the checksum embedded on <html data-sz-checksum="...">, the abort protocol fires.

When production.mangleVars or production.mangleGlobalVars emits CSS variable aliases, the same checksum map also includes CSS-variable entries such as var:--_sz-p:--cz or var:--brand-primary. The debug helper keeps a separate window.__csszyx.varMangleMap for inspection.

// What happens on mismatch (automatically, no code needed)
if (!verifyMangleChecksum(expectedChecksum)) {
abortHydration(); // Preserve SSR HTML, block React hydration
}
  1. Enable checksum injection in the plugin

    vite.config.ts
    ...csszyx({
    production: {
    mangle: true,
    injectChecksum: true, // ← enables the checksum in HTML
    },
    })
  2. Initialize the runtime at app startup

    // src/main.tsx (or app/layout.tsx for Next.js)
    import { initRuntime } from "@csszyx/runtime";
    initRuntime({
    development: process.env.NODE_ENV === "development",
    strictHydration: true,
    });
  3. (Optional) Per-element recovery via szRecover

    The legacy global allowCSRRecovery flag was replaced by the per-element szRecover JSX attribute (see next section). It scopes recovery to subtrees where it’s actually safe instead of opting the whole page in.

By default a hydration mismatch aborts the entire page — the safe choice for a production app you don’t want to render with broken styles. For specific subtrees where re-rendering on the client is cheaper than aborting, opt in per-element with the szRecover JSX attribute.

<section szRecover="csr">
{/* Hydration mismatch in this subtree triggers a client re-render
instead of aborting the whole document. */}
<UserGeneratedContent html={post.body} />
</section>
<aside szRecover="dev-only">
{/* Same recovery behaviour, but ONLY in development builds.
Stripped from the production manifest at build time. */}
<DebugPanel />
</aside>

How it works (build → SSR → hydration)

Section titled “How it works (build → SSR → hydration)”
  1. Build. The csszyx unplugin scans every JSX element with szRecover and tags it with data-sz-recovery-token="<12-hex>". The token is a deterministic hash of ${file}:${line}:${col}:${elementType} — stable across rebuilds, so HMR doesn’t churn the manifest.

  2. SSR HTML. The unplugin injects a single manifest script into <head> containing every emitted token:

    <script id="__SZ_RECOVERY_MANIFEST__" type="application/json">
    {
    "buildId": "…",
    "checksum": "…",
    "mangleChecksum": "…",
    "tokens": {
    "a3b9…": { "mode": "csr", "component": "section", "path": "…" }
    }
    }
    </script>

    checksum protects the recovery token set. mangleChecksum is the value compared with the page’s data-sz-checksum hydration checksum.

  3. Hydration. @csszyx/runtime/verify reads the manifest, then verifyRecoveryToken(element, manifest) matches each element’s token against an entry. A valid match permits client recovery for that subtree; an unknown token rejects.

ModeDev manifestProd manifestWhen to use
csrincludedincludedSubtrees that legitimately may render differently on client (auth-gated, locale-aware, A/B)
dev-onlyincludedstrippedDebug overlays, dev-mode banners — the build emits a single rolled-up warning listing the stripped paths

Production strips dev-only because the runtime’s __SZ_ALLOW_CSR_RECOVERY__ flag is dev-mode only; shipping these tokens would just add bytes the runtime would refuse anyway.

szRecover is typed on React.HTMLAttributes via @csszyx/types/jsx. The triple-slash reference in the umbrella csszyx package’s type output means importing csszyx anywhere in the project surfaces the prop with full IntelliSense — no extra tsconfig wiring:

<section szRecover="csr">…</section> // ✓ valid
<section szRecover="dev-only">…</section> // ✓ valid
<section szRecover="ssr">…</section> // ✗ Type '"ssr"' is not assignable
import {
initRuntime,
startHydration,
endHydration,
abortHydration,
verifyMangleChecksum,
verifyMangleMapIntegrity,
getHydrationErrors,
isHydrationAborted,
} from "@csszyx/runtime";
// Start hydration session
startHydration();
// Verify checksum (throws on mismatch if strictHydration: true)
const isValid = verifyMangleChecksum(expectedChecksum);
// Check if hydration was aborted
if (isHydrationAborted()) {
// Preserve SSR HTML
}
// Get all hydration errors
const errors = getHydrationErrors();

Enable debug logging to see what’s happening:

initRuntime({ debug: true });

This logs:

  • Mangle map loaded from DOM
  • Checksum verification result
  • Any hydration errors with class-level detail
  • Recovery token status
import type { HydrationErrorType } from "@csszyx/runtime";
// Error types:
// 'checksum_mismatch' — SHA-256 mismatch between server/client
// 'missing_manifest' — No mangle map in DOM
// 'invalid_manifest' — Mangle map is malformed
// 'class_not_found' — Mangled class has no mapping
// 'token_invalid' — Recovery token signature is invalid