← Blog
Next.js

5 Next.js Optimizations That Cut Our Load Time by 60%

By Muzamal Ali7 min readNext.js · Performance · Core Web Vitals
5 Next.js Optimizations That Cut Our Load Time by 60% — Next.js article by Muzamal Ali

Most Next.js performance advice reads as a feature list. This is the order I actually apply those features in on client work, with the measurements that told me whether each one was worth doing. The numbers below come from production projects — including my own site, which went from a 64 to an 88 mobile PageSpeed score using exactly the sequence described here.

How do you measure Next.js performance first?

Before touching code, separate your two sources of truth. Lab data — a Lighthouse run, PageSpeed Insights' simulated result — is reproducible and exists to diagnose. Field data — the Chrome UX Report and Search Console's Core Web Vitals panel — is collected from real Chrome users over a rolling 28-day window, and it is what Google actually ranks you on.

The workflow that works: find the problem in field data, reproduce and diagnose it in the lab, then confirm the fix back in field data. Skipping the field half misleads in both directions. On my own site I spent an afternoon chasing a 1.6-second "element render delay" that existed only in Lighthouse's simulated throttling against localhost — in a real browser the element painted with first paint. The opposite trap is worse: localhost hides your real server response time. My Render-hosted site pays roughly 800ms of TTFB that no amount of component optimization can touch — a fact only visible in field data, and one that changed where I spent my effort.

Practical setup: PageSpeed Insights weekly on your three highest-traffic templates, Search Console's Core Web Vitals report reviewed with the same seriousness as error monitoring, and local Lighthouse treated strictly as a debugging tool, never a scorecard.

Why images dominate LCP

The Largest Contentful Paint metric is often won or lost on the hero image. On production Next.js apps I have shipped, switching from raw <img> tags to next/image with explicit sizes and priority on above-the-fold assets consistently moved LCP from the 3–4s range into sub-2.5s territory. The key is not just the component — it is pairing it with correctly sized source files and avoiding oversized originals that the browser then scales down at runtime.

What does next/font actually fix?

Fonts are the sneakiest LCP regression in Next.js because most audits never flag them. With font-display: swap — the widespread default — text paints in a fallback font and then repaints when the webfont arrives. If that text is your LCP element, the browser records the repaint as a new, later LCP candidate. On one project that single mechanism added about 1.5 seconds to reported LCP with no visible problem anywhere in the network waterfall.

next/font fixes the delivery half automatically: it self-hosts the files, adds preload tags, and generates metric-adjusted fallbacks so the eventual swap does not shift layout. For the font used by the largest text on the page I go one step further:

import { Geist } from "next/font/google";

const geist = Geist({
  subsets: ["latin"],
  display: "optional", // first paint is final — no late swap repaint
});

With display: "optional", the browser uses the webfont if it is ready at first paint — which a preloaded, self-hosted font usually is — and otherwise keeps the metric-matched fallback for that page view. LCP stops depending on font arrival entirely. Keep swap for decorative type that never renders the largest element.

Dynamic imports for heavy UI

Dashboards and admin panels tend to accumulate heavy dependencies: charts, editors, maps. Loading them in the main bundle punishes every route. I use next/dynamic with ssr: false only when the component truly needs the window object; otherwise SSR stays on so first paint still carries meaningful HTML. Grouping dynamic imports next to the route segment that needs them keeps the mental model clear for the rest of the team.

Server Components for static shells

Where content does not need client-side state, defaulting to React Server Components means less JavaScript shipped to the browser. I use client components at the leaves — interactive widgets — and keep page shells, layouts, and static copy on the server. That split shows up in bundle analyzer output as smaller shared chunks and faster hydration on low-end devices.

Which caching headers matter on a Next.js site?

Next.js fingerprints everything under /_next/static and ships it with Cache-Control: public, max-age=31536000, immutable — that part is free. The gap is public/: images, fonts, and icons served from there carry no caching policy at all unless you add one in next.config.ts:

async headers() {
  return [
    {
      source: "/images/:path*",
      headers: [
        { key: "Cache-Control", value: "public, max-age=86400, must-revalidate" },
      ],
    },
  ];
},

The second half of caching is the HTML itself. Statically generated pages are cheap to render, but if your host has no CDN in front of the Node process, every visitor still pays the full server round trip. That is a hosting decision, not a React decision — which is exactly why the TTFB line in your field data deserves a look before you refactor a single component.

Bundle analyzer in practice

Adding @next/bundle-analyzer behind an environment flag gives a repeatable workflow: build, open the treemap, sort by size, and question any dependency that appears in the critical path for landing pages. I treat analyzer runs as part of release prep for client-facing apps, not a one-off audit.

Prefetching that helps, not hurts

The App Router's Link prefetch defaults are usually correct, but on pages with dozens of links I selectively disable prefetch for low-priority destinations. For high-intent flows — checkout, signup, primary CTA targets — I ensure those routes are prefetched and warmed so navigation feels instant. Combining that with router.prefetch for likely next steps after form success reduced perceived latency in a recent engagement.

What did the numbers look like before and after?

For concreteness, here is my own site's relaunch, measured with identical PageSpeed mobile runs before and after applying everything above:

MetricBeforeAfter
Performance score (mobile)6488
Largest Contentful Paint4.7 s3.2 s
Total Blocking Time330 ms180 ms
Cumulative Layout Shift00.002
Heaviest render-path resource162 KB analytics scriptdeferred past load

The single biggest win was none of the glamorous items: it was stopping the hero headline's entrance animation from hiding the LCP element until JavaScript arrived. Performance work is usually like that — the top fix is specific to your page, which is why measurement has to come before checklists.

Which common "optimizations" make performance worse?

  • priority on below-the-fold images. Preloading a screenshot nobody can see yet steals bandwidth from the hero during the critical first seconds. I have caught this in my own code.
  • Lazy-loading the hero. loading="lazy" on above-the-fold media adds a whole scheduling round trip to your LCP. Never lazy-load the largest element.
  • Entrance animations on the LCP element. Animating your headline in from opacity: 0 after hydration re-registers LCP seconds later. Animate everything else; let the largest element paint immediately.
  • Blanket "use client". Marking layouts client-side drags entire trees into the bundle and the hydration cost. Push the directive to the leaves.
  • Analytics before load. A 162 KB gtag script fetched during startup competes with everything; loading it after the load event costs nothing measurable in data quality.

Each of these looks like diligence — preloading, animating, instrumenting. The metric moves the other way.

What I got wrong on my own project

My site shipped for months with a 1.8-second artificial loading screen because it felt polished. Deleting it was the largest single UX improvement of the relaunch — nothing I optimized afterwards came close. I had also set priority on a project screenshot that sat three viewports below the fold, and my custom cursor wrote left/top styles on every mousemove, forcing layout work during scroll.

The common root cause: every one of those decisions was made by eye and never measured. What I would do differently — and now do on client work — is put the PageSpeed number in the definition of done from the first sprint, so regressions surface the week they are introduced rather than at relaunch.

Takeaway

Performance work is cumulative: images, code splitting, server-first rendering, and intentional prefetching each trim a little waste. Measured together, those techniques are how we cut load times by roughly sixty percent on a real production codebase without rewriting the product.

If you want a deeper look at how I apply these techniques on client work, the case studies page breaks down real projects with before/after numbers, and I cover the team side of this in how I structure React components for teams of 10+.

Muzamal Ali

Muzamal Ali — Senior Frontend Engineer & Team Lead

Senior Frontend Engineer with 5+ years building production React and Next.js applications. I've led teams of 3–9 developers across healthcare, aviation, AI, and SaaS platforms. Based in Pakistan, working async with European tech teams.

Working on something similar?

I help European tech teams ship better frontends.

Related Articles