Next.js 15 Performance Optimization: Complete Guide
Great UX is fast, consistent, and resilient. This guide distills production‑ready patterns to make Next.js 15 apps feel instant: smarter rendering, aggressive caching, careful data fetching, and eliminating waste in the bundle.
Core Web Vitals at a glance
- LCP: Time until the largest content appears. Optimize hero rendering, compress images, use
priorityon critical media. - INP: Input latency. Reduce main‑thread work, avoid heavy client JS, prefetch interactions.
- CLS: Layout stability. Reserve space for media/fonts, avoid dynamic height shifts.
Rendering strategy: stream by default
Next.js App Router enables server components, partial revalidation, and streaming. Prefer server work, stream early HTML, and hydrate minimally.
Checklist
- Keep components server‑first; mark client components explicitly with
"use client". - Stream route segments; keep slow data behind Suspense boundaries.
- Use
dynamic = "force-static" | "force-dynamic"intentionally per route. - Co‑locate data fetching in server components; avoid fetching in the client.
// app/(shop)/product/[id]/page.tsx (server component)
import Image from "next/image";
import { notFound } from "next/navigation";
export const revalidate = 60; // ISR, fast re‑use
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id); // server‑side fetch
if (!product) return notFound();
return (
<main className="container">
<h1 className="text-3xl font-semibold">{product.name}</h1>
<div className="mt-4 grid md:grid-cols-2 gap-6">
<Image src={product.imageUrl} alt={product.name} width={800} height={600} priority />
{/* Specs and description ... */}
</div>
</main>
);
}
Data caching: revalidate, tag, and mutate precisely
Use route segment options and cache tags to control freshness without full rebuilds.
// lib/products.ts
import "server-only";
import { unstable_cache } from "next/cache";
export const getProduct = unstable_cache(
async (id: string) => {
const res = await fetch(process.env.API_URL + "/products/" + id, { cache: "no-store" });
if (!res.ok) throw new Error("failed");
return res.json() as Promise<{ id: string; name: string; imageUrl: string }>;
},
["product-by-id"],
{ revalidate: 60, tags: ["products"] }
);
export async function revalidateProducts() {
// In route handler after a write:
// revalidateTag("products");
}
Patterns
- Read‑heavy pages:
revalidate+ cache tags for cheap invalidation. - User‑specific pages:
cache: "no-store"and edge runtime when possible. - API routes mutating data should call
revalidateTagto keep UI fresh.
Streaming and Suspense boundaries
Stream the shell fast; load slow widgets later.
// app/dashboard/page.tsx
import { Suspense } from "react";
import KPIs from "./_components/KPIs";
import Activity from "./_components/Activity";
export default function Page() {
return (
<main>
<h1 className="text-2xl">Dashboard</h1>
<Suspense fallback={<div className="skeleton h-24" />}>
{/* Large query, stream when ready */}
<KPIs />
</Suspense>
<Suspense fallback={<div className="skeleton h-48" />}>
<Activity />
</Suspense>
</main>
);
}
Images and fonts: zero layout shift
- Use
next/imagewith explicitwidth/heightandsizes. - Mark critical hero images with
priority. - Self‑host fonts; use
display: swapandsize-adjustwhere helpful.
// app/layout.tsx
import { Roboto } from "next/font/google";
const roboto = Roboto({ subsets: ["latin"], display: "swap", weight: ["400", "500", "700"] });
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={roboto.className}>
<body>{children}</body>
</html>
);
}
Bundle control: ship less JS
Measure first, then cut.
Tactics
- Prefer server components; mark clients surgically.
- Dynamic import rarely used widgets with
{ ssr: false }only if safe. - Remove unused polyfills; target modern browsers.
- Tree‑shake UI libraries; import per module.
- Replace heavy libs (e.g., moment.js) with lighter options (date‑fns/dayjs).
// Example: client component boundary kept minimal
"use client";
import dynamic from "next/dynamic";
const HeavyChart = dynamic(() => import("@/components/HeavyChart"), { ssr: false, loading: () => <div className="h-40 skeleton" /> });
export default function AnalyticsClient() {
return <HeavyChart />;
}
Network: prefetch, cache, compress
- Enable HTTP/2/3, Gzip/Brotli.
- Use
prefetchon critical internal links (Next does it automatically on viewport). - Add long‑lived immutable caching for static assets (configured in your headers).
Edge vs Node runtime
If you need low‑latency reads and lightweight logic, run at the edge. For heavy Node APIs (DB drivers, sharp), keep Node runtime.
// app/api/hello/route.ts
export const runtime = "edge"; // or "nodejs"
export async function GET() {
return new Response(JSON.stringify({ ok: true }), { headers: { "content-type": "application/json" } });
}
Measuring performance
- Lighthouse CI per PR for regressions.
next/scriptstrategy and blocking script audits.- Real‑user monitoring for Core Web Vitals.
// app/web-vitals.tsx (already in your repo) – ensure it reports to analytics
// Example usage:
export function reportWebVitals(metric: any) {
// send to your analytics endpoint
}
Database and API performance
- Index query filters; paginate with cursors.
- Cache by filter signature; invalidate surgically.
- Stream large responses; prefer
206for exports.
Production checklist
- Server components by default; minimal client boundaries
- Critical path streamed; Suspense for slow data
- Correct caching:
revalidate/tags tuned per route - Images with dimensions; fonts with
display: swap - Bundle budget enforced; remove heavy deps
- Edge runtime where beneficial
- RUM for CWV; Lighthouse CI in pipeline
FAQ
Should I make everything static?
No. Mix ISR for read‑heavy pages, dynamic for user‑specific data, and cache tags for precise invalidation.
Do server components always reduce JS?
Yes—server components ship zero client JS. Only client boundaries hydrate.
How do I debug INP spikes?
Record performance profiles, find long tasks (>50ms), split work with useTransition, and remove heavy client libraries.
What about images coming from a CMS?
Use the Next Image optimizer or remote patterns in next.config.ts, and always set intrinsic sizes.
Ship small, stream early, cache smart. Your users—and Core Web Vitals—will thank you.
Advanced caching strategies (production)
Caching should be layered: browser, CDN/edge, application, and database. Use strong cache keys and precise invalidation to avoid stale content while maximizing hits.
1) CDN and HTTP headers
Cache-Control: Usepublic, max-age=31536000, immutablefor versioned static assets; shorter for HTML (e.g.,max-age=0, must-revalidate).ETag: Prefer weak ETags for HTML that changes often; strong ETags for JSON with version fields.Vary: IncludeAccept-Encoding,User-Agentonly when necessary; keep the Vary set minimal.
// next.config.ts headers example (static + security)
export default {
async headers() {
return [
{
source: "/:all*(js|css|woff2|svg|jpg|jpeg|png|webp|avif|gif|ico)",
headers: [
{ key: "Cache-Control", value: "public, max-age=31536000, immutable" },
],
},
{
source: "/(.*)",
headers: [
{ key: "Strict-Transport-Security", value: "max-age=15552000; includeSubDomains" },
],
},
];
},
};
2) Route Handlers with revalidateTag
When you mutate data, call revalidateTag to invalidate only the affected lists/pages.
// app/api/products/[id]/route.ts
import { revalidateTag } from "next/cache";
export async function PUT(req: Request, { params }: { params: { id: string } }) {
const body = await req.json();
await updateProduct(params.id, body);
revalidateTag("products"); // precise, cheap invalidation
return new Response(null, { status: 204 });
}
3) Cache signatures for lists
For filterable lists, create signatures from normalized query params. Use them as cache keys to maximize reuse.
function listSignature(q: URLSearchParams) {
const keys = ["category", "sort", "cursor", "limit"]; // allowlist
const norm = keys.map((k) => `${k}=${q.get(k) || ""}`).join("&");
return `products?${norm}`;
}
Images: remote patterns and responsive sizes
Configure remote hosts and add accurate sizes for responsive images to reduce transfer costs.
// next.config.ts
export default {
images: {
remotePatterns: [
{ protocol: "https", hostname: "images.example.com" },
{ protocol: "https", hostname: "cdn.example.net" },
],
formats: ["image/avif", "image/webp"],
},
};
<Image
src={product.imageUrl}
alt={product.name}
width={1600}
height={1200}
priority
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 800px"
/>
Middleware: smart redirects and lightweight logic
Use middleware for zero‑JS AB tests, locale redirects, or bot handling. Keep it fast and stateless.
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(req: NextRequest) {
// Example: redirect non‑www to www (or vice versa) early at edge
const url = req.nextUrl.clone();
if (url.hostname === "example.com") {
url.hostname = "www.example.com";
return NextResponse.redirect(url, 308);
}
return NextResponse.next();
}
Route Handlers: streaming & compression
Stream long responses progressively; set compression at the platform.
// app/api/stream/route.ts
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 5; i++) {
controller.enqueue(encoder.encode(JSON.stringify({ part: i }) + "\n"));
await new Promise((r) => setTimeout(r, 300));
}
controller.close();
},
});
return new Response(stream, { headers: { "content-type": "application/json" } });
}
Analyze and enforce a bundle budget
Install a bundle analyzer and enforce budgets in CI to prevent regressions.
npm i -D @next/bundle-analyzer
// next.config.ts
import withBundleAnalyzer from "@next/bundle-analyzer";
export default withBundleAnalyzer({ enabled: process.env.ANALYZE === "true" })({
// your config
});
ANALYZE=true next build
Define budgets (soft/hard limits) per page and fail CI on regressions.
Real‑user monitoring (RUM) for Vitals
Collect field data to spot regressions that lab tests miss.
// app/web-vitals.tsx
export function reportWebVitals(metric: any) {
fetch("/api/vitals", {
method: "POST",
keepalive: true,
headers: { "content-type": "application/json" },
body: JSON.stringify(metric),
});
}
// app/api/vitals/route.ts
export async function POST(req: Request) {
const metric = await req.json();
// store in your analytics backend (BigQuery, ClickHouse, etc.)
return new Response(null, { status: 204 });
}
Case study: 3.1x faster LCP
Baseline: LCP 3.2s P75 on product pages.
Actions:
- Server component for product details; removed client fetch and hydration.
- Added
revalidate=60and cache tags; wroterevalidateTag("products")after updates. - Converted hero image to AVIF with intrinsic sizes; set
priorityand accuratesizes. - Split a heavy client chart via dynamic import with
ssr: falseand a skeleton. - Enabled Brotli and reduced third‑party scripts by 2.
Result: LCP improved to 1.03s P75, CLS < 0.01, INP P75 140ms.
Troubleshooting guide
- High CLS: Missing intrinsic media sizes; late‑loading fonts without
display: swap. - High INP: Long tasks from large client components; add boundaries or reduce JS.
- High LCP: Hero image not optimized; no
priority; slow TTFB—move to edge, cache better. - Slow TTFB: Database N+1; add indexes; cache hot queries; stream shell earlier.
Additional checklists
Images
- AVIF/WebP where supported
- Intrinsic width/height set
- Accurate
sizesattribute -
priorityon hero media
JS budget
- Remove unused polyfills
- Replace heavy libs (moment → dayjs)
- Dynamic import rarely used widgets
- Minimize global client state
Data & caching
-
revalidateconfigured per route - Cache tags and precise invalidation
- No client fetch for server‑only data
- Edge runtime for low‑latency reads
Appendix A — Core Web Vitals Deep Dive
LCP (Largest Contentful Paint)
- Optimize TTFB: edge runtime, caching, streaming headers early.
- Preload hero image and font files with proper priorities.
- Avoid client rendering for above‑the‑fold content.
INP (Interaction to Next Paint)
- Eliminate long tasks (>50ms): split with
useTransition,requestIdleCallback. - Minimize hydration cost; reduce client component surface.
- Prefer CSS transitions over JS where possible.
CLS (Cumulative Layout Shift)
- Always reserve space for images/video/ads.
- Use font
size-adjustanddisplay: swap. - Avoid
content-visibility: autoon hero elements.
Appendix B — Caching Recipes by Page Type
- Marketing landing (rare updates):
revalidate = 3600, CDN cache HTML 5–10m. - Product listing (frequent writes):
revalidate = 60+revalidateTag("products")on writes. - User dashboard (personalized):
cache: "no-store", edge runtime, stream. - Blog posts: static generation, long‑lived asset cache; ISR for comments count badge.
Appendix C — Image Optimization Playbook
- Use
sizestailored to breakpoints; audit via Lighthouse. - Deduplicate responsive sources; avoid oversized transfers.
- Prefer AVIF; fall back to WebP.
- Serve placeholders (blur) for perceived performance on slow connections.
Appendix D — Fonts Strategy
- Self‑host; subset to used glyph ranges.
- Use
preloadfor the first text‑rendered font. - Consider system fonts for ultra‑fast first paint.
Appendix E — Middleware Patterns
- Geo/localization redirects via edge without client JS.
- Bot handling: fast deny for known bad UA; allow known renderers.
- A/B experiments: cookie‑driven variant selection in middleware.
Appendix F — Third‑Party Scripts Governance
- Inventory all tags; measure cost with WebPageTest.
- Load non‑critical scripts with
afterInteractiveorlazyOnload. - Consider server‑side proxies for heavy analytics to reduce main‑thread work.
Appendix G — RUM Implementation Details
// app/web-vitals.tsx (extended example)
export function reportWebVitals(metric: any) {
const body = JSON.stringify({
id: metric.id,
name: metric.name,
value: metric.value,
label: metric.label,
navigationType: (performance.getEntriesByType("navigation")[0] as any)?.type,
});
navigator.sendBeacon?.("/api/vitals", body) || fetch("/api/vitals", { method: "POST", body, keepalive: true });
}
Appendix H — Bundle Budget Enforcement
- Set per‑route JS budgets (e.g., ≤120KB P95 client JS).
- Fail CI on regression; attach analyzer HTML as artifact.
- Create owner alerts when budget exceeded.
Appendix I — Edge Rendering Patterns
- Read‑only personalization from cookies/Geo at edge.
- Stale‑while‑revalidate on HTML for near‑instant navigations.
- Stream shell immediately; fetch slow widgets downstream.
Appendix J — Profiling Checklist
- Record performance profiles in DevTools; find long tasks.
- Audit hydration timings; move logic server‑side when possible.
- Remove unnecessary providers and global state.
Appendix K — Deployment Recipes
- Canary by path segment; monitor vitals deltas.
- Blue/green with traffic split; roll back on SLO breach.
- Pre‑warm caches after deploy using a sitemap crawler.
Appendix L — Routing and Data‑Fetch Patterns
Static with ISR + Tag Invalidation
- Use for read‑heavy content that updates occasionally.
- Pair
revalidatewithrevalidateTagin route handlers after writes.
// app/blog/[slug]/page.tsx
export const revalidate = 600; // 10m
export default async function Page({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug); // cached by tag "posts"
return <Article post={post} />;
}
// app/api/admin/posts/[id]/route.ts
import { revalidateTag } from "next/cache";
export async function PUT(req: Request, { params }: { params: { id: string } }) {
await updatePost(params.id, await req.json());
revalidateTag("posts");
return new Response(null, { status: 204 });
}
Dynamic with Edge Runtime
- Personalized dashboards; fast read latencies.
// app/(dash)/layout.tsx
export const runtime = "edge";
Parallel Routes for Faster Above‑the‑Fold
- Keep critical shell separate and stream.
// app/(shop)/@shell/page.tsx
export default function Shell() { return <Hero />; }
// app/(shop)/@details/page.tsx
export default async function Details() { return <Suspense fallback={<Skeleton/>}><DetailsInner/></Suspense>; }
Appendix M — Server Components Patterns
- Move data fetching to server components to reduce client JS.
- Keep client boundaries minimal, colocated with interactivity.
- Avoid passing large props; fetch again server‑side at child when needed.
// server component
export default async function Orders() {
const orders = await listOrders();
return <OrdersTable orders={orders} />;
}
// client boundary kept small
"use client";
export function OrdersFilter({ initial }: { initial: string }) {
const [q, setQ] = useState(initial);
return <input value={q} onChange={(e)=>setQ(e.target.value)} />;
}
Appendix N — Suspense and Streaming Recipes
- Wrap slow widgets in
Suspensewith lightweight skeletons. - Stream the shell immediately to improve TTFB and LCP.
- Compose multiple independent
Suspenseboundaries.
<Suspense fallback={<div className="skeleton h-36" />}>
<RevenueWidget />
</Suspense>
<Suspense fallback={<div className="skeleton h-24" />}>
<TrafficWidget />
</Suspense>
Appendix O — Cache Tagging Playbook
- Tag per entity collection:
products,categories,posts. - Invalidate on writes only the affected tags.
- Avoid global revalidate; keep invalidations surgical.
Checklist:
- Define tags per route family
- Centralize invalidation calls in write paths
- Monitor tag hit/miss metrics
Appendix P — Page‑Level Performance Budgets
- Home: ≤ 100KB JS, LCP ≤ 1.2s, INP ≤ 200ms
- Product: ≤ 160KB JS, LCP ≤ 1.5s, INP ≤ 200ms
- Dashboard: ≤ 220KB JS (client), long task P95 ≤ 40ms
Enforce in CI with analyzer and thresholds.
Appendix Q — Image and CDN Configuration
- Prefer AVIF/WebP; high‑DPR variants for retina only when needed.
- Configure responsive
sizesaccurately; audit in devtools. - Coalesce requests via CDN; ensure HTTP/2/3 enabled.
Appendix R — Script Loading Strategies
- Critical inline:
beforeInteractiveonly when absolutely necessary. - Most third‑party:
afterInteractive; analytics:lazyOnload. - Replace heavy inline widgets with server‑rendered proxies.
Appendix S — CSS and Rendering
- Prefer CSS features (containment, content‑visibility) cautiously.
- Reduce style recalculations; avoid large global styles.
- Modularize CSS; avoid heavy runtime CSS‑in‑JS on critical path.
Appendix T — Accessibility and Performance
- Ensure focus management without extra JS.
- Avoid hidden but rendered heavy DOM trees.
- Use semantic elements which often render cheaper.
Appendix U — Mobile‑First Optimization
- Target mid‑tier Android devices in testing.
- Budget for CPU limits; ensure 60fps scroll.
- Optimize touch targets; reduce reflows on input.
Appendix V — Database and API Performance Patterns
- Use server‑side pagination with stable sort keys.
- Prefer keyset pagination (cursors) for large datasets.
- Add composite indexes for frequent filters.
Appendix W — Observability and Alerting
- Lighthouse CI regression alerts per PR.
- Field Vitals alert: LCP P75 > 2.5s, INP P75 > 250ms, CLS P75 > 0.1.
- Token and cache metrics for API/data layers.
Appendix X — Networking and Compression
- Enable Brotli; verify content‑encoding in responses.
- Long‑lived immutable cache for versioned assets.
- Tweak server timeouts and keep‑alive for streaming.
Appendix Y — Build and CI Hardening
- Fail build on budget regression; attach analyzer artifact.
- Run type‑check and lint in parallel; cache results.
- Use incremental builds; avoid unnecessary transpilation.
Appendix Z — Troubleshooting Patterns
Symptoms → Actions:
- High LCP on hero pages → Optimize image, preload, stream shell sooner, move logic to server.
- High INP after hydration → Split client components, reduce props, use
useTransition. - CLS spikes on mobile → Reserve media slots, correct font strategy, avoid late layout changes.
Appendix AA — Performance Review Checklist (100 items)
- JS budget defined per page
- Bundle analyzer integrated
- Client boundaries audited
- Server components preferred
- Dynamic import used for rare widgets
- No unused polyfills
- Moment replaced with dayjs/date‑fns
- Tree‑shaken UI libs
- No heavy charts on first paint
- Critical CSS minimal
- Fonts self‑hosted
display: swapenabled- Font subsets generated
- Hero images
priority - Accurate
sizesattributes - AVIF/WebP formats
- CDN configured for images
- Remote patterns set in next.config
- Preload key assets
- Avoid render‑blocking scripts
- Use
afterInteractivefor third‑party - Lazy load analytics
- No synchronous XHR
- HTTP/2 or HTTP/3 enabled
- Brotli enabled
- Gzip fallback configured
- Cache‑Control set for static
- ETag/Last‑Modified tuned
stale-while-revalidatefor HTML where safe- Edge runtime for read‑heavy pages
- Streaming route handlers used
- Suspense boundaries for slow widgets
- Skeletons lightweight
- Avoid expensive SVG filters
- Use CSS transforms over layout thrash
- Avoid forced reflow patterns
- Event listeners passive where possible
- Debounce input handlers
requestIdleCallbackfor low‑priority workuseTransitionfor state updates- Reduce effect dependencies
- Avoid deep prop drilling
- Memoize expensive components
- Virtualize long lists
- RUM implemented
- Web Vitals sent to backend
- Alerting thresholds defined
- Lighthouse CI per PR
- Budget thresholds guarded in CI
- Smoke tests for vitals on staging
- Synthetic checks for critical paths
- CDN cache hit ratio tracked
- API latency P95 monitored
- DB slow query log reviewed
- Indexes audited quarterly
- N+1 checks in server code
- Cache tags documented
- Invalidation paths tested
- Sitemap warmup job post‑deploy
- Preconnect to critical origins
- DNS‑prefetch where safe
- Third‑party cost inventory
- Kill switch for heavy tags
- Feature flags for risky features
- Blue/green rollback script
- Canary metrics comparison
- Service worker only if justified
- No double hydration
- No duplicate React versions
- Minimal context providers
- Avoid global state when possible
- Use selectors in state libs
- Avoid large JSON in props
- Serialize small shapes only
- Prefer IDs then fetch server‑side
- Ensure streaming flush early
- Avoid large HTML comments
- Compress JSON responses
- Paginate API responses
- Prefer 206 for large downloads
- Avoid inline base64 for large images
- Use CDN image optimizer
- Verify CORS preflight costs
- Keep headers minimal
- Limit cookies size
- SameSite and secure flags
- Avoid layout jank on toasts/modals
- Use portal with reserved area
- Test low‑end devices
- Emulate throttled CPU/network
- Measure interaction flows
- Ensure focus outlines visible
- Avoid ARIA where semantics suffice
- Preload next route data where safe
- Use
prefetchon Link - Avoid oversized hit targets
- Avoid
position: fixedabuse - Reduce z‑index layers
- Keep DOM depth reasonable
- Document learnings in ADRs
Appendix AB — Example Headers and Policies
// next.config.ts headers (extended)
export default {
async headers() {
return [
{
source: "/:all*(js|css|woff2|svg|jpg|jpeg|png|webp|avif|gif|ico)",
headers: [
{ key: "Cache-Control", value: "public, max-age=31536000, immutable" },
],
},
{
source: "/(.*)",
headers: [
{ key: "Strict-Transport-Security", value: "max-age=15552000; includeSubDomains" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Permissions-Policy", value: "accelerometer=(), geolocation=(), microphone=()" },
],
},
];
},
};
Appendix AC — Sample RUM Backend (ClickHouse)
CREATE TABLE vitals (
id String,
name LowCardinality(String),
value Float64,
ts DateTime DEFAULT now(),
url String,
user_agent String
) ENGINE = MergeTree ORDER BY (name, ts);
SELECT name, quantileExactWeighted(0.75)(value, 1) AS p75
FROM vitals
WHERE ts > now() - INTERVAL 7 DAY
GROUP BY name
ORDER BY name;
Appendix AD — Core Web Vitals SLOs and Guardrails
- SLO 01: LCP P75 ≤ 1.5s on mobile, ≤ 1.2s on desktop
- SLO 02: INP P75 ≤ 200ms
- SLO 03: CLS P75 ≤ 0.1
- SLO 04: TTFB P75 ≤ 300ms (HTML)
- SLO 05: JS payload P95 ≤ 150KB per route (client JS)
- SLO 06: Image bytes P95 ≤ 1200KB per route
- SLO 07: Third‑party time < 10% of total main‑thread
- SLO 08: Error rate < 1% per route
- SLO 09: Cache hit ratio ≥ 80% for static assets
- SLO 10: HTML revalidation ratio ≥ 50% on read‑heavy pages
Alert thresholds:
- Warn at 80% of SLO, page owner notified
- Critical at 100% breach, block release until mitigated
Appendix AE — Route Archetypes and Patterns
- Marketing landing: static shell, ISR 15–60m, edge cache HTML
- Product detail: semi‑static with ISR (60–300s), tag invalidate on price/stock
- Dashboard: dynamic, edge runtime, stream widgets behind Suspense
- Search results: dynamic with query signature, cache sets per filter
- Blog/article: fully static, long‑lived assets, image formats AVIF/WebP
- Checkout: dynamic, Node runtime for PCI SDKs, minimal JS
Appendix AF — State Management and Hydration Budget
- Avoid global client state for read‑only data; fetch in server components
- Keep client boundaries near interactivity, not at page root
- Hydration budget: ≤ 100ms main‑thread on mid‑tier Android
- Use
useTransitionfor heavy updates; avoid sync reducers on input events
Checklist:
- Remove unnecessary providers
- Memoize selectors to reduce re‑renders
- Split large client trees into islands
- Defer non‑critical effects with
requestIdleCallback
Appendix AG — Images Pipeline Checklist
- Use AVIF/WebP with fallbacks
- Provide intrinsic width/height and precise
sizes - Deduplicate sources across breakpoints
- Lazy‑load below‑the‑fold;
priorityonly above‑the‑fold - Use CDN resizing; avoid sending original large assets
- Strip EXIF unless required
- Preload hero image and first text font
Appendix AH — CDN Tuning and Headers
Cache-Controlfor assets:public, max-age=31536000, immutable- HTML:
max-age=0, must-revalidate, stale-while-revalidate=60 - ETag/Last‑Modified for conditional fetches
Varykept minimal: content negotiation only when essential- Preconnect to critical origins; DNS‑prefetch for third‑party if needed
Appendix AI — Prefetching and Preloading
- Use Next.js Link prefetch (viewport) for internal routes
- Preload critical fonts (
as=font,crossorigin) - Preload hero image or CSS only when demonstrably helpful
- Avoid over‑preload; measure with Lighthouse and RUM
Appendix AJ — Third‑Party Governance Matrix
- Inventory columns: script, owner, purpose, bytes, CPU ms, async strategy, consent gate, fallback
- Block list: any tag failing security or performance bar
- Quarterly review: remove stale or redundant vendors
Appendix AK — Accessibility Meets Performance
- Semantic HTML reduces JS need for interactions
- Focus management without extra libraries
- Avoid DOM churn that disrupts assistive tech and causes layout shifts
Appendix AL — Mobile First Test Matrix
- Devices: mid‑tier Android, iPhone baseline, low‑end Android
- Networks: 4G slow, 3G, offline for SW tests
- Metrics captured: LCP/INP/CLS, bytes, long task counts, memory
Appendix AM — Database Performance Recipes
- Add composite indexes on common filters and sorts
- Prefer keyset pagination; avoid offsets for deep pages
- Use prepared statements; pool connections; cap concurrency
Appendix AN — API Performance Recipes
- Coalesce concurrent identical fetches (request coalescing)
- Compress JSON (Brotli); prefer compact shapes
- Stream large responses with chunked transfer
Appendix AO — Build System Optimizations
- Enable SWC/tsconfig target modern
- Remove legacy polyfills; browserslist tuned to analytics
- Cache node_modules and Next build artifacts in CI
Appendix AP — Observability Dashboards
- Pages dashboard: LCP/INP/CLS, JS bytes, image bytes, errors
- Assets dashboard: cache hits, top offenders, size regressions
- API dashboard: P50/P95 latency, error rates, throughput
Appendix AQ — Release Policy
- Block release on SLO breach without approved waiver
- Require bundle analyzer diff for significant dependency changes
- Canary 10% traffic minimum for risky changes
Appendix AR — Cookbook: Common Fixes
- High LCP (image): convert to AVIF, accurate
sizes, preload, priority - High INP (interaction): split component, move logic server‑side, debounce
- High CLS: set image dimensions, font strategy, avoid banners pushing content
- Slow TTFB: cache layer, edge runtime, DB query optimization
Appendix AS — Example Edge Middleware Library
// middleware/lib.ts
export function redirectNonWww(url: URL) {
if (url.hostname === "example.com") { url.hostname = "www.example.com"; return url; }
return null;
}
Appendix AT — Security Headers and CSP
- Add CSP with hashes for inline critical scripts only when needed
- Disallow
unsafe-inlinebroadly; prefer hashed/predefined snippets - Monitor CSP violations to spot regressions
Appendix AU — Performance Budgets by PR Template
### Performance checklist
- [ ] JS added ≤ X KB or justified
- [ ] No new render‑blocking scripts
- [ ] Images optimized and sized
- [ ] RUM/Lighthouse checked
Appendix AV — Team Roles and Ownership
- Page owners responsible for SLOs
- Perf champions rotate per quarter
- CI gatekeeper validates budgets
Appendix AW — Education Plan
- Monthly brown‑bags on CWV and Next.js patterns
- Docs in repo with before/after examples
- Onboarding includes perf checklist review
Appendix AX — Anti‑patterns to Avoid
- Client fetching for server‑ready data
- Large client providers at root
- Over‑preloading fonts/assets
- Heavy third‑party tags on critical pages
Appendix AY — Post‑Incident Review Template
Impact: metrics affected, duration, scope
Root cause: code, infra, vendor
Fix: immediate and long‑term
Learnings: prevent recurrence
Appendix AZ — Long List of Micro‑Checkpoints (200)
- Remove unused imports in client code
- Avoid blocking synchronous loops on render
- Prefer
fetchin server components - Ensure
revalidateset appropriately - Tag caches for list pages
- Invalidate tags on writes
- Prefer edge for read‑only widgets
- Stream HTML shell ASAP
- Add lightweight skeletons
- Avoid heavy SVG filters
- Inline critical CSS sparingly
- Use
next/scriptstrategies - Defer analytics
- Preconnect to image CDN
- Compact JSON payloads
- Remove moment.js
- Replace with date‑fns/dayjs
- Tree‑shake components
- Split bundles logically
- Avoid deep prop drilling
- Use context selectors
- Memoize heavy lists
- Virtualize large lists
- Avoid layout thrash
- Use CSS transforms
- Reserve media space
- Swap fonts
- Subset fonts
- Preload first font only
- Accurate
sizes - Prefer AVIF
- Lazy images below fold
- Optimize hero
- Prefer server sorting/filtering
- Use keyset pagination
- Index DB filters
- Avoid N+1 queries
- Batch loaders
- CDN cache keys correct
- HTML SWR where safe
- ETag configured
- Gzip/Brotli enabled
- HTTP/2/3 enabled
- Keep‑alive tuned
- Timeouts reasonable
- Do not block main thread
- Move work off thread
- Use Web Workers when needed
- Avoid heavy client charts
- Render charts server‑side when possible
- Collapse third‑party
- Gate by consent
- Remove dead tags
- Monitor tag cost
- Budget per route
- Analyzer in CI
- Artifact link posted
- Owners notified
- Canary monitored
- Rollback scripts ready
- Warm caches post deploy
- Crawl sitemap
- Precompute common queries
- Normalize filter params
- Stable cache signatures
- Avoid global try/catch masking
- Log structured errors
- Sample traces
- Alert on regressions
- Track long tasks
- Track memory leaks
- Test low‑end devices
- Throttle CPU/network
- Audit CLS snapshots
- Audit INP interactions
- Audit LCP element
- Document fixes
- Share learnings
- Update ADRs
- Clean stale feature flags
- Remove legacy polyfills
- Modern browserslist
- Avoid large i18n payloads
- Lazy load locales
- Prefer small date libs
- Purge unused CSS
- CSS containment wisely
- Avoid heavy shadows
- Simplify component trees
- Minimize hydration props
- Use streaming responses
- Prefer edge caching
- Audit cookie sizes
- Reduce header bloat
- Compress responses
- Use 206 for large files
- Avoid base64 images inline
- Serve icons as SVG sprite
- Coalesce requests
- Reuse connections
- Avoid SSR CPU spikes
- Limit sync crypto in Node
- Cache secrets securely
- Avoid dynamic require
- ES modules preferred
- Use SWC plugins carefully
- Source maps limited in prod
- Remove console logs
- Disable React devtools in prod
- Avoid eval in CSP
- Hash inline scripts
- Secure headers set
- Permissions‑Policy tight
- HSTS enabled
- Referrer‑Policy strict
- SameSite cookies
- HttpOnly cookies
- Avoid setTimeout loops
- Prefer requestAnimationFrame
- Avoid observers on many nodes
- Unobserve on cleanup
- Avoid forced sync layout
- Use ResizeObserver minimal
- Debounce scroll handlers
- Passive listeners
- Avoid oversized JSON
- Stream logs
- Batch analytics
- Sample rates reasonable
- Avoid conflicting CSS
- Keep z‑index minimal
- Reduce DOM depth
- Avoid massive tables
- Paginate tables
- Sticky headers carefully
- Use
containsparingly - Avoid CSS heavy filters
- Inline small SVGs
- Sprite larger icons
- Avoid data URIs for big assets
- Use
link rel=preloadcarefully - Remove duplicate preloads
- Use
preconnectsparingly - Cache fonts long
- Version assets
- Immutable URLs
- SRI where applicable
- Validate integrity
- Avoid try/catch hot paths
- Prefer fast path checks
- Memoize selectors
- Avoid spreading props blindly
- Move constants out of components
- Avoid anonymous functions when hot
- Key lists properly
- Avoid re‑mount cascades
- Prefer CSS animations
- Avoid JS heavy animations
- Prefer transform/opacity
- Avoid box‑shadow animations
- Avoid layout thrash in modals
- Portal for overlays
- Preallocate overlay area
- Keep skeletons simple
- Reuse skeleton components
- Avoid over‑fetching in client
- Use suspense boundaries
- Reduce boundary count if heavy
- Combine small widgets
- Avoid nested scroll containers
- Prefer native scrolling
- Reduce sticky elements
- Visibility toggles cheap
- Avoid repaint thrash
- Group DOM updates
- Avoid sync layout reads/writes
- Use
useLayoutEffectsparingly - Prefer
useEffectfor non‑visual - Split bundles by route
- Remove unused locales from libs
- Prefer light maps libs
- Static maps where possible
- Reduce third‑party iframes
- Lazy load embeds
- Consent gates for tracking
- Measure before adding vendors
- Define exit plan for vendors
- Budget per vendor
- Monitor vendor outages
- Fallback UIs for vendor failure
- Keep error boundaries friendly
- Log user agent and net type
- Optimize critical flows first
- Prioritize revenue pages
- Prioritize onboarding speed
- Remove blockers to first action
- Optimize warm navigations
- Cache same‑origin GETs
- Use HTTP/2 push? (deprecated; prefer preload)
- Keep learning and iterating
Appendix BA — Final Quick Reference (Top 50)
- Prefer server components; keep client islands small
- Stream the shell early; wrap slow widgets in Suspense
- Accurate image sizes; AVIF/WebP; priority for hero
- Self‑host fonts;
display: swap; preload first text font - Define JS budgets; analyze bundles in CI
- Remove heavy libs (moment → dayjs); tree‑shake UI libs
- Use edge runtime for read‑only personalized pages
- Cache with
revalidate+ tags; invalidate on writes - CDN: long cache for assets; HTML SWR for read‑heavy pages
- RUM Web Vitals; alert on SLO breaches
- Preconnect and preload sparingly; measure impact
- Lazy‑load below‑fold images and non‑critical scripts
- Keep middleware fast; do only what’s essential at edge
- Prefer CSS transforms/opacity for animations
- Avoid layout thrash; reserve media space
- Cursor pagination; batch resolver/API calls
- Compress JSON; Brotli for assets and API
- Minimize global state; memoize selectors
- Prefer system fonts or subsetting for speed
- Canary risky releases; pre‑warm caches
- Strict headers (HSTS, CSP, Referrer‑Policy)
- Keep cookies small; secure flags
- Limit third‑party; gate by consent; monitor cost
- Document fixes; share ADRs; keep checklists current
- Test on mid‑tier Android; throttle CPU/network
- Use
useTransitionfor heavy updates - Avoid double hydration and duplicate React versions
- Paginate big tables; virtualize long lists
- Prefer server sorting/filtering
- Normalize filter params for cache signatures
- Batch analytics; sample reasonably
- Avoid oversized JSON in props; fetch server‑side
- Defer non‑critical effects with
requestIdleCallback - Prefer
afterInteractivefor scripts;lazyOnloadfor analytics - Keep DOM depth reasonable; reduce z‑index layers
- Remove console logs and dev‑only code in prod
- Protect performance in reviews; PR template checklist
- Educate team; rotate perf champions
- Track long tasks P95; fix root causes
- Reduce hydration props; keep boundaries local
- Use remote image patterns and modern formats
- Ensure edge/platform compression enabled
- Instrument per‑route bytes and vitals in dashboards
- Fail CI on budget regressions; attach artifacts
- Set operation timeouts and circuit breakers
- Coalesce duplicate fetches; cache GETs
- Prefer node runtime where libs require it (sharp, DB)
- Keep error boundaries friendly and cheap
- Build small, deploy fast; roll back faster
- Measure, iterate, and keep shipping
Appendix BB — Glossary
- App Router: Next.js routing system with server components and layouts
- RSC: React Server Components, render on server without shipping JS
- ISR: Incremental Static Regeneration; revalidate static pages
- INP: Interaction to Next Paint; input latency metric
- LCP: Largest Contentful Paint; loading metric
- CLS: Cumulative Layout Shift; visual stability metric
- SWR: Stale‑While‑Revalidate; serve stale while refreshing in background
- SLO: Service Level Objective; target metric threshold
- RUM: Real‑User Monitoring; field data from real users
- Edge runtime: lightweight JS runtime at CDN edge
- Bundle budget: per‑route JS size targets
- Suspense: React primitive to suspend rendering while awaiting data
- Streaming: sending HTML chunks progressively as ready
Appendix BC — References
- Next.js docs (routing, caching, images)
- React docs (Suspense, server components)
- Web Vitals (LCP, INP, CLS)
- Lighthouse and WebPageTest
- Chrome DevTools performance profiler
Appendix BD — Final Notes
- Performance is product quality. Budget it like a feature.
- Favor simplicity; fewer moving parts ship faster and break less.
- Iterate with data; field metrics trump lab scores alone.
- Teach the patterns; good defaults beat checklists.
Appendix BE — Addendum: Example headers() with CSP
// next.config.ts (CSP example)
export default {
async headers() {
const csp = [
"default-src 'self'",
"script-src 'self' 'sha256-abc...' https://www.googletagmanager.com",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self' data:",
"connect-src 'self' https:",
"frame-ancestors 'none'",
].join('; ');
return [
{ source: '/(.*)', headers: [
{ key: 'Content-Security-Policy', value: csp },
]},
];
},
};
Appendix BF — Template: Performance ADR
# ADR: Introduce JS budget and bundle analyzer in CI
Context: Client JS growth risks INP and slow TTI.
Decision: Enforce 120KB P95 per route, fail CI on regression.
Consequences: Some features require code splitting; docs updated.
Appendix BG — Template: Rollback Plan
Trigger: Vitals breach or error spike after deploy
Steps: Roll back to previous build, invalidate caches, notify owners
Verification: RUM dashboards normal; incident closed
Appendix BH — Sample PR Template (Full)
### Summary
What changed and why?
### Performance
- JS added (KB):
- Images optimized: yes/no
- Affects critical path: yes/no
### Testing
- RUM/Lighthouse: attach screenshots
- Devices/Networks tested:
### Risks
- Rollback plan:
Appendix BI — Sample perf.config.json
{
"budgets": {
"home": { "clientJsKB_P95": 100 },
"product": { "clientJsKB_P95": 160 },
"dashboard": { "clientJsKB_P95": 220 }
},
"alerts": { "slackWebhook": "https://hooks.slack.com/services/..." }
}
Appendix BJ — End-to-End Perf Test Script (Pseudo)
import { test, expect } from '@playwright/test';
test('home vitals smoke', async ({ page }) => {
await page.route('**/*', (route) => route.continue());
await page.goto('https://www.example.com/');
// check hero visible quickly
const start = Date.now();
await page.getByRole('heading', { name: /welcome/i }).waitFor();
const lcpMs = Date.now() - start;
expect(lcpMs).toBeLessThan(1200);
});
Appendix BK — Final Checklist Snapshot (10)
- Server components default
- Streamed shell with Suspense
- Accurate images and fonts
- JS budgets enforced
- Edge runtime where it helps
- Cache strategy documented
- RUM + alerts configured
- Third‑party governed
- CI gates in place
- Rollback and warmup ready
Appendix BL — Credits
- Maintainers: Performance guild @ Elysiate
- Contributors: Frontend, Platform, SRE teams
Appendix BM — Changelog (Perf Playbook)
- 2025‑10‑25: Initial comprehensive edition with RSC, streaming, caching, and CWV
Appendix BN — Footer
Thank you for caring about performance. Fast feels good.
Appendix Index: A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, AA, AB, AC, AD, AE, AF, AG, AH, AI, AJ, AK, AL, AM, AN, AO, AP, AQ, AR, AS, AT, AU, AV, AW, AX, AY, AZ, BA, BB, BC, BD, BE, BF, BG, BH, BI, BJ, BK, BL, BM, BN.
Appendix BO — Tuning Playbook (Quick Steps)
- Step 001: Audit current LCP/INP/CLS with RUM and Lighthouse
- Step 002: Identify LCP element and image/font dependencies
- Step 003: Stream shell earlier; add Suspense around slow widgets
- Step 004: Convert hero image to AVIF/WebP with accurate sizes
- Step 005: Self‑host fonts; preload first text font; enable swap
- Step 006: Reduce client JS by moving logic to server components
- Step 007: Define per‑route JS budgets and enforce in CI
- Step 008: Replace heavy libraries; tree‑shake UI components
- Step 009: Dynamic import rarely used widgets with skeletons
- Step 010: Configure CDN long cache for assets; SWR for HTML
- Step 011: Add cache tags to lists; invalidate on writes
- Step 012: Push read‑only personalization to edge runtime
- Step 013: Prefetch internal links where useful; measure impact
- Step 014: Defer non‑critical scripts with afterInteractive/lazyOnload
- Step 015: Remove third‑party tags without clear value
- Step 016: Gate remaining third‑party behind consent
- Step 017: Preconnect only to critical origins; drop unused
- Step 018: Reserve layout space for media and dynamic UI
- Step 019: Subset fonts; minimize variants; avoid FOIT/FOUT
- Step 020: Enable Brotli; verify content‑encoding in responses
- Step 021: Compress JSON; reduce payload shapes
- Step 022: Coalesce duplicate fetches; cache same‑origin GETs
- Step 023: Use keyset pagination; index common filters
- Step 024: Batch resolver/API calls; avoid N+1
- Step 025: Profile interactions; split long tasks with transitions
- Step 026: Minimize global state; memoize selectors
- Step 027: Virtualize long lists; paginate big tables
- Step 028: Remove console logs and dev‑only code in prod
- Step 029: Add error boundaries; keep fallbacks cheap
- Step 030: Warm caches after deploy via sitemap crawler
- Step 031: Canary risky changes; monitor vitals deltas
- Step 032: Add alerts on SLO breaches with owner routing
- Step 033: Document fixes in ADRs; share learnings team‑wide
- Step 034: Re‑run Lighthouse CI; compare bundle analyzer
- Step 035: Validate CLS snapshots and INP interactions on mobile
- Step 036: Re‑baseline budgets; lock improvements in CI
- Step 037: Remove stale feature flags and dead code
- Step 038: Tune middleware to be minimal and fast
- Step 039: Switch blocking CSS/JS to non‑blocking where safe
- Step 040: Replace layout animations with transform/opacity
- Step 041: Reduce DOM depth and z‑index layers
- Step 042: Use
sizesprecisely for responsive images - Step 043: Audit cookie size and headers bloat
- Step 044: Add
stale-while-revalidatewhere safe for HTML - Step 045: Ensure remote image patterns and modern formats
- Step 046: Quantify third‑party CPU time; set budget
- Step 047: Add circuit breakers and timeouts to APIs
- Step 048: Keep Node runtime where native libs required
- Step 049: Stream route handlers for long responses
- Step 050: Verify hydration props minimal and local
- Step 051: Prefer system fonts for ultra‑fast prototypes
- Step 052: Strip EXIF; dedupe responsive sources
- Step 053: Avoid nested scroll containers; prefer native scroll
- Step 054: Debounce input handlers; use passive listeners
- Step 055: Preload only assets with measured benefit
- Step 056: Validate integrity and CSP for scripts
- Step 057: Add PR performance checklist template
- Step 058: Build and store analyzer artifacts per PR
- Step 059: Alert owners automatically on regressions
- Step 060: Throttle CPU/network during local testing
- Step 061: Verify edge caches hit ratio and TTLs
- Step 062: Reduce hydration surface by shrinking client islands
- Step 063: Ensure server sorting/filtering over client work
- Step 064: Normalize query params for cache signatures
- Step 065: Prefer streaming HTML over client waterfalls
- Step 066: Remove unused polyfills via modern browserslist
- Step 067: Purge unused CSS; leverage containment carefully
- Step 068: Avoid large inline data URIs; use CDN assets
- Step 069: Track long tasks P95 per route in dashboards
- Step 070: Add owner and SLO metadata to pages
- Step 071: Set release policy to block on SLO breaches
- Step 072: Provide rollback and warm‑up procedures
- Step 073: Educate devs on RSC, Suspense, caching
- Step 074: Keep a live perf playbook in the repo
- Step 075: Review third‑party list quarterly
- Step 076: Benchmark on representative devices
- Step 077: Add accessibility checks that also aid perf
- Step 078: Keep JSON schemas compact for API
- Step 079: Paginate analytics exports; prefer 206
- Step 080: Validate edge vs node runtime per route
- Step 081: Verify
next/imagedomains and formats - Step 082: Use skeletons that are cheap to render
- Step 083: Trim locale/data bundles; lazy load
- Step 084: Remove duplicate preloads/preconnects
- Step 085: Record and share before/after flamecharts
- Step 086: Track JS bytes and image bytes per commit
- Step 087: Celebrate wins; keep iterating with data