Platform Optimization

Improve Core Web Vitals in Next.js with better rendering, images and hydration

Choose what the server sends, what the browser executes and what your measurement actually represents.

A component tree distinguishes server-rendered nodes from interactive client nodes.

Improve Core Web Vitals in Next.js by delivering the important content early, limiting browser JavaScript and reserving space for everything that arrives later. Next.js supplies useful building blocks, but the rendering strategy and component boundaries still determine how much work a visitor's device must do. Diagnose the slow metric before changing the framework configuration.

The examples target Next.js 16 with the App Router, checked against documentation displaying v16.4.2. They illustrate implementation patterns, not benchmark results. Pages Router applications and React applications without Next.js need different integration points, although the browser problems remain the same.

Start with the route and the metric

Pick a real route with a measurable problem, rather than the easiest page to optimize. Record the device class, navigation type, deployment and user state. A cached public article, a signed-in dashboard and a product page with a personalized recommendation feed do not share the same critical path.

LCP describes when the largest eligible visible content is painted. A delayed server response, a late image request or main-thread work can all postpone it. INP concerns interaction responsiveness across the visit. Hydration, a filter handler and the rendering work after a state update may contribute at different moments. CLS concerns unexpected movement, including movement after the first screen appears.

Use field measurements to identify the affected population, then a browser performance recording to explain the delay. A standard Lighthouse navigation audit cannot establish production INP. Conversely, a single quick click on a developer laptop cannot establish that the application's real users have good responsiveness. The INP diagnosis guide explains how to separate input, processing and presentation delays.

Keep a short baseline record before editing: route, observed LCP element, largest relevant tasks, layout-shift sources and scripts loaded by that route. This turns a broad optimization effort into a testable claim. If the hero image starts late, the first change should address discovery or prioritization, not unrelated package cleanup.

Match rendering to the content's actual requirements

Static generation, request-time rendering, streaming and client rendering describe different parts of delivery. Server Components describe where component code executes. A Server Component can participate in a statically rendered route or in request-time work; calling something a Server Component does not establish its caching policy.

The practical trade-offs are these:

Approach Potential advantage Risk to verify
Static or cached HTML Early content without repeating origin work Stale content if invalidation is wrong
Request-time server rendering Fresh or personalized HTML Slow data dependencies delaying the response
Streaming Useful sections can arrive before slower sections The LCP content still waiting behind a boundary
Client-rendered content Interactive views can fetch on demand Script and data waterfalls before useful content

Do not choose request-time work simply because the application uses React. Public documentation that changes only on publication can often be prepared ahead of requests. Personalized content may require dynamic work, but the surrounding navigation and descriptive text need not wait for every personalized dependency.

Streaming is especially easy to misread. Sending a skeleton earlier can improve the first impression without moving the final LCP element earlier. Inspect the actual content arrival, not only the initial document byte. Give a fallback and its replacement comparable geometry so that progressive delivery does not produce a jump.

Cache behavior depends on the Next.js version and enabled features. Inspect the generated route and deployment behavior instead of copying assumptions from an older release. A warm development session is not evidence that a cold production request will follow the same path. Test both direct document loads and client navigations because they can request different resources.

Keep client boundaries small enough to explain

In the App Router, pages and layouts are Server Components by default. Use Client Components for state, event handlers and browser APIs. Place 'use client' at the smallest useful interactive boundary, such as a filter or an expandable control, instead of marking a whole content-heavy layout as client code for one button.

The boundary affects the imported module graph. An expensive library imported by that client module can enter the browser bundle even if most of the page appears static. Review what the interactive component imports, and pass the data it actually needs. Moving a large data object through serialized props can create transfer and processing costs even when its transformation ran on the server.

A Client Component is not synonymous with client-only HTML. Next.js can prerender its initial output and hydrate it in the browser. This distinction matters when investigating a page that looks complete before its controls respond. The HTML arrived, but the required JavaScript and event handling may still be catching up.

Use a production bundle analysis to identify why a dependency reaches a route. Then inspect a performance trace to see whether its execution matters. Compressed file size and CPU time answer different questions. The JavaScript execution guide provides the next investigation when download improvements leave long tasks unchanged.

Give the real LCP image an explicit loading plan

Use next/image to supply dimensions and responsive candidates, then describe the image's displayed width accurately. Intrinsic width and height reserve its aspect ratio; CSS controls its rendered size. With fill, the parent needs positioning and a defined size or aspect ratio. Missing geometry can turn an image improvement into a layout problem.

This app/page.jsx example assumes that public/images/storefront.webp exists and is the initial viewport's important image:

jsx
// app/page.jsx
import Image from 'next/image';

export default function Page() {
  return (
    <main style={{ maxWidth: 960, margin: '0 auto' }}>
      <h1>A guide to faster storefronts</h1>
      <Image
        src="/images/storefront.webp"
        alt="A storefront with a wide display window"
        width={1600}
        height={900}
        sizes="(max-width: 960px) 100vw, 960px"
        loading="eager"
        fetchPriority="high"
        style={{ width: '100%', height: 'auto' }}
      />
    </main>
  );
}

The sizes value matches this example's full-width container capped at 960 CSS pixels. Change it when the layout changes. A card in a multi-column grid should not claim the whole viewport simply because copying the string is convenient. Check the selected image candidate in the network panel at representative viewport widths and device pixel ratios.

In Next.js 16, priority is deprecated in favor of preload. That does not mean every old priority use should become an unconditional preload. The Image component reference generally favors eager loading or a high fetch priority and advises against combining preload with loading or fetchPriority.

The example therefore uses eager loading plus a priority hint, without preload. A preload is a separate option when a known critical image needs earlier discovery. Do not preload several competing mobile and desktop hero candidates without understanding which resource the browser will actually use. Keep noncritical offscreen images lazy and verify the real LCP element after the change.

Also check the image delivery service. Remote sources need an appropriate remotePatterns configuration, and the default optimizer requires a compatible runtime. A static export needs an explicit image delivery strategy, such as a custom loader or preoptimized files. The image optimization guide covers the format, dimensions and quality decisions that remain outside the component.

Treat fonts and layout stability as one decision

next/font can serve font files from the application's deployment. With next/font/google, the visitor's browser does not need to request the font from Google. With next/font/local, files come from the project. Neither choice removes the need to consider glyph coverage, weights, fallback metrics and the amount of data sent.

This root layout assumes a licensed Latin font at app/fonts/body-latin.woff2 and the measurement component shown later:

jsx
// app/layout.jsx
import localFont from 'next/font/local';
import { WebVitals } from './web-vitals';

const bodyFont = localFont({
  src: './fonts/body-latin.woff2',
  display: 'swap',
});

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={bodyFont.className}>
      <body>
        <WebVitals />
        {children}
      </body>
    </html>
  );
}

The example is an English layout, not a universal multilingual font recipe. Korean and Hebrew pages need appropriate glyph coverage, language settings and direction where relevant. A Latin-only file cannot render every language. Avoid downloading several full families on every route when the visitor uses only one language.

display: 'swap' allows fallback text while the font arrives. Check whether the replacement changes wrapping, navigation width or card height. Framework fallback adjustments help, but they do not prove that the entire page has zero CLS. Ads, embeds, asynchronously inserted banners and streamed sections still need reserved space.

Compare the slow-network page before and after the font loads. Look at the heading that determines LCP, not only a sample paragraph. If text reflows, adjust the fallback and layout or reduce the font dependency. The font optimization guide explains that investigation in more detail.

Schedule scripts without confusing delay with removal

next/script controls when a script is loaded, but it does not make its execution free. Choose the scope as carefully as the strategy: a widget needed on one route should not automatically run from the root layout on every route.

The supported strategies have different purposes:

Strategy Intended timing Practical caution
beforeInteractive Early critical scripts in the root layout Reserve for code the whole application truly needs early
afterInteractive After some hydration; the default Can still compete with early interactions
lazyOnload During browser idle time after loading Later execution can still interrupt a visitor
worker Experimental worker-based loading Not supported with the App Router in the checked documentation

Loading later can protect the initial critical path while worsening a later interaction. Reproduce the action that happens when the widget initializes. Where appropriate, load a nonessential feature only when the visitor requests it, and provide visible feedback during that wait. Consent requirements remain separate from loading strategy: a delayed analytics script is not automatically a consent-aware integration.

Investigate hydration and the work after it

Hydration attaches React behavior to prerendered output. Keep the server output and the initial client output consistent. Browser-only state, nondeterministic values or different content branches can produce mismatches. Hiding every mismatch warning does not repair the underlying delivery model.

A large hydration task can delay input, but not every INP issue is hydration. Later state updates can rerender large subtrees, trigger expensive layout or run synchronous calculations. Record the slow action after the page settles as well as during initial loading. Inspect the interval from the user's input through the next paint.

React scheduling tools can prioritize some updates, but they do not move arbitrary synchronous JavaScript off the main thread. First remove unnecessary computations, narrow the updated subtree and avoid repeated layout work. For substantial CPU work, consider an appropriate worker boundary. Every change still needs a correctness check: a fast filter that briefly displays the wrong result is not a successful optimization.

Measure a production build with a stable callback

The App Router uses useReportWebVitals from next/web-vitals inside a small Client Component. Keeping the callback reference stable avoids duplicate reporting caused by replacing the function on rerender. This example writes observations to the browser console and does not transmit user data:

jsx
// app/web-vitals.jsx
'use client';

import { useReportWebVitals } from 'next/web-vitals';

function reportMetric({ id, name, value, rating }) {
  console.info('web-vital', { id, name, value, rating });
}

export function WebVitals() {
  useReportWebVitals(reportMetric);
  return null;
}

The layout above mounts the component once. Observe a production build, interact with the page and change the page's visibility when investigating metrics that report later in the lifecycle. Do not expect every metric to appear immediately after load. Visits without qualifying interactions can have no INP value; missing values must not become zeros.

For real-user collection, replace logging with an explicitly designed telemetry pipeline. Define sampling, consent where applicable, data minimization and metric deduplication. Preserve the metric identifier and name, and handle updates according to the library version. LCP and INP use time units; CLS is unitless. Do not average unlike units or count several updates as several independent visits.

Route changes need extra care. Measurement behavior for soft navigations depends on browser and library support. Do not assume a client-side URL change resets every standard Core Web Vital, or attach a late metric to whichever URL happens to be visible when it arrives. Document the navigation semantics of the version actually shipped.

Ship one change with a clear acceptance test

Choose the bottleneck, make the smallest relevant change and repeat the same journey under comparable conditions. For an image fix, verify request timing, selected candidate and LCP. For a client-boundary change, verify bundle inclusion, interaction timing and functionality. For a layout fix, inspect both loading and later content updates.

Use the Core Web Vitals checker to interpret measured values, then monitor the affected production routes and device groups. Keep the platform optimization guides close to the release checklist. The next useful step is a recorded improvement tied to one cause, followed by a regression check on the rest of the experience.

Related guides