Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
---
title: 'Next.js 15 & React 19 Performance Optimization: A Practical Guide (With Case Study)'
description: 'Learn how to optimize your Next.js 15 and React 19 applications, featuring real-world performance techniques like edge caching, SVG sprites, and React 19 ref updates.'
date: '2026-06-11'
tags: ['Web Performance', 'Next.js 15', 'React 19', 'Core Web Vitals', '2026']
published: true
image: './images/post-image.png'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check if the referenced image file exists

fd -t f "post-image.png" content/blog-post-2026-06-11/

Repository: deepu0/frontend-junction

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Find the image file anywhere in the repo and also check the expected directory.
set -euo pipefail

echo "== Expected path check =="
if [ -f "content/blog-post-2026-06-11/images/post-image.png" ]; then
  echo "FOUND: content/blog-post-2026-06-11/images/post-image.png"
else
  echo "MISSING: content/blog-post-2026-06-11/images/post-image.png"
fi

echo
echo "== Search for post-image.png anywhere =="
fd -t f "post-image.png" .

Repository: deepu0/frontend-junction

Length of output: 196


Fix missing referenced image asset
The MDX frontmatter sets image: './images/post-image.png', but content/blog-post-2026-06-11/images/post-image.png does not exist anywhere in the repo, so the post will have a broken/missing image. Add the asset or update the frontmatter path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@content/blog-post-2026-06-11/web-performance-in-2026-a-practical-guide.mdx`
at line 7, The MDX frontmatter 'image' field is pointing to
'./images/post-image.png' which doesn't exist; either add the missing image
asset at that relative location under the post's images folder or update the
frontmatter 'image' value to point to an existing image (or remove the key).
Locate the frontmatter in web-performance-in-2026-a-practical-guide.mdx, verify
available image filenames in the post's images directory, then either
copy/rename the correct PNG to match './images/post-image.png' or change the
'image' string to the correct existing filename/path.

---

# Next.js 15 & React 19 Performance Optimization: A Practical Guide

Performance is the ultimate feature. In modern web development, a 100ms delay in page load can cost up to 1% in conversions. With Google's search algorithm actively penalizing slow sites, optimizing your **Next.js 15** and **React 19** applications is critical for organic traffic and user retention.

In this guide, we walk through a real-world performance case study, demonstrating how we optimized a production platform, dropping the **Time to First Byte (TTFB)** from 800ms to under 50ms and shrinking HTML payload sizes by over **60%**.

---

## 1. Case Study: The Sluggish Job Board

We audited a modern frontend job board, **OnlyFrontendJobs.com**, running Next.js 15 and React 19. While the site was beautifully designed, it suffered from three critical performance bottlenecks:
1. **Slow Job Details Page (TTFB > 800ms)**: Every single job posting page was rendered dynamically on demand, hitting the database on every request and bypassing the CDN.
2. **Massive HTML Payload (410 KB on Jobs Feed)**: The jobs listing page rendered 20 jobs but generated a huge HTML document, leading to severe DOM bloat.
3. **Main-Thread Blocking on Search Input (INP > 350ms)**: Typing in the search filter box caused typing lag and UI stuttering on mobile devices.

Here is exactly how we solved each of these issues.

---

## 2. Solution 1: CDN Edge Caching & Static Pre-rendering

### The Problem
The job details page (`/jobs/[slug]`) had no caching configured. Because it was a dynamic route, Next.js rendered the page on every single request, hitting the database, waiting for the query, and then returning the HTML.

### The Fix
We implemented **Incremental Static Regeneration (ISR)** and pre-rendered the most popular job pages at build time.

First, we added `export const revalidate = 1800` to the page file. This instructs the CDN (Vercel/Cloudflare) to cache the generated HTML at the edge for 30 minutes.

Second, we added `generateStaticParams` to statically pre-generate the 50 most recently published jobs at build time:

```typescript
// src/app/jobs/[slug]/page.tsx

// ISR: Cache job details at the CDN edge for 30 minutes
export const revalidate = 1800;

import { getJob, getRecentJobSlugs } from './data'

// Pre-render the 50 most recent jobs at build time for instant loading
export async function generateStaticParams() {
const slugs = await getRecentJobSlugs()
return slugs.map((slug) => ({ slug }))
}

export default async function JobDetailPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const job = await getJob(slug)
// ... render page
}
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

### The Result
The Time to First Byte (TTFB) dropped from **800ms to under 50ms** for cached requests, making page transitions load **instantly** and completely shielding the database from traffic spikes.

---

## 3. Solution 2: Replacing Inline SVGs with Sprite Sheets

### The Problem
The jobs page rendered a list of 20 jobs. Each job card rendered 8-10 inline SVGs (location pin, calendar, salary badge, hot/featured stars, etc.) using Lucide React.

This resulted in **190 raw SVGs** being inlined directly in the HTML document, bloating the HTML payload size to **409.7 KB** and inflating the DOM node count to **1,113+ nodes**, which triggered Lighthouse warnings and degraded rendering speed.

### The Fix
We extracted all inline SVGs into a single, consolidated **SVG Sprite Sheet** (placed once at the root or loaded as an external asset) and referenced them inside components using the `<use>` tag.

**Before (Heavy Inline SVG repeated 20x):**
```tsx
export function LocationIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-map-pin">
<path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z" />
<circle cx="12" cy="10" r="3" />
</svg>
);
}
```

**After (Ultra-lightweight Sprite reference):**
```tsx
export function LocationIcon() {
return (
<svg className="w-4 h-4 text-slate-400">
<use href="/sprite.svg#map-pin" />
</svg>
);
}
```

### The Result
The HTML payload size of the jobs page shrank from **409 KB to under 150 KB** (a 63% reduction), and DOM nodes dropped below 600, making mobile scrolling incredibly smooth and responsive.

---

## 4. Solution 3: Memoizing Expensive Calculations

### The Problem
In the client-side jobs feed component, the filtering and search logic ran directly inside the render block:
```tsx
const filteredJobs = jobs.filter((job) => {
// Heavy string matching and comparison logic
});
```
Because the component rendered a massive list, typing in the search box triggered a full component re-render on every single keystroke, executing the `.filter` loop over the entire array repeatedly. This caused severe input lag and a poor **INP (Interaction to Next Paint)** score.

### The Fix
We wrapped the filtering logic in `useMemo`, ensuring that the filter loop only executes when the dependencies (`jobs`, `statusFilter`, or `searchQuery`) actually change:

```tsx
// Memoize filtered jobs to prevent main thread blocking on keystrokes
const filteredJobs = useMemo(() => {
return jobs.filter((job) => {
if (statusFilter === 'published' && job.status !== 'published') return false
if (statusFilter === 'queue' && (job.status !== 'draft' || !!job.rejected_at)) return false
if (statusFilter === 'rejected' && !job.rejected_at) return false
if (searchQuery) {
const q = searchQuery.toLowerCase()
return (
job.title.toLowerCase().includes(q) ||
(job.company || '').toLowerCase().includes(q) ||
(job.location && job.location.toLowerCase().includes(q))
)
}
return true
})
}, [jobs, statusFilter, searchQuery]);
```

### The Result
Typing in the search box no longer triggers any main-thread blocking, dropping the input response time to **under 15ms** and achieving a perfect INP score.

---

## Conclusion

By combining **CDN edge caching**, **SVG sprite sheets**, and **React memoization**, we transformed a heavy Next.js application into a lightning-fast, production-ready platform.

When building your Next.js 15 and React 19 apps, always measure your payload sizes, monitor your TTFB, and make sure you are not letting heavy calculations block the main thread. Your users (and Google's ranking algorithms) will thank you.
Loading