Next.js Performance Patterns I Use in Every Project
Battle-tested Next.js patterns for image optimization, code splitting, API route caching, and runtime performance that I apply across every client project.
Beyond the Docs: Patterns from Production
Next.js documentation covers the basics of performance. But after shipping 17+ production Next.js applications across fintech, real estate, and SaaS, I've developed patterns that go well beyond next/image and dynamic imports.
These are the patterns I apply to every client project from day one.
1. The Route Group Preload Pattern
Next.js App Router supports route groups, but most developers use them only for layout organization. I use them for strategic preloading.
app/
(marketing)/ → Lightweight layout, minimal JS
page.js
about/
(dashboard)/ → Heavy layout, loaded only when needed
dashboard/
settings/By separating marketing pages from dashboard pages into route groups with different root layouts, the marketing site loads zero dashboard JavaScript. This reduced our marketing page bundle by 73%.
2. API Route Response Streaming
For API routes that aggregate data from multiple sources, I use streaming responses instead of waiting for all data to resolve:
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
const sources = [fetchUserData(), fetchAnalytics(), fetchNotifications()];
for (const promise of sources) {
const data = await promise;
controller.enqueue(encoder.encode(JSON.stringify(data) + "\n"));
}
controller.close();
},
});
return new Response(stream);
}On the client, I parse the stream progressively. The UI populates section by section instead of all-at-once, improving perceived load time by 40%.
3. Image Optimization Beyond next/image
next/image handles format conversion and resizing, but I add two more layers:
Blur placeholder generation at build time:
I generate tiny (20px wide) blurred placeholder images during the build step and inline them as base64. This eliminates the layout shift that occurs while the full image loads.
Priority hints for above-the-fold:
Only the hero image and the first visible project thumbnail get priority={true}. Everything else uses loading="lazy". This single change improved LCP by 800ms on one project.
4. The SWR Cascade Pattern
For dashboards with multiple data-dependent components, I chain SWR hooks with a dependency cascade:
const { data: user } = useSWR('/api/user');
const { data: projects } = useSWR(
user ? `/api/projects?userId=${user.id}` : null
);
const { data: analytics } = useSWR(
projects ? `/api/analytics?projectIds=${projects.map(p => p.id).join(',')}` : null
);Each hook only fires when its dependency is available. Combined with SWR's built-in caching, subsequent page visits load the entire dashboard from cache in under 100ms.
5. Font Loading Strategy
I've seen Next.js apps load 6+ font weights "just in case." My rule: load only what renders above the fold, then lazy-load the rest.
const inter = Inter({
subsets: ['latin'],
weight: ['400', '700'], // Only regular and bold
display: 'swap',
});Additional weights (300, 500, 900) are loaded via a @font-face declaration with font-display: optional, meaning they only apply if they've already been cached from a previous visit.
6. Middleware-Level Redirects
Client-side redirects cause a visible flash. Server-side redirects in getServerSideProps add latency. I handle common redirects in Next.js middleware, which runs at the edge before any page rendering occurs:
This eliminates both the flash and the server latency. Redirects complete in under 10ms.
7. Static Shell + Dynamic Islands
For pages that are 80% static and 20% dynamic, I use a pattern I call Static Shell + Dynamic Islands:
The page itself is statically generated (ISR). Dynamic parts like user-specific data, real-time counters, or personalized recommendations are loaded as client-side islands inside Suspense boundaries.
The static shell loads in ~200ms. Dynamic islands populate within 500ms. Users see a complete, interactive page in under a second.
Measuring Impact
Every pattern above is driven by metrics. The three I track on every project:
| Metric | Target | Tool |
|---|---|---|
| LCP | < 1.5s | Vercel Analytics |
| FID | < 50ms | Web Vitals |
| Bundle Size | < 150kB first load | next build output |
The Philosophy
Performance isn't a feature you add later. It's a design constraint you embrace from day one. Every component, every API call, every font choice is a performance decision.
The fastest code is code that never runs. The best optimization is the feature you decided not to build.