WellAlly Logo
WellAlly康心伴
Development

Case Study: Optimizing Core Web Vitals in a Next.js Content Blog

A step-by-step walkthrough of identifying and fixing performance bottlenecks (LCP, INP, CLS) on a Next.js site, covering image optimization, lazy loading with dynamic imports, and font handling.

W
2025-12-18
8 min read

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/image component.
  • 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:

code
// src/components/BlogPostLayout.js
const BlogPostLayout = ({ post }) => (
  <div>
    <h1>{post.title}</h1>
    <img src={post.heroImage} alt={post.title} />
    {/* ... rest of the post */}
  </div>
);
Code collapsed

After:

code
// 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>
);
Code collapsed

After:

code
// 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>
);
Code collapsed

How it works

  1. Code Splitting: next/dynamic creates separate JavaScript bundles (chunks) for RecipeCalculator and CommentsSection. These chunks are not downloaded during the initial page load.
  2. Lazy Loading: The JavaScript for these components is only fetched and executed when they are about to be rendered.
  3. ssr: false: For the CommentsSection, which might rely on browser-only APIs like window, we can disable server-side rendering to prevent errors.
  4. Loading State: The loading option 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:

  1. Unsized Images: As we saw, images without defined dimensions cause the layout to reflow once they load.
  2. 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.

code
// 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;
Code collapsed

How it works

The next/font module is incredibly powerful for a few reasons:

  1. 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.
  2. Zero CLS: next/font generates 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.
  3. 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/font for 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

#

Article Tags

nextjsperformancewebdevfrontend
W

WellAlly's core development team, comprised of healthcare professionals, software engineers, and UX designers committed to revolutionizing digital health management.

Expertise

Healthcare TechnologySoftware DevelopmentUser ExperienceAI & Machine Learning

Found this article helpful?

Try KangXinBan and start your health management journey

© 2024 康心伴 WellAlly · Professional Health Management