Yasiru Senarathna Logo
All ArticlesDevelopment
June 18, 2026

Next.js Performance Tips Every Frontend Developer Should Know

From image optimisation and lazy loading to route caching and bundle analysis the techniques I use to keep Next.js sites fast, score well in Core Web Vitals, and deliver a frictionless user experience.

UX Design Process Workspace

Performance is a design decision

A beautifully designed interface is entirely worthless if it takes five seconds to become interactive. Users don't care about your bento grids or premium typography if the page stutters while they try to scroll. As frontend developers, we have a massive amount of control over how users experience the speed of our products.

Next.js provides an incredibly powerful set of tools out of the box, but simply installing the framework doesn't guarantee a fast site. You have to know which levers to pull. Here are the core optimization techniques I implement on every production build to protect my Core Web Vitals and ensure a high-end feel.


1. Master the Next.js Image component

Images are usually the heaviest assets on a page and the main culprit behind a poor Largest Contentful Paint (LCP). The next/image component handles WebP/AVIF conversion and lazy loading automatically, but you need to configure the priority and sizes props correctly to actually reap the benefits.

Always add the priority prop to the main hero image so Next.js preloads it immediately. For responsive layouts, explicitly define the sizes prop so the browser doesn't download a massive 4K image for someone viewing your site on an iPhone.

HeroSection.tsx
import Image from "next/image";

export default function HeroSection() {
return (
<div className="relative w-full aspect-video">
<Image
src="/images/hero.webp"
alt="Product overview"
fill
// 1. Preload the LCP image to fix load times
priority

// 2. Prevent CLS by ensuring object-cover
className="object-cover"

// 3. Serve the exact right size per breakpoint
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</div>
);
}

2. Eliminate Cumulative Layout Shift (CLS) with next/font

If you care about typography, you know how jarring it is when a fallback system font suddenly snaps into your custom geometric sans-serif half a second after the page loads. This flash of unstyled text (FOUT) ruins the premium feel of a design and heavily penalizes your Cumulative Layout Shift (CLS) score.

Next.js provides next/font. It automatically optimizes your fonts (including custom local fonts) and removes external network requests for improved privacy and performance. Most importantly, it utilizes an inbuilt size-adjust feature that mathematically matches your fallback font size to your custom font, making the swap completely invisible.

layout.tsx
import localFont from "next/font/local";

// Load custom fonts locally with preload and zero layout shift
const allianceNo2 = localFont({
src: "../fonts/AllianceNo2-Regular.woff2",
variable: "--font-alliance",
display: "swap",
});

export default function RootLayout({ children }) {
return (
<html lang="en" className={`${allianceNo2.variable}`}>
<body className="font-sans">
{children}
</body>
</html>
);
}

3. Lazy load heavy components

Not everything needs to be in the initial JavaScript bundle. If you have a heavy 3D animation, a complex Framer Motion interaction, or a large modal that is hidden by default, load it dynamically.

Using next/dynamic defers the loading of these components until they are actually needed. This drastically reduces your initial payload and improves your Time to Interactive (TTI), ensuring the core content loads instantly.

Dashboard.tsx
import dynamic from "next/dynamic";
import { useState } from "react";

// Dynamically import the heavy modal component
// It will not be included in the initial page bundle
const HeavyChartModal = dynamic(
() => import("@/components/HeavyChartModal"),
{
loading: () => <p>Loading analytics...</p>,
// Disable SSR if the component relies on window object
ssr: false
}
);

export default function Dashboard() {
const [isOpen, setIsOpen] = useState(false);

return (
<div>
<button onClick={() => setIsOpen(true)}>
View Stats
</button>

{isOpen && <HeavyChartModal />}
</div>
);
}

4. Defer third-party scripts

If you run a monetized platform or use heavy analytics tools, you know that third-party scripts can completely hijack the browser's main thread. This causes your beautifully optimized site to freeze up while it waits for an ad network to load.

Instead of placing standard <script> tags in your head, use next/script. By setting the strategy to lazyOnload, Next.js will wait until the page has fully loaded and the user's browser is idle before fetching the heavy tracking or ad scripts.

layout.tsx
import Script from "next/script";

export default function RootLayout({ children }) {
return (
<html>
<body>
{children}

// Wait for the browser to be idle before loading ads or analytics
<Script
src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"
strategy="lazyOnload"
/>
</body>
</html>
);
}

5. Aggressively cache your data fetches

The App Router radically changed how data is cached in Next.js. If you are fetching data from a CMS or an external API for content that doesn't change every single second (like blog posts or product descriptions), you should be caching it.

Instead of hitting your database on every user request, use Incremental Static Regeneration (ISR) to cache the data on the server and revalidate it in the background at set intervals.

app/articles/page.tsx
// Fetching data inside a Server Component
async function getArticles() {
const res = await fetch(
"https://api.example.com/articles",
{
// Cache result, revalidate every 1 hour (3600s)
next: { revalidate: 3600 }
}
);

if (!res.ok) throw new Error("Failed to fetch");
return res.json();
}

export default async function ArticlesPage() {
const articles = await getArticles();

return (
<ul>
{articles.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}

6. Utilize Route Prefetching

Sometimes performance isn't just about actual load times; it's about perceived load times. If an interface feels instantaneous to the user, they perceive the application as high-quality.

Next.js natively prefetches routes whenever a <Link> component enters the user's viewport. By the time the user actually clicks the link, the destination page is already loaded in the background. Ensure you are using the native Link component instead of standard anchor tags <a> to take advantage of this invisible speed boost.


7. Run bundle analysis regularly

You can't fix what you can't see. The easiest way to ruin your Next.js performance is by accidentally importing an entire heavy library (like Lodash or Moment.js) when you only needed one utility function. Over time, these imports stack up, creating a massive JavaScript payload that chokes mobile devices.

Install @next/bundle-analyzer. I run this before every major deployment. It generates an interactive visual map of your JavaScript bundles so you can see exactly which dependencies are bloating your client-side payload, allowing you to either replace them or dynamically import them.


The short version

Building fast applications requires discipline. Optimize your images, defer heavy components and scripts, lock down your typography with native font tools, and cache your server responses. Good UX doesn't just look good it feels fast.


Yasiru Senarathna
Yasiru SenarathnaUI/UX Designer & Marketer

I design and build digital products from Sri Lanka, ensuring the bridge between aesthetic design and technical performance is seamless. Want to talk shop about React or Next.js? Feel free to reach out.

Back to Articles