
Speed is not just a feature — it directly impacts user retention, SEO rankings, and conversion rates. Studies consistently show that a one-second delay in page load time can reduce conversions by up to 7%. Optimising your web app's performance is one of the highest-return investments you can make as a developer.
Google's Core Web Vitals are the industry-standard metrics for measuring user experience:
| Metric | What It Measures | Good Score |
|---|---|---|
| LCP (Largest Contentful Paint) | How fast the main content loads | ≤ 2.5s |
| INP (Interaction to Next Paint) | How responsive the page is to input | ≤ 200ms |
| CLS (Cumulative Layout Shift) | How stable the layout is during load | ≤ 0.1 |
You can measure these using PageSpeed Insights or the Chrome DevTools Performance panel.
Images are typically the heaviest assets on a page. A few high-impact changes:
width and height on <img> tags to
prevent layout shift (CLS).loading="lazy" attribute.<img
src="/images/hero.webp"
alt="Hero image"
width="1200"
height="600"
loading="lazy"
/>
In Next.js, the <Image> component handles all of this automatically.
Don't ship your entire JavaScript bundle upfront. Split it into smaller chunks loaded on demand:
import dynamic from "next/dynamic";
// This component is only loaded when it's actually needed
const HeavyChart = dynamic(() => import("@/components/HeavyChart"), {
loading: () => <p>Loading chart...</p>,
});
React's built-in React.lazy() works similarly outside of Next.js.
JavaScript and CSS loaded in the <head> block the browser from rendering
anything. Best practices:
defer or async to third-party <script> tags.<!-- Defer non-critical scripts -->
<script src="analytics.js" defer></script>
A Content Delivery Network serves your static assets (images, fonts, JS, CSS) from servers geographically close to each user. This dramatically reduces latency. Vercel, Netlify, and Cloudflare all provide built-in CDN caching.
HTTP caching allows browsers to reuse previously downloaded resources:
Cache-Control: public, max-age=31536000, immutable
This header tells browsers to cache the file for one year. Use hashed filenames
(e.g., bundle.abc123.js) so cached files are automatically invalidated when
content changes.
Every kilobyte of JavaScript has a cost — it must be downloaded, parsed, and executed:
date-fns over moment.js, zustand over
redux for simple state.// ❌ Imports the entire lodash library (~70kb gzipped)
import _ from "lodash";
// ✅ Only imports the debounce function (~2kb)
import debounce from "lodash/debounce";
Web fonts are often a hidden performance bottleneck:
font-display: swap so text remains visible while fonts load.next/font to automatically optimise and self-host Google
Fonts.@font-face {
font-family: "Inter";
src: url("/fonts/inter.woff2") format("woff2");
font-display: swap;
}
Use <link rel="preload"> to tell the browser to fetch important resources
early:
<link
rel="preload"
href="/fonts/inter.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<link rel="preload" href="/images/hero.webp" as="image" />
Web performance is an ongoing practice, not a one-time fix. Start by measuring with Lighthouse or PageSpeed Insights, identify your biggest bottlenecks, and work through them methodically. Even a few targeted improvements — optimised images, lazy loading, and a CDN — can make a night-and-day difference for your users.