arrow_backBack to portfolio
January 28, 2026·10 min read

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.

Next.jsPerformanceReact

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:

javascript
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:

javascript
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.

javascript
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:

MetricTargetTool
LCP< 1.5sVercel Analytics
FID< 50msWeb Vitals
Bundle Size< 150kB first loadnext 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.