Pulseby Kraavon
Framework guide

Next.js performance

Next.js gives you the tools to hit good Core Web Vitals and equally good ways to lose them. The framework specific failures are consistent enough to be worth naming: a client boundary drawn too high, a hero image left to lazy load, and third party scripts dropped into the layout where they block the main thread during hydration.

What usually goes wrong

The pattern repeats across codebases. A page is marked use client because one button needs an onClick, which pulls the entire subtree into the browser bundle. The hero image comes from a CMS through a plain img tag, so it downloads at full resolution. Analytics and a chat widget are added to the root layout, so they execute during hydration on the slowest device the site will ever see. None of this is exotic, and all of it is measurable.

How to fix it

Ordered by return on effort. The quick wins are framework features you already have.

Use next/image with priority on the hero

Quick win

next/image generates responsive variants, serves AVIF and WebP, and reserves space from the width and height you pass, which addresses oversized images and layout shift in one component. Add priority to the LCP image: it sets fetchpriority high and disables lazy loading, both of which matter for the element that defines your LCP. Leaving the hero to lazy load is one of the most common self inflicted regressions in a Next.js codebase.

Load third party scripts with next/script

Quick win

The strategy prop decides when a script runs. afterInteractive for anything that can wait until hydration is done, lazyOnload for chat widgets and analytics that nobody needs in the first seconds. Dropping a raw script tag into the layout puts it on the critical path, where it competes with your own code for the main thread.

Self host fonts with next/font

Quick win

next/font downloads the font at build time, serves it from your own origin and generates a fallback with matching metrics. That removes a third party connection, removes the render blocking request, and removes the reflow when the webfont swaps in. It is close to a free CLS improvement.

Keep components on the server

Moderate

Every component marked use client ships its code to the browser and joins the hydration pass. The default in the App Router is a Server Component, and the discipline that matters is pushing use client down to the leaves that genuinely need state or an event handler, rather than marking a whole page. Bundle size and Total Blocking Time both follow from this more than from any other single decision.

Dynamic import what is not needed for the first paint

Moderate

Charts, editors, modals and video players rarely need to be in the initial bundle. next/dynamic with ssr false keeps them out of the server render and out of the critical path. This site loads its own animation that way, which is why a 440 kB file costs nothing until it is nearly on screen.

Choose the rendering strategy per route

Moderate

A route that does not vary per visitor should be static. generateStaticParams turns a dynamic segment into pre-rendered HTML at build time, which makes Time to First Byte a file read. Reach for a dynamic render only where the content genuinely depends on the request.

Stream with Suspense rather than blocking on data

Involved

A slow database call in a Server Component holds the whole response. Wrapping that part in Suspense lets the shell stream immediately and the slow section arrive later, which improves First Contentful Paint without making the data any faster.

Audit the bundle for what is actually shipped

Involved

A single large date or icon library imported for one helper can dominate the client bundle. Check what crosses the server boundary, prefer per function imports, and confirm that a heavy dependency is not being pulled in by a Server Component that then gets marked use client.

Verifying the change

Measure a production build rather than the dev server. Development mode ships unminified code, disables several optimisations and recompiles on demand, so its numbers are meaningless. Run next build and next start, then audit that. Pulse reports the same metrics against a deployed URL, with the resource breakdown showing exactly how much JavaScript reached the browser.

Common questions

Does the App Router make Core Web Vitals better automatically?

Partly. Server Components reduce how much JavaScript reaches the browser, which helps INP and Total Blocking Time, and streaming helps First Contentful Paint. None of that saves you from an oversized hero image, a blocking third party script or a client boundary drawn too high in the tree.

Why is my Next.js LCP slow when Lighthouse says my JavaScript is fine?

The usual cause is the image rather than the code. If the hero is lazy loaded, set by CSS, or rendered by a client component after hydration, the browser cannot discover it early. next/image with priority resolves most cases.

Should I use next/image for every image?

For content images, yes. It handles responsive variants, modern formats and dimension reservation. Small decorative SVGs and icons are usually better inlined, since the optimisation pipeline adds nothing to a file that is already tiny.

How do I find which components are shipping to the client?

Search for use client and check how far up the tree each one sits. A single directive near the root pulls everything beneath it into the client bundle, which is the most common reason a Next.js app ships far more JavaScript than its authors expect.

Related

Audit your Next.js site

Point Pulse at a deployed page and see how much JavaScript reaches the browser, which images are oversized, and what to fix first.