4 Next.js Optimizations That Cut My Load Time (With Real Numbers)

Programming· 5 min read

The Optimization Nobody Does (But Has The Most Impact)

Last week I analyzed the bundle of a Next.js project in production.

Lighthouse: 67 on mobile.

I looked at the waterfall. 3.2s First Contentful Paint. The problem wasn't the code. It was what Next.js loads by default that you never optimize.

Here are the 4 things I changed (with real numbers) and why you're probably overlooking them too.

1. Next/Image: It's Not Automatic (And Your Images Are Still Heavy)

Everyone uses `next/image`. Few configure it properly.

The typical problem:

```jsx import Image from 'next/image'

export default function Hero() { return ( <Image src="/hero.jpg" width={1200} height={600} alt="Hero image" /> ) } ```

Looks correct. But Next.js serves the image in WebP format only if the browser supports it. Without more configuration, you're still serving heavy JPEGs to modern users.

What actually works:

```jsx import Image from 'next/image'

export default function Hero() { return ( <Image src="/hero.jpg" width={1200} height={600} alt="Hero image" priority // Load this image first quality={85} // 85 vs 100 default placeholder="blur" blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..." // Generate with plaiceholder /> ) } ```

Real impact: Largest Contentful Paint dropped from 2.8s to 1.1s on 4G connections.

But here's what nobody tells you: you need to configure `next.config.js` too:

```javascript module.exports = { images: { formats: ['image/avif', 'image/webp'], deviceSizes: [640, 750, 828, 1080, 1200], imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], }, } ```

AVIF is more efficient than WebP. Next.js supports it but it's not enabled by default.

2. Next/Font: The Trick Google Doesn't Tell You

Google Fonts is convenient. It's also a bottleneck.

Each request to `fonts.googleapis.com` adds latency. And if you're using multiple weights, each one is another roundtrip.

The solution I use now:

```javascript import { Inter } from 'next/font/google'

const inter = Inter({ subsets: ['latin'], display: 'swap', variable: '--font-inter', // Only the weights you actually use weight: ['400', '500', '700'], // Preload to eliminate FOUT preload: true, })

export default function RootLayout({ children }) { return ( <html lang="en" className={inter.variable}> <body>{children}</body> </html> ) } ```

Why it works:

1. Next.js downloads fonts at build time 2. Serves them from your domain (no external DNS lookup) 3. `display: 'swap'` shows text immediately 4. Only loads necessary weights

Impact: Completely eliminated FOUT (Flash of Unstyled Text). Cumulative Layout Shift dropped to 0.001.

The interesting part: I had 5 Inter weights loading before. Only used 3. Those 2 extra weights added unnecessary payload.

3. Route Prefetching: Next.js's Hidden Superpower

Next.js preloads routes automatically when they appear in viewport. But the default configuration is conservative.

```jsx import Link from 'next/link'

export default function Navigation() { return ( <nav> <Link href="/about" prefetch={true}> About Us </Link> <Link href="/blog" prefetch={true}> Blog </Link> </nav> ) } ```

This preloads routes immediately. When the user clicks, navigation is instant.

The trick I use for critical pages:

```jsx 'use client' import { useEffect } from 'react' import { useRouter } from 'next/navigation'

export default function HomePage() { const router = useRouter()

useEffect(() => { // Prefetch critical routes on mount router.prefetch('/pricing') router.prefetch('/demo') }, [router])

return ( // Your content ) } ```

Impact: Navigation from landing to pricing feels instant. Literally 0ms perceived wait time.

But careful: don't prefetch everything. Each prefetch consumes bandwidth. Prioritize routes with most traffic.

4. Bundle Analysis: Webpack Bundle Analyzer Doesn't Lie

I installed `@next/bundle-analyzer` and discovered I had 300KB of Lodash in my bundle.

I was only using `debounce` and `throttle`.

```bash npm install @next/bundle-analyzer ```

```javascript // next.config.js const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true', })

module.exports = withBundleAnalyzer({ // your config }) ```

Run:

```bash ANALYZE=true npm run build ```

The browser opens with a visual map of your bundle. Everything that's big and you don't recognize: candidate for removal.

What I found in my build:

  • Lodash: 300KB → replaced with native functions
  • Moment.js: 230KB → switched to date-fns
  • Unused Tailwind classes: 80KB → configured purge correctly

Total removed: ~600KB of JavaScript

Impact: Time to Interactive dropped from 4.1s to 2.3s on 3G.

What Really Matters

Next.js optimizations aren't magic. They're specific decisions about what to load, when, and how.

Most projects have obvious quick wins:

1. Configure next/image properly (don't just use the component) 2. Self-host your fonts with next/font 3. Prefetch critical routes proactively 4. Analyze your bundle and eliminate unnecessary dependencies

You don't need a complete rewrite. You need to measure, find bottlenecks, and attack them one by one.

My current workflow:

1. Production build 2. Lighthouse on mobile (not desktop) 3. Bundle analyzer to identify weight 4. Chrome DevTools → Performance to find blocks 5. Fix the biggest problem first 6. Repeat

The tools are there. Next.js gives you the APIs. But you have to use them actively.

The difference between a "functional" Next.js site and a truly fast one isn't the framework. It's deliberate configuration.

---

*Want to see bundle analyzer in action? Next week I'm sharing the complete analysis of a real project with specific numbers from each optimization.*

Brian Mena

Brian Mena

Software engineer building profitable digital products: SaaS, directories and AI agents. All from scratch, all in production.

LinkedIn