How to Improve Website Performance Using React and Next.js
A practical guide to faster React and Next.js websites: measure Core Web Vitals, ship less JavaScript with Server Components, pre-render with SSG, and optimize images, fonts, and third-party scripts.
Performance is a feature. A slow page loses visitors before they see your work, and since Core Web Vitals became a ranking signal, it costs you search traffic too. The good news: React and Next.js already give you most of the tools you need. You just have to use them on purpose.
I recently rebuilt this portfolio from a client-side Vite app into Next.js. Below are the techniques that made the biggest difference, with code you can copy into your own project.
1. Measure before you optimize
Guessing is the fastest way to waste a weekend. Start with the three Core Web Vitals Google uses to judge real-user experience:
- LCP (Largest Contentful Paint): how fast the main content appears. Aim for 2.5 seconds or less.
- INP (Interaction to Next Paint): how quickly the page responds to clicks and typing. Aim for 200 ms or less.
- CLS (Cumulative Layout Shift): how much the layout jumps while loading. Aim for 0.1 or less.
Use Lighthouse or PageSpeed Insights for lab data, then confirm with field data from real users. Next.js exposes the metrics through useReportWebVitals, so you can send them to any analytics tool:
"use client";
import { useReportWebVitals } from "next/web-vitals";
export function WebVitals() {
useReportWebVitals((metric) => {
// Replace with your analytics endpoint
console.log(metric.name, Math.round(metric.value));
});
return null;
}2. Render on the server and ship less JavaScript
In the App Router, every component is a React Server Component by default. Its code runs on the server and never reaches the browser, so it adds zero bytes to your JavaScript bundle. Only components marked with "use client" are sent to the browser and hydrated.
The rule of thumb: push "use client" as far down the tree as possible. Keep pages and layouts on the server and make only the interactive leaves client components.
// Server Component: none of this code is sent to the browser
import { LikeButton } from "./like-button"; // the only client code on this page
export default async function PostPage({ params }) {
const { slug } = await params;
const post = await getPost(slug);
return (
<article>
<h1>{post.title}</h1>
<PostBody content={post.content} />
<LikeButton slug={slug} />
</article>
);
}The fastest JavaScript is the JavaScript you never send to the browser.
3. Pre-render pages with SSG and ISR
If a page looks the same for every visitor, there is no reason to render it on every request. Static Site Generation (SSG) builds the HTML once at build time and serves it straight from a CDN, which is about as fast as the web gets. This article is rendered exactly that way.
// Build one static HTML page per post at build time
export const generateStaticParams = () =>
blogs.map((post) => ({ slug: post.slug }));
// Any slug not returned above becomes a 404 instead of a server render
export const dynamicParams = false;When content changes regularly, use Incremental Static Regeneration. The page stays static, and Next.js rebuilds it in the background at most once per interval:
// Regenerate this page at most once an hour
export const revalidate = 3600;Save dynamic rendering for pages that really depend on the request, such as dashboards, carts, or anything that reads cookies.
4. Optimize images with next/image
Images are usually the heaviest part of a page and the most common LCP element. On this site, the 40 px logo in the navbar was being served as a 2481 px, 167 KB JPEG. Switching to next/image fixed that in one line.
- It resizes images to the size actually displayed and serves modern formats like WebP and AVIF.
- It lazy-loads images below the fold by default.
- It reserves space using the image's width and height, which prevents layout shift (CLS).
import Image from "next/image";
import hero from "@/assets/images/hero.webp";
export function Hero() {
return (
<Image
src={hero}
alt="Dashboard preview"
sizes="(min-width: 1024px) 50vw, 100vw"
placeholder="blur"
preload // the LCP image: load it early (called "priority" before Next.js 16)
/>
);
}Always set sizes for responsive images. Without it, the browser may download a much larger file than the layout needs.
5. Load fonts with next/font
Loading Google Fonts with a CSS @import blocks rendering: the browser has to fetch the stylesheet, then the font files, before it can draw text. next/font downloads the fonts at build time, serves them from your own domain, and generates a size-adjusted fallback so text does not jump when the real font arrives.
import { Syne } from "next/font/google";
const syne = Syne({
subsets: ["latin"],
display: "swap",
variable: "--font-syne",
});
export default function RootLayout({ children }) {
return (
<html lang="en" className={syne.variable}>
<body>{children}</body>
</html>
);
}6. Split heavy components with next/dynamic
Charts, maps, rich text editors, and video players can each add hundreds of kilobytes. If they are not needed for the first paint, load them only when they are rendered:
"use client";
import dynamic from "next/dynamic";
const RevenueChart = dynamic(() => import("./revenue-chart"), {
ssr: false,
loading: () => <div className="h-80 animate-pulse rounded-2xl bg-secondary" />,
});Give the loading placeholder the same height as the real component. Otherwise you trade a smaller bundle for layout shift.
7. Keep interactions responsive
A fast first load means little if the page freezes when someone types. INP measures exactly that. React gives you two tools to keep urgent updates (typing, clicking) ahead of expensive ones (filtering a long list):
useDeferredValuelets the input update instantly while the expensive result catches up.useTransitionmarks a state update as non-urgent, so React can interrupt it for user input.
"use client";
import { useDeferredValue, useState } from "react";
export function ProjectSearch({ projects }) {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
const results = projects.filter((project) =>
project.title.toLowerCase().includes(deferredQuery.toLowerCase())
);
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<ProjectList items={results} />
</>
);
}Deferring only helps if ProjectList skips re-rendering when its props have not changed. Instead of sprinkling memo and useMemo everywhere, enable the React Compiler, which adds that memoization automatically. In Next.js 16, install babel-plugin-react-compiler and set reactCompiler: true in next.config.ts.
8. Tame third-party scripts
Chat widgets, tag managers, and tracking pixels are frequent performance killers because they run on the main thread while your page is loading. Load them with next/script and choose a strategy that matches how important they are:
import Script from "next/script";
// Not needed for the first paint: load it once the browser is idle
<Script src="https://widget.example.com/chat.js" strategy="lazyOnload" />Before adding any script, ask whether you really need it. The cheapest third-party script is the one you remove.
9. Fetch data in parallel and cache it
Sequential await calls create waterfalls: each request waits for the previous one to finish. When requests do not depend on each other, start them together:
// Slow: the second request waits for the first
const profile = await getProfile();
const projects = await getProjects();
// Fast: both requests run at the same time
const [profile, projects] = await Promise.all([getProfile(), getProjects()]);Then cache what does not change on every request. With fetch in a Server Component, a single option keeps the response cached and refreshes it in the background:
const res = await fetch("https://api.example.com/projects", {
next: { revalidate: 3600 }, // reuse the response for up to an hour
});Quick checklist
- Measure LCP, INP, and CLS with real-user data before changing anything.
- Keep components on the server and add
"use client"only where you need interactivity. - Pre-render static pages with
generateStaticParams, and userevalidatefor content that changes. - Serve every image through
next/imagewith correctsizes, and preload the LCP image. - Load fonts with
next/fontinstead of CSS imports. - Lazy-load heavy, below-the-fold components with
next/dynamic. - Use
useDeferredValue,useTransition, and the React Compiler to keep interactions fast. - Load third-party scripts with
next/script, or remove them. - Run independent data requests in parallel and cache the results.
You do not need to do everything at once. Pick the metric that is worst on your site, fix its biggest cause, measure again, and repeat. Small, measured improvements add up to a site that feels instant.