In the competitive world of online content, performance is everything. For our hypothetical "NutriLife" blog, a content-heavy platform built with Next.js, poor performance was becoming a major issue. Despite having great content, readers were bouncing due to slow page loads and a jumpy, unpredictable layout. This not only created a frustrating user experience but also negatively impacted our SEO rankings, as Google heavily favors sites with good Core Web Vitals.
This case study is a step-by-step walkthrough of how we identified and fixed the key performance bottlenecks—Largest Contentful Paint (LCP), Interaction to Next Paint (INP), and Cumulative Layout Shift (CLS)—transforming our sluggish blog into a lightning-fast, user-friendly experience.
What we'll cover:
- Pinpointing performance issues using Lighthouse.
- Drastically improving LCP with the
next/imagecomponent. - Boosting INP by lazy loading non-critical components.
- Eliminating CLS caused by images and custom fonts.
Prerequisites:
- Basic understanding of React and Next.js.
- Node.js and npm/yarn installed.
- Familiarity with browser developer tools (specifically the Lighthouse tab).
Understanding the Problem: The "Before" State
A quick Lighthouse audit of a typical article page on the NutriLife blog revealed some painful truths:
- Largest Contentful Paint (LCP): 4.2s (Poor) - The large, high-resolution hero image for each recipe was taking far too long to appear.
- Interaction to Next Paint (INP): 350ms (Needs Improvement) - The page felt sluggish. Clicking on interactive elements like the comments section toggle had a noticeable delay.
- Cumulative Layout Shift (CLS): 0.28 (Poor) - The most jarring issue. As images and custom fonts loaded, the text would jump around the page, causing users to lose their reading position.
Our mission was clear: tackle each of these Core Web Vitals head-on using the powerful tools Next.js provides.
Step 1: Conquering LCP with next/image
What we're doing
The primary culprit for our poor LCP score was the unoptimized hero image at the top of each blog post. We were using a standard <img> tag, which meant the browser was downloading a massive, one-size-fits-all image file.
The solution is the built-in next/image component, which automatically handles several crucial optimizations.
Implementation
We replaced the old <img> tag with the next/image component.
Before:
// src/components/BlogPostLayout.js
const BlogPostLayout = ({ post }) => (
<div>
<h1>{post.title}</h1>
<img src={post.heroImage} alt={post.title} />
{/* ... rest of the post */}
</div>
);
After:
// src/components/BlogPostLayout.js
import Image from 'next/image';
const BlogPostLayout = ({ post }) => (
<div>
<h1>{post.title}</h1>
<Image
src={post.heroImage}
alt={post.title}
width={1200}
height={600}
priority // ✨ Key addition for LCP
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
{/* ... rest of the post */}
</div>
);```
### How it works
1. **Automatic Resizing:** Next.js creates and serves smaller, optimized versions of the image in modern formats like WebP. The `sizes` prop tells the browser which image size to download based on the viewport width, preventing mobile devices from downloading massive desktop-sized images.
2. **The `priority` Prop:** This is the most critical change for LCP. Adding `priority` tells Next.js to preload this image, as it's the most important visual element "above the fold."
3. **Required `width` and `height`:** These props are essential for preventing layout shift (which we'll tackle next) because they allow the browser to reserve the correct amount of space for the image before it loads.
**Result:** Our LCP dropped from **4.2s to 1.9s** (Good). ✅
## Step 2: Boosting Responsiveness (INP) with Dynamic Imports
### What we're doing
Our INP was suffering because the browser's main thread was busy parsing and executing a large amount of JavaScript on initial load. Heavy, non-essential components like the interactive recipe calculator and the third-party comments section were blocking user interactions.
The solution is code splitting via `next/dynamic`. This allows us to defer the loading of certain components until they are actually needed.
### Implementation
We wrapped the non-critical components in a `dynamic()` import.
**Before:**
```jsx
// src/components/BlogPostLayout.js
import RecipeCalculator from './RecipeCalculator';
import CommentsSection from './CommentsSection';
const BlogPostLayout = ({ post }) => (
<div>
{/* ... post content ... */}
<RecipeCalculator />
<CommentsSection postId={post.id} />
</div>
);
After:
// src/components/BlogPostLayout.js
import dynamic from 'next/dynamic';
// Dynamically import components that are not critical for the initial view
const DynamicRecipeCalculator = dynamic(() => import('./RecipeCalculator'), {
loading: () => <p>Loading calculator...</p>,
});
const DynamicCommentsSection = dynamic(() => import('./CommentsSection'), {
ssr: false, // This component relies on browser APIs
loading: () => <p>Loading comments...</p>,
});
const BlogPostLayout = ({ post }) => (
<div>
{/* ... post content ... */}
<DynamicRecipeCalculator />
<DynamicCommentsSection postId={post.id} />
</div>
);
How it works
- Code Splitting:
next/dynamiccreates separate JavaScript bundles (chunks) forRecipeCalculatorandCommentsSection. These chunks are not downloaded during the initial page load. - Lazy Loading: The JavaScript for these components is only fetched and executed when they are about to be rendered.
ssr: false: For theCommentsSection, which might rely on browser-only APIs likewindow, we can disable server-side rendering to prevent errors.- Loading State: The
loadingoption provides a helpful placeholder to the user, improving the perceived performance.
Result: By reducing the initial JavaScript payload, the main thread became free much sooner. Our INP improved from 350ms to 150ms (Good). ✅
Step 3: Eliminating CLS with next/font and Sized Images
What we're doing
Our terrible CLS score had two sources:
- Unsized Images: As we saw, images without defined dimensions cause the layout to reflow once they load.
- Custom Font Loading: The site uses a custom Google Font. The browser would render a fallback font first, and then swap in the custom font once it downloaded, causing a jarring "flash" and shifting all the text.
Implementation
1. Image Sizing: We already solved the image portion in Step 1 by providing width and height props to the next/image component. This simple step is a huge win for CLS.
2. Font Optimization: We use the next/font module to handle font loading efficiently.
// src/pages/_app.js
import { Inter } from 'next/font/google';
// Configure the font
const inter = Inter({
subsets: ['latin'],
display: 'swap', // This is the default and best for performance
});
function MyApp({ Component, pageProps }) {
return (
<main className={inter.className}>
<Component {...pageProps} />
</main>
);
}
export default MyApp;
How it works
The next/font module is incredibly powerful for a few reasons:
- Automatic Self-Hosting: It automatically downloads the Google Font at build time and serves it from your own domain. This eliminates an extra network request to Google's servers, improving privacy and performance.
- Zero CLS:
next/fontgenerates a fallback font that closely matches the metrics of your custom font. This means when the real font swaps in, there's virtually no layout shift. - Preloading: Critical font files are automatically preloaded.
Result: With sized images and optimized font loading, our CLS score dropped from 0.28 to 0.02 (Good). ✅
Conclusion: A Healthier, Faster Blog
By systematically addressing each Core Web Vital, we transformed the NutriLife blog from a sluggish, frustrating experience into a high-performance website.
Summary of Achievements:
- LCP: Reduced from 4.2s to 1.9s by prioritizing the hero image with
next/image. - INP: Improved from 350ms to 150ms by code-splitting and lazy-loading non-critical components.
- CLS: Slashed from 0.28 to 0.02 by providing image dimensions and using
next/fontfor stable font loading.
This case study demonstrates that achieving excellent Web Vitals in a content-heavy Next.js application isn't about magic; it's about strategically using the powerful, built-in tools the framework provides. By optimizing images, splitting your code, and handling fonts correctly, you can deliver a superior user experience that both your readers and search engines will love.
Resources
- Official Documentation:
- Further Reading: