How We Cut /pricing TTFB from 400 ms to <10 ms with ISR and Client Islands
How We Cut /pricing TTFB from 400 ms to <10 ms with ISR and Client Islands
Release: v1.0.81 · Date: 2026-03-26 · Ticket: PERF-19
The Problem
The BlockManOS /pricing page was opted into fully dynamic server-side rendering via export const dynamic = 'force-dynamic'. This meant every visitor — including unauthenticated users who had never touched the app — triggered a fresh Lambda invocation on each page load. The reason: the page called auth() on the server to decide whether to render a "Dashboard" or "Sign In" link in the navigation header.
The result was a Time to First Byte (TTFB) of 200–400 ms on cold starts, for a page whose content changes at most when pricing plans are updated — not on every request.
The /sign-in page had the same problem: no dynamic content whatsoever, yet it was also server-rendered on every request.
The Solution
1. ISR on /pricing
Incremental Static Regeneration (ISR) lets Next.js serve a pre-built, CDN-cached page and silently regenerate it in the background on a schedule. We set a one-hour revalidation window:
// src/app/pricing/page.tsx
export const revalidate = 3600;
This means the first request after a cache miss triggers a background rebuild. All subsequent requests within the hour hit the CDN with a TTFB of <10 ms.
2. The Client Island Pattern for Session-Aware UI
The only thing that was truly dynamic on the pricing page was a single nav link: "Dashboard" for signed-in users, "Sign In" for everyone else. Rather than blocking the entire page on a server-side session check, we extracted this into a minimal "use client" component:
// src/components/pricing-nav-link.tsx
"use client";
import Link from "next/link";
import { useSession } from "next-auth/react";
export function PricingNavLink() {
const { data: session } = useSession();
const userId = session?.user?.id ?? null;
return userId ? (
<Link href="/dashboard">Dashboard</Link>
) : (
<Link href="/sign-in">Sign In</Link>
);
}
The static page shell is delivered instantly from CDN cache. PricingNavLink hydrates on the client, fetches the session, and updates the link — all without blocking the initial render. From the user's perspective, the page loads near-instantly and the nav link resolves in a subsequent client-side tick.
This is the "client island" pattern: keep the page shell static, isolate dynamic behaviour into small client components that hydrate independently.
3. Full Static Generation on /sign-in
The sign-in page has no dynamic content at all — it renders a fixed layout with authentication provider buttons. We opted it into full static generation at build time:
// src/app/sign-in/[[...sign-in]]/page.tsx
export const dynamic = "force-static";
The page is now built once per deployment and served from the edge on every request.
4. /api/health Headers in vercel.json
As a related housekeeping change, we added explicit headers to the health-check endpoint to prevent accidental caching and to allow cross-origin requests:
// vercel.json
{
"headers": [
{
"source": "/api/health",
"headers": [
{ "key": "Cache-Control", "value": "no-store" },
{ "key": "Access-Control-Allow-Origin", "value": "*" }
]
}
]
}
This ensures monitoring tools and external uptime checkers always receive a live response and are never served a stale cached result.
Performance Impact
| Page | Before | After | Mechanism |
|---|---|---|---|
/pricing | 200–400 ms TTFB (Lambda cold-start) | <10 ms TTFB (CDN cache hit) | ISR (revalidate = 3600) |
/sign-in | Per-request SSR | Build-time static, edge-served | force-static |
What Did Not Change
- The pricing plan content and layout are identical.
- The sign-in UI is identical.
- Authenticated users still see "Dashboard" in the pricing nav — it just resolves client-side after hydration rather than server-side.
src/app/terms/page.tsxwas intentionally left unchanged; its rendering strategy is controlled by the@saas-factory-live/shellpackage.- No API behaviour, billing logic, or data models were modified.