Next.js

How I Reduced My Next.js Website Loading Time

My site scored 52 on PageSpeed and I was embarrassed. Here's every change that took it to 94 — images, fonts, and killing client JS.

ZZ

Zeeshan Zakir

July 21, 20265 min readNext.js
How I Reduced My Next.js Website Loading Time

A client forwarded me a PageSpeed Insights screenshot of the site I'd built for him, with a one-word message: "Why?"

Mobile score: 52. Orange and red circles everywhere. And here's the embarrassing part — this was a Next.js site. The framework whose whole pitch is performance. The framework I chose because of performance. It turns out Next.js gives you excellent tools and absolutely will not stop you from ignoring all of them, which is exactly what I had done.

Two evenings of work later the score was 94. Here is every change, in order of impact, with the ones that turned out to be a waste of time at the end.

Step zero: measure before touching anything

I opened Lighthouse in an incognito window (extensions skew results — my password manager alone was costing points) and PageSpeed Insights for real-world field data. The report told me exactly where the pain was: Largest Contentful Paint at 4.8 seconds, a pile of unused JavaScript, and layout shift from fonts.

The single most useful thing Lighthouse tells you is which element is your LCP. Mine was the hero image. That focused everything that followed — no guessing.

Fix 1: Images, done properly this time (biggest win)

I was using next/image, technically. But the hero image was a 2400px-wide photo being displayed at 800px, without the priority prop, so the browser discovered it late and downloaded far too much of it.

Three changes:

<Image
  src={hero}
  alt="Dashboard preview"
  priority              // preloads the LCP image
  sizes="(max-width: 768px) 100vw, 800px"
  className="rounded-xl"
/>

The priority prop tells Next.js this image matters now. The sizes prop tells the browser which resolution to actually download instead of defaulting to something huge. And I re-exported the source image at a sane resolution because no sizes attribute fixes a 4MB original.

LCP went from 4.8s to 2.1s. One fix, half the problem gone.

Fix 2: Fonts with next/font (killed the layout shift)

The site loaded Google Fonts the old way — a <link> tag in the head. That's a render-blocking request to another domain, plus the classic flash of shifting text when the font arrives.

next/font self-hosts the font files and injects proper fallbacks:

import { Inter } from 'next/font/google';

const inter = Inter({ subsets: ['latin'], display: 'swap' });

Cumulative Layout Shift dropped from 0.21 to effectively zero, and one external request disappeared entirely. Ten minutes of work. I now do this in the first hour of every project.

Fix 3: The "use client" audit (the Next.js-specific win)

This was the eye-opener. I grepped for "use client" and found it at the top of components that had no interactivity at all — including, somehow, the footer. Early in the App Router era I'd sprinkled it around whenever an error mentioned hooks, and every one of those components was shipping its JavaScript to the browser for no reason.

The rule I follow now: push "use client" to the leaves. A page stays a Server Component; the one interactive button inside it becomes a tiny client component. After the audit, first-load JavaScript dropped from roughly 380KB to 210KB. The framework was right all along; I had just been overriding it.

Fix 4: Dynamic imports for below-the-fold weight

The pricing page had a chart. The chart library was loading on every page because it sat in a shared component. Two lines fixed it:

const RevenueChart = dynamic(() => import('./RevenueChart'), {
  loading: () => <ChartSkeleton />,
});

Same treatment for a testimonial carousel and an embedded map. None of these are visible in the first viewport, so there's no reason they should compete with the hero for bandwidth.

Fix 5: What the bundle analyzer confessed

@next/bundle-analyzer draws you a map of your JavaScript, and mine had two shameful continents: a full date library imported for exactly one "3 days ago" label, and an icon package imported in a way that dragged in far more than the six icons I used.

The date library got replaced with Intl.RelativeTimeFormat — built into the browser, zero KB. The icons got proper named imports. Every project I've analyzed since has had at least one dependency like this: huge, barely used, and invisible until you look.

Fix 6: Third-party scripts on a leash

Analytics and a chat widget were loading eagerly in the head. next/script with strategy="lazyOnload" pushes them to after everything meaningful:

<Script src="https://widget.example.com/chat.js" strategy="lazyOnload" />

Nobody needs the chat bubble in the first 500ms. Nobody has ever needed the chat bubble in the first 500ms.

What didn't move the needle

Honesty corner. I spent an evening wrapping components in memo and useMemo — no measurable difference; that's for re-render problems, not load time. Obsessive preconnect tags for domains that loaded lazily anyway: nothing. Micro-optimizing Tailwind class counts: please, no. The lesson that stuck: optimize what the measurement points at, not what feels clever.

The mistake worth confessing

At one point I lazy-loaded everything, including the hero image. The score went down. Lazy-loading your LCP element is like putting your front door behind a queue — the whole point is that it arrives first. priority on the LCP, lazy on the rest. That distinction is half of image performance.

Where it landed

Mobile 52 → 94, desktop 98. But the number I actually care about came from analytics a month later: bounce rate on mobile dropped noticeably, and the client stopped sending screenshots. The site didn't get new features or a redesign. It just stopped making people wait — which, on the modern web, is a feature.



Need help building this?

I offer full-stack development services for startups and product teams.

If you want a faster path from idea to shipped product, I can help with architecture, frontend systems, backend APIs, and launch-ready builds.

View Services

Share this post

Related posts

More practical reading from the blog to keep your momentum going.