Free quote
Back to Blog
Article
September 30, 202618 min read

Next.js Core Web Vitals Optimization: How to Score 90+ on Lighthouse in 2026

KB

Konrad Bachowski

Tech lead, HeyNeuron

Next.js Core Web Vitals Optimization: How to Score 90+ on Lighthouse in 2026

Why Core Web Vitals Still Matter in 2026

Only 55.7% of web origins globally pass all three Core Web Vitals as of January 2026, according to Chrome User Experience Report (CrUX) data — meaning nearly half the web is losing ranking positions and conversions right now. For Next.js teams, that's both a problem and an opportunity.

The business case is clear: every 100ms of additional load time cuts conversion rates by roughly 1%, according to digitalapplied.com's 2026 page speed statistics. Vodafone improved its LCP score by 31% and saw an 8% increase in sales. Pinterest cut perceived wait times by 40% and gained 15% more SEO traffic. These aren't edge cases — they're the expected outcome when you take performance seriously.

Next.js already gives you a structural advantage. Sites built on Next.js pass all three Core Web Vitals at a 58% rate, compared to 38% for WordPress, according to platform benchmark data from digitalapplied.com. But "better than WordPress" isn't the same as "optimized." This guide covers what you actually need to do to push your Next.js app past 90 on Lighthouse — and why it pays off.


The 2026 Core Web Vitals Landscape

Three metrics define Google's Page Experience signal in 2026:

Metric What It Measures Good Threshold 2026 Pass Rate
LCP (Largest Contentful Paint) Loading speed ≤ 2.5s 68.3% of origins
INP (Interaction to Next Paint) Responsiveness ≤ 200ms 57% of origins
CLS (Cumulative Layout Shift) Visual stability ≤ 0.1 80.9% of origins

INP is the hardest to fix — 43% of sites still fail it as of early 2026, according to CrUX data aggregated by thestacc.com. That's because INP measures the responsiveness of every interaction on the page, not just the initial load, which makes JavaScript-heavy React apps particularly vulnerable.

The mobile/desktop split matters too. Only 49.7% of sites pass on mobile versus 57.1% on desktop (HTTP Archive, 2025). Since Google uses mobile-first indexing, desktop Lighthouse scores can be misleading — always measure field data from CrUX alongside synthetic Lighthouse runs.

The SEO impact is real: 91% of position-1 search results pass all three Core Web Vitals, versus only 47% of results on page 2, per digitalapplied.com's benchmarks. Sites in the top 10% for CWV performance hold a 3.2-position ranking advantage over the bottom 50%.


LCP: Fix the Largest Contentful Paint

Images cause 71% of LCP elements across the web (CrUX data). In Next.js, the most common LCP failures are:

  1. Missing priority prop on the hero image
  2. Using raw <img> tags instead of next/image
  3. External font requests blocking render
  4. Slow server response times (TTFB above 800ms)

next/image: The Non-Negotiable Starting Point

The next/image component handles WebP/AVIF conversion, lazy loading, and responsive srcsets automatically. The one thing it won't do for you is mark your LCP image as high priority:

// WRONG — lazy by default, defers LCP image
<Image src="/hero.jpg" alt="Hero" width={1200} height={600} />

// RIGHT — tells browser to preload this immediately
<Image
  src="/hero.jpg"
  alt="Hero"
  width={1200}
  height={600}
  priority
  sizes="(max-width: 768px) 100vw, 1200px"
/>

Use priority on exactly one image per page — the above-the-fold element that will become the LCP. Using it on multiple images defeats the purpose and can slow the page down.

Preconnect to External Resources

If your LCP image comes from a CDN or your fonts come from Google Fonts, browser needs time to establish the connection. Add preconnect hints in <head>:

// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html>
      <head>
        <link rel="preconnect" href="https://fonts.googleapis.com" />
        <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
      </head>
      <body>{children}</body>
    </html>
  )
}

TTFB: The Upstream Problem

If your Lighthouse score shows good images but still slow LCP, the culprit is usually Time to First Byte. Pages that take longer than 600ms TTFB cannot pass LCP even with perfect image optimization. Options in order of impact:

  • Static generation (SSG/ISR) — zero server compute on each request
  • Edge functions — compute runs at CDN edge instead of origin data center
  • Cache-Control headers — instruct the CDN to serve pre-rendered HTML
// next.config.js — configure ISR for marketing pages
// In app directory, use: export const revalidate = 3600
// In pages directory:
export async function getStaticProps() {
  return {
    props: { data },
    revalidate: 3600, // regenerate at most once per hour
  }
}

INP: The Hardest Core Web Vital to Fix

INP replaced FID in March 2024 and is significantly harder to optimize because it measures the worst-case interaction latency across the entire page session, not just the first input. The threshold is 200ms from interaction to next paint — and 43% of sites globally still fail it.

Server Components First

The biggest single change you can make in Next.js is not defaulting to "use client". Every client component ships JavaScript to the browser, executes during hydration, and contributes to main thread contention — all of which delays INP.

// BAD — entire component tree is client-side JS
"use client"
export default function ProductPage({ product }) {
  return <div>{product.name}</div>  // This doesn't need to be interactive
}

// GOOD — only the interactive part is a client component
// ProductPage (Server Component) — renders product data on server
// AddToCartButton (Client Component) — only this ships JS

A practical audit: grep your codebase for "use client" and ask whether each one actually needs browser APIs or event handlers. Components that only read data and render HTML belong on the server.

Break Long Tasks

Any JavaScript task running longer than 50ms blocks user interaction and fails INP. In React, this usually happens during:

  • Complex state updates re-rendering large component trees
  • Heavy useEffect computations on mount
  • Synchronous data transformations in render

The fix is scheduler.yield() (supported in Chrome 115+) or chunking work across multiple frames:

// Yield to the main thread between expensive operations
async function processLargeDataset(items) {
  const results = []
  for (let i = 0; i < items.length; i++) {
    results.push(processItem(items[i]))
    // Yield every 50 items to stay under 50ms task threshold
    if (i % 50 === 0) {
      await scheduler.yield()
    }
  }
  return results
}

Defer Non-Critical Third-Party Scripts

Analytics, chat widgets, and advertising scripts are responsible for a significant share of INP failures on real-world pages. Load them after the page is interactive:

import Script from 'next/script'

// afterInteractive = loads after page hydrates, doesn't block INP
// lazyOnload = loads during browser idle time
<Script
  src="https://analytics.example.com/script.js"
  strategy="lazyOnload"
/>

Never use strategy="beforeInteractive" for third-party scripts unless they're genuinely required before hydration (rare).


CLS: Eliminate Layout Shift

Cumulative Layout Shift is the most fixable of the three metrics — 80.9% of origins already pass it. The failures are almost always from a small set of causes.

Always Specify Image Dimensions

// Browser reserves space before the image loads — no CLS
<Image
  src="/product.jpg"
  alt="Product"
  width={400}
  height={300}
/>

// Raw img without dimensions — browser doesn't know space to reserve
<img src="/product.jpg" alt="Product" />  // CLS likely

Adding explicit width and height to images reduces CLS by an average of 0.05 points per element, according to CrUX analysis. On pages with multiple images, this alone can move you from "needs improvement" to "good."

Font Optimization

Custom fonts loading late push text downward, creating layout shift. Next.js next/font handles this automatically with zero CLS:

// app/layout.tsx
import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',  // text stays visible during font load
  preload: true,
})

This inlines critical font CSS, prevents FOUT (Flash of Unstyled Text), and eliminates the network request for the font CSS file — three wins with one config.

Reserve Space for Dynamic Content

Ads, embeds, and dynamically loaded components inserted above existing content cause CLS. Always pre-reserve space with a fixed-height container:

/* Reserve space for an ad slot before it loads */
.ad-container {
  min-height: 250px;
  width: 300px;
}

Bundle Size and Code Splitting

A large JavaScript bundle is the root cause of both slow LCP (delays hydration) and poor INP (blocks main thread). Next.js handles route-level code splitting automatically, but within routes, you need to be intentional.

Analyze Your Bundle

# Install bundle analyzer
npm install @next/bundle-analyzer

# next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
})

module.exports = withBundleAnalyzer({ /* your config */ })

# Run analysis
ANALYZE=true npm run build

Look for: duplicate libraries (multiple versions of lodash), large components loaded on every page, and moment.js (replace with date-fns or Intl API).

Dynamic Imports for Below-the-Fold Content

import dynamic from 'next/dynamic'

// Modal, chart, or heavy component — loaded only when needed
const HeavyChart = dynamic(() => import('./HeavyChart'), {
  loading: () => <div className="h-64 animate-pulse bg-gray-100" />,
  ssr: false,  // only for browser-only libraries
})

The skeleton loader prevents CLS while the component loads. Use this pattern for anything below the fold or behind a user interaction.


Caching Strategy: Where 70% of the Wins Live

Poor caching is the most common reason a Next.js app scores 60 despite correct implementation. The four-layer cache stack:

Layer What It Caches Config
Browser Static assets, fonts Cache-Control: max-age=31536000
CDN (Vercel Edge / Cloudflare) HTML pages, API responses s-maxage=3600, stale-while-revalidate
Next.js (ISR/full-route cache) Rendered HTML segments revalidate = N
Fetch/React cache Data fetched in Server Components fetch(url, { next: { revalidate: 3600 } })

The most impactful fix is usually at the CDN layer — ensuring pages rendered at edge aren't bypassing the cache on every request. Check your network tab: if the X-Vercel-Cache header shows MISS on every reload, your pages aren't being cached.


Automating Performance Budgets with Lighthouse CI

This is the section most guides skip: how to prevent regressions. A single PR with an unoptimized image or a heavy library can undo weeks of optimization work. Lighthouse CI runs Lighthouse on every PR and can block merges if scores drop below your thresholds.

Setup in GitHub Actions

# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [pull_request]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci && npm run build
      - name: Run Lighthouse CI
        run: |
          npm install -g @lhci/cli
          lhci autorun
        env:
          LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}

Define Your Performance Budget

// lighthouserc.json
{
  "ci": {
    "collect": {
      "url": ["http://localhost:3000/", "http://localhost:3000/products"],
      "numberOfRuns": 3
    },
    "assert": {
      "assertions": {
        "categories:performance": ["error", { "minScore": 0.9 }],
        "first-contentful-paint": ["error", { "maxNumericValue": 2000 }],
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
        "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
        "total-blocking-time": ["error", { "maxNumericValue": 300 }]
      }
    }
  }
}

With this in place, any PR that drops performance below 90 gets a failing check. Your team can still merge after review, but the failure surfaces the issue before it reaches production. This single change eliminates most performance regressions before users see them.


Cost Breakdown: DIY vs. Hiring Help

Core Web Vitals optimization cost depends heavily on how bad the starting point is and how much engineering time you have.

Route Initial Cost Ongoing Best For
DIY (in-house dev) 20-60 hrs developer time 4-8 hrs/quarter (monitoring + fixes) Teams with experienced Next.js devs
Freelance performance specialist $1,500 – $5,000 audit + fixes $500 – $1,500/quarter Startups needing a one-time boost
Agency (full optimization sprint) $5,000 – $15,000 $1,000 – $3,000/quarter retainer Complex apps, eCommerce, enterprise
Performance SaaS (SpeedCurve, Calibre) $200 – $800/month Ongoing subscription Monitoring only — still need devs to fix

ROI math for a $100K/month revenue eCommerce site: A 200ms improvement in LCP typically yields a 2% conversion lift. At $100K/month, that's $2,000/month extra revenue — paying back a $5,000 freelance engagement in under 3 months.

For Polish-market teams, working with a Next.js development agency in Poland typically runs 40–60% lower than Western European rates for the same quality, making the ROI calculus even more favorable.


10-Item Core Web Vitals Optimization Checklist

Use this before every major release:

  • [ ] LCP image has priority prop — confirm in DevTools Network tab that LCP image loads in first batch of requests
  • [ ] All images have explicit width and height — prevents CLS from image layout shift
  • [ ] next/font used for all custom fonts — eliminates font-related CLS and FOUT
  • [ ] No "use client" on data-only components — server components reduce JS bundle shipped to browser
  • [ ] Dynamic imports for below-fold heavy components — charts, modals, rich editors
  • [ ] Third-party scripts use lazyOnload strategy — analytics/chat/ads must not block INP
  • [ ] ISR or static generation on marketing pages — TTFB under 200ms
  • [ ] CDN caching headers set correctly — verify X-Cache: HIT in response headers
  • [ ] Lighthouse CI gate configured in CI/CD — blocks PRs that drop performance below 90
  • [ ] Field data (CrUX / PageSpeed Insights) checked monthly — synthetic Lighthouse scores don't capture real user behavior

When NOT to Optimize

Four scenarios where spending heavily on Core Web Vitals is the wrong priority:

1. Authenticated-only apps with no SEO value. SaaS dashboards, admin panels, and internal tools don't rank in Google. Users are already on your platform — performance still matters for UX, but CWV won't affect your acquisition metrics. Prioritize feature development instead.

2. Your Lighthouse score is already above 90. Chasing 100/100 past 90 delivers diminishing returns. The ranking and conversion benefits accrue between 0–90; the last 10 points rarely move business metrics. Document your current score, set up monitoring, and stop optimizing.

3. Your bottleneck is infrastructure, not frontend. If TTFB is 2 seconds because your database queries are slow, no amount of frontend optimization will pass LCP. Fix the API response time first. Lighthouse will tell you if TTFB is the problem under "Server response times."

4. You have under 1,000 monthly sessions. Google's field data algorithm requires minimum traffic thresholds to calculate CrUX scores. Low-traffic sites often don't have enough data to appear in Page Experience reports at all. Focus on content and distribution first.


Frequently Asked Questions

How long does it take to improve Core Web Vitals in Next.js?

Quick wins (priority image, next/font) take 2–4 hours and often move LCP from failing to passing. INP improvements require a more thorough audit — expect 2–5 days for a complex app. Full optimization to 90+ Lighthouse typically takes 1–3 weeks depending on app complexity.

Does improving Core Web Vitals directly boost Google rankings?

Core Web Vitals are a confirmed Google ranking signal, but a lightweight one. Pages with identical content quality will rank higher if they pass CWVs — but great content on a slow page will still outrank poor content on a fast page. The bigger direct impact is on conversion rates and bounce rates.

What's the difference between Lighthouse scores and CrUX data?

Lighthouse is synthetic — it runs in a controlled environment with a simulated device and network. CrUX is field data — real users on real devices and networks. A 95 Lighthouse score doesn't guarantee passing CWVs if your real users are on slow mobile networks. Always check both.

Should I use next/image for all images on the page?

Yes for content images. For SVG icons and tiny decorative images under 1KB, you can use native <img> without meaningful performance impact. The optimization matters most for images over 20KB or any image that could be the LCP element.

What causes INP failures in Next.js apps specifically?

The most common causes are: (1) excessive client components shipping too much JavaScript, (2) large useEffect handlers running on every render, (3) undeferred third-party scripts (analytics, chat widgets), and (4) synchronous state updates in event handlers that trigger expensive re-renders.

How do I measure INP in development?

INP is a field metric — it requires real user interactions in a real browser. In development, use Chrome DevTools' Performance panel with a slow CPU throttle and interact with your page. The "Long Tasks" visualization shows tasks over 50ms. For production, Google Search Console's Core Web Vitals report and PageSpeed Insights show real user INP from CrUX.

Is a 100/100 Lighthouse score achievable in Next.js?

Yes, but only on simple pages. Pages with analytics, third-party embeds, A/B testing tools, or heavy UI components will struggle to hit 100. Aim for 90+ as your practical target — the marginal gains past 90 don't justify the engineering trade-offs.

Does self-hosting Next.js hurt Core Web Vitals vs Vercel?

Not inherently. Self-hosted deployments with a CDN in front (Cloudflare, Fastly) match Vercel's performance. The main risk is misconfigured cache headers — if your CDN isn't caching HTML, every request hits your origin server. See our guide on how to self-host Next.js on VPS with Docker and Nginx for the correct cache configuration.


Getting to 90+

The path to a 90+ Lighthouse score in Next.js follows a consistent sequence: fix images and fonts first (typically moves score from 60 to 80), then eliminate unnecessary client components and defer third-party scripts (80 to 90), then tune caching and add Lighthouse CI (90 to 95+). Each layer builds on the previous.

The monitoring step is where most teams fail. Without Lighthouse CI in your pipeline, every release risks regression. A score that takes three weeks to achieve can drop back to 70 in a single PR.

If your team needs support implementing these optimizations or building the monitoring infrastructure, our Next.js and web application team works with both new builds and existing codebases. Get in touch with your current Lighthouse scores and we can scope what's needed.


Related articles: - Next.js App Router Best Practices 2026 - How to Self-Host Next.js on VPS with Docker and Nginx - How to Build a PWA with Next.js 2026 - How to Choose the Right Tech Stack for Your Web App - React Native Performance Optimization Guide 2026 - How Much Does It Cost to Build a SaaS Platform

Stay up to date with AI and automation

Subscribe to our newsletter to receive specific tips and tools once a week. Join over 2,000 subscribers.

Your data is safe. Zero spam.