Engineering

Next.js performance in production: fix the 20% users feel

Field data instead of Lighthouse theatre. The hero image, client boundaries, third-party scripts, and caching decisions that change how a Next.js site feels.

ZX

Zynitex Engineering

Product & engineering notes

29 September 20269 min readUpdated 18 October 2026
Developer laptop displaying performance timing charts beside a stopwatch and smartphone

The short answer

Start with field data, not a Lighthouse score. Most Next.js performance wins come from the hero image, heavy client components, third-party scripts, and the order in which content streams. Set a mobile budget of LCP under 2.5s, INP under 200ms, and CLS under 0.1 at the 75th percentile, then fix the largest contributor.

What this note covers

  • Lab scores are diagnostics; field data is the truth.
  • The LCP image is usually the highest-leverage fix.
  • Every `use client` boundary ships code; keep them small and low.
  • Stream slow sections instead of blocking the shell.
  • Caching is a product decision, especially for personalized pages.
On this page(9 sections)
  1. 01Measure the right number
  2. 02The usual suspects
  3. 03Fix LCP first
  4. 04Stop shipping the whole client
  5. 05Streaming beats a spinner
  6. 06Caching is a product decision
  7. 07Third-party scripts are a tax
  8. 08A two-week sprint
  9. 09What good looks like

A Lighthouse score on a fast laptop is a lab result. Production performance is a field result from a mid-range Android phone on a crowded network, with your analytics, chat widget, and cookie banner competing for the main thread.

Next.js gives you the tools to do well. It also makes it easy to wrap the whole app in a client provider and call it architecture. The difference shows up in the field.

Measure the right number

  • Core Web Vitals at the 75th percentile, mobile, per route group.
  • LCP: largest contentful paint.
  • INP: interaction to next paint.
  • CLS: cumulative layout shift.
  • Real-user data from CrUX or your own useReportWebVitals stream.

Set a budget before you optimize: LCP under 2.5 seconds, INP under 200 milliseconds, CLS under 0.1. If you’re far off, fix the biggest contributor, not every item on a checklist. A page can score 95 in the lab and still feel slow on a train.

The usual suspects

  • A hero image that isn’t prioritised or is served at the wrong size.
  • A client component at the root that pulls the whole tree into the browser.
  • A 3D scene or animation library on the critical path.
  • Third-party scripts loading before the copy.
  • Custom fonts without a swap strategy.
  • A data-fetching waterfall that delays the first useful paint.
Field budget / mobileIllustrative
LCP2.1sgoal < 2.5s
INP180msgoal < 200ms
CLS0.05goal < 0.1
JS412kbwatch the budget

Lab scores are diagnostics. The 75th percentile on a real phone is the number that matters.

A budget makes trade-offs visible before the page ships.

Fix LCP first

Find the LCP element in DevTools. If it’s the hero image, make it priority, give it accurate dimensions, and provide a sizes value that matches the layout. A priority image inside a client component that waits for a context provider is still late.

hero.tsxtsx
<Image
  src="/hero.webp"
  alt="Team reviewing a product dashboard"
  width={1600}
  height={900}
  priority
  sizes="(max-width: 768px) 100vw, 60vw"
/>
  • Don’t lazy-load the hero.
  • Don’t animate the hero from opacity 0; it delays the visual paint.
  • Use AVIF/WebP through the image pipeline and set a sensible cache TTL.
  • Preload only what the first viewport needs.
  • Test the actual mobile viewport, not a resized desktop window.

Stop shipping the whole client

Every use client boundary is a place where React has to ship, parse, and execute code. Keep formatting, data shaping, and static content on the server. Put state where it’s needed, not at the root.

scene.tsxtsx
const SplineScene = dynamic(
  () => import("@/components/spline-scene"),
  {
    ssr: false,
    loading: () => <div className="h-[420px]" aria-hidden />,
  }
);

Heavy visual libraries are fine when they’re behind a user action or below the fold. They’re expensive when they block the hero. The same rule applies to map libraries, charting packages, and rich text editors.

Note

Server Components are not a free pass

A server component that fetches five things in sequence still delays the response. Parallelise data, stream the shell, and keep the critical path short.

Streaming beats a spinner

Server Components let you send the shell early and stream the slow parts. Use Suspense around product recommendations, reviews, or dashboard widgets. Don’t suspend the hero copy for a personalization call.

  • Render the static shell immediately.
  • Stream below-the-fold data inside Suspense.
  • Show a stable skeleton with reserved space.
  • Avoid a global loading screen that hides everything.

Caching is a product decision

Static pages are cheap. Personalized pages need a clear cache key. Decide what can be stale for five minutes, what must be real-time, and who is allowed to see a cached response.

  • Marketing pages: static or long revalidation.
  • Catalogue pages: short revalidation plus tag-based invalidation.
  • User dashboards: no shared cache.
  • CMS content: invalidate by tag after publish.
  • API responses: explicit cache headers and a key that includes tenant or user scope.

Watch out

Never cache another user’s data

A shared cache with the wrong key is not a performance bug. It’s a data leak. Test with two accounts before you enable caching on anything personalized.

Third-party scripts are a tax

Every tag manager, chat widget, heatmap, A/B tool, and review widget adds network and main-thread work. Load them after interaction or consent, self-host where possible, and remove what nobody uses.

  • Audit every script quarterly and name an owner.
  • Load chat and feedback widgets on interaction, not on page load.
  • Delay marketing tags until consent or idle time.
  • Self-host fonts and critical scripts where licences allow.
  • Check what the tag manager is injecting after a “small” update.

A two-week sprint

DaysFocusOutput
1–2Field data and route prioritiesA ranked list by traffic and P75 metrics
3–5LCP elementsHero images, fonts, and render order fixed
6–8Client boundariesHeavy libraries moved behind interaction or lazy loading
9–10Third partiesScripts deferred, removed, or consent-gated
11–12Streaming and cachingSlow sections isolated; cache keys reviewed
13–14GuardrailsBudgets in CI or a dashboard, not a one-off audit

What good looks like

Not a 100 Lighthouse score. A fast first screen, a page that responds when tapped, and no layout jump when the cookie banner appears. The rest is decoration.

The most useful question is not “is this fast?”. It’s “which part would our users notice if we fixed it tomorrow?”. Usually it’s one image, one script, and one component.

Direct answers

Questions people actually ask

Short, checkable answers. Every entry here is also published as structured FAQ data for search and answer engines.

Why is my Lighthouse score high but field LCP slow?

Lighthouse runs in a controlled lab environment and often on a fast machine. Field data includes real devices, networks, cache states, and third-party scripts. Use CrUX or real-user monitoring to find the actual LCP element and route.

Do Server Components make everything fast automatically?

No. They reduce client-side JavaScript for static and data-shaped content, but a slow server, sequential fetches, or a heavy client boundary can still hurt. Measure the response and the critical path, not just the bundle.

How do I measure INP?

Collect interaction events in the field, including event timing and long tasks. Lab tools can approximate it, but INP is best measured with real user interactions. Start by finding the routes and components that block the main thread.

Should I use a CDN for a Next.js app?

Usually yes for static assets and cacheable pages. If you deploy to a platform with edge caching, understand what it caches and how invalidation works. Personalized responses should not be cached at the edge without a user-specific key.

References

Sources and further reading

Compliance notes are operational guidance, not legal or tax advice. Rules and thresholds change — confirm your position with a qualified professional.

ZX

Written by

Zynitex Engineering

We design and build the systems behind Zynibill, Invoice Studio, and client platforms like Regalia, Kristive, and ElyteSpark. These notes are the things we wish we had read before shipping them.

More about Zynitex