Website performance isn’t just a technical metric — it’s a business metric. Studies consistently show that a 1-second delay in page load time can reduce conversions by 7%. Google uses Core Web Vitals as a ranking signal. Users abandon sites that take more than 3 seconds to load.
In this guide, I’ll walk through 10 battle-tested techniques that will dramatically improve your website’s performance.
1. Optimize Your Images
Images are often the single largest contributor to page weight. A few strategies:
Use Modern Formats
<picture>
<!-- AVIF: Best compression, great browser support -->
<source srcset="hero.avif" type="image/avif" />
<!-- WebP: Great fallback -->
<source srcset="hero.webp" type="image/webp" />
<!-- JPEG: Universal fallback -->
<img src="hero.jpg" alt="Hero image" width="1200" height="630" />
</picture>
AVIF offers 50% better compression than JPEG and 20% better than WebP, with excellent quality at small file sizes.
Lazy Load Below-the-Fold Images
<!-- Native lazy loading - no JavaScript needed -->
<img src="article-thumbnail.jpg" alt="..." loading="lazy" decoding="async" />
Specify Dimensions to Prevent Layout Shift
Always include width and height attributes. The browser uses the aspect ratio to reserve space before the image loads, preventing Cumulative Layout Shift (CLS).
2. Implement Effective Caching
The fastest request is the one never made. Use HTTP caching aggressively:
# Nginx configuration
location ~* \.(js|css)$ {
# Fingerprinted assets can be cached forever
add_header Cache-Control "public, max-age=31536000, immutable";
}
location ~* \.(html)$ {
# HTML should revalidate
add_header Cache-Control "public, max-age=0, must-revalidate";
}
location ~* \.(jpg|jpeg|png|webp|avif|svg|ico)$ {
add_header Cache-Control "public, max-age=2592000"; # 30 days
}
Service Worker Caching
For more control, use a Service Worker with a cache-first strategy for static assets:
// sw.js
const CACHE_VERSION = "v1";
const STATIC_CACHE = `static-${CACHE_VERSION}`;
self.addEventListener("fetch", (event) => {
// Cache-first for images and fonts
if (
event.request.destination === "image" ||
event.request.destination === "font"
) {
event.respondWith(
caches.match(event.request).then(
(cached) =>
cached ||
fetch(event.request).then((response) => {
const clone = response.clone();
caches
.open(STATIC_CACHE)
.then((cache) => cache.put(event.request, clone));
return response;
}),
),
);
}
});
3. Minimize and Compress JavaScript
JavaScript is the most expensive resource per byte. A 100KB image costs much less to process than 100KB of JavaScript.
Code Splitting
Don’t ship code the user doesn’t need immediately:
// Instead of importing everything upfront:
import { HeavyChart } from "./charts"; // ❌
// Lazy load when needed:
const HeavyChart = lazy(() => import("./charts")); // ✅
// Route-based code splitting in React Router
const Dashboard = lazy(() => import("./pages/Dashboard"));
Tree Shaking
Ensure your bundler can eliminate dead code:
// ❌ Imports entire library (prevents tree shaking)
import _ from "lodash";
const result = _.groupBy(data, "category");
// ✅ Import only what you need
import groupBy from "lodash/groupBy";
const result = groupBy(data, "category");
Measure JavaScript Impact
# Check your bundle size
npx bundlephobia lodash
# Analyze your webpack bundle
npx webpack-bundle-analyzer
4. Eliminate Render-Blocking Resources
Resources that block the browser from rendering the page are critical to address:
<head>
<!-- ❌ Blocks rendering - browser must fetch and parse before rendering -->
<link rel="stylesheet" href="styles.css" />
<script src="analytics.js"></script>
<!-- ✅ Non-blocking CSS with preload -->
<link
rel="preload"
href="styles.css"
as="style"
onload="this.rel='stylesheet'"
/>
<!-- ✅ Async script - doesn't block HTML parsing -->
<script src="analytics.js" async></script>
<!-- ✅ Defer script - runs after HTML parsing -->
<script src="app.js" defer></script>
</head>
Inline Critical CSS
Extract the CSS needed for above-the-fold content and inline it:
<style>
/* Critical CSS inlined for instant rendering */
body {
font-family: system-ui;
margin: 0;
}
header {
background: #fff;
padding: 1rem;
}
.hero {
min-height: 60vh;
display: flex;
align-items: center;
}
</style>
<!-- Non-critical CSS loaded asynchronously -->
<link
rel="preload"
href="/styles/full.css"
as="style"
onload="this.rel='stylesheet'"
/>
5. Use a Content Delivery Network (CDN)
A CDN serves your content from edge servers closest to your users, dramatically reducing latency:
Without CDN: User in Sydney → Server in New York = 200ms RTT
With CDN: User in Sydney → CDN edge in Sydney = 2ms RTT
Popular CDN options:
- Cloudflare — Free tier available, excellent DDoS protection
- Fastly — Developer-friendly, real-time purging
- AWS CloudFront — Deep AWS integration
- Vercel Edge Network — Built-in for Vercel deployments
6. Optimize Web Fonts
Fonts can block rendering and cause Flash of Invisible Text (FOIT):
/* Preload critical fonts */
/* In HTML <head>: */
/* <link rel="preload" href="/fonts/inter-400.woff2" as="font" crossorigin /> */
@font-face {
font-family: "Inter";
src: url("/fonts/inter-400.woff2") format("woff2");
font-weight: 400;
font-display: swap; /* Show fallback font immediately */
}
/* Use font-display: optional to prevent layout shift */
@font-face {
font-family: "DisplayFont";
src: url("/fonts/display.woff2") format("woff2");
font-display: optional; /* Only use if font loads fast enough */
}
Variable Fonts
Replace multiple font files with one variable font:
/* ❌ 5 files for 5 weights */
@font-face {
src: url("inter-300.woff2");
font-weight: 300;
}
@font-face {
src: url("inter-400.woff2");
font-weight: 400;
}
@font-face {
src: url("inter-700.woff2");
font-weight: 700;
}
/* ✅ 1 file for all weights */
@font-face {
font-family: "Inter";
src: url("inter-variable.woff2") format("woff2-variations");
font-weight: 100 900;
}
7. Preload and Prefetch Resources
Hint the browser about resources it will need:
<!-- Preload: High priority, will be used soon -->
<link rel="preload" href="/fonts/inter.woff2" as="font" crossorigin />
<link rel="preload" href="/hero-image.avif" as="image" />
<link rel="preload" href="/critical-script.js" as="script" />
<!-- Prefetch: Lower priority, may be needed later -->
<link rel="prefetch" href="/dashboard" />
<!-- DNS Prefetch: Resolve DNS for external domains early -->
<link rel="dns-prefetch" href="https://api.example.com" />
<!-- Preconnect: Establish TCP connection early -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
8. Reduce Server Response Time
If your TTFB (Time to First Byte) is over 200ms, investigate:
Database Query Optimization
// ❌ N+1 query problem
const posts = await db.posts.findMany();
const postsWithAuthors = await Promise.all(
posts.map((post) => db.users.findOne({ id: post.authorId })),
);
// ✅ Join in a single query
const postsWithAuthors = await db.posts.findMany({
include: { author: true },
});
Edge Computing
Deploy compute close to users with edge functions:
// Vercel Edge Function (runs in 35+ regions)
export const config = { runtime: "edge" };
export default async function handler(req) {
const data = await fetch("https://api.example.com/data");
return new Response(JSON.stringify(await data.json()), {
headers: { "Content-Type": "application/json" },
});
}
9. Measure Core Web Vitals
You can’t improve what you don’t measure. Focus on Google’s Core Web Vitals:
| Metric | Good | Needs Improvement | Poor |
|---|---|---|---|
| LCP (Largest Contentful Paint) | < 2.5s | 2.5–4s | > 4s |
| INP (Interaction to Next Paint) | < 200ms | 200–500ms | > 500ms |
| CLS (Cumulative Layout Shift) | < 0.1 | 0.1–0.25 | > 0.25 |
Measure in Code
import { onLCP, onINP, onCLS } from "web-vitals";
onLCP(({ value, rating }) => {
console.log(`LCP: ${value}ms — ${rating}`);
// Send to analytics
analytics.track("web_vital", { metric: "LCP", value, rating });
});
onINP(({ value }) => {
analytics.track("web_vital", { metric: "INP", value });
});
onCLS(({ value }) => {
analytics.track("web_vital", { metric: "CLS", value });
});
10. Use Resource Hints and Priority
The browser has its own heuristics for fetching resources, but you can help it:
<!-- fetchpriority: Control resource priority -->
<!-- The LCP image should load first -->
<img src="hero.jpg" alt="Hero" fetchpriority="high" />
<!-- Below-the-fold images are less important -->
<img src="secondary.jpg" alt="Secondary" fetchpriority="low" loading="lazy" />
<!-- Third-party scripts are usually not critical -->
<script
src="https://widget.example.com/embed.js"
fetchpriority="low"
async
></script>
Optimize Long Tasks
Break up long JavaScript tasks to keep the main thread responsive:
// ❌ One big task that blocks the main thread for 500ms
function processLargeDataset(data) {
return data.map((item) => expensiveTransform(item));
}
// ✅ Yield to browser between chunks
async function processLargeDataset(data) {
const results = [];
for (let i = 0; i < data.length; i++) {
results.push(expensiveTransform(data[i]));
// Yield every 100 items to allow browser to handle events
if (i % 100 === 0) await scheduler.yield();
}
return results;
}
Performance Checklist
Before shipping any page:
- Images are in AVIF/WebP format with proper dimensions
- Images below the fold have
loading="lazy" - LCP image has
fetchpriority="high"and is preloaded - JavaScript is code-split and tree-shaken
- Critical CSS is inlined, non-critical CSS is deferred
- Fonts use
font-display: swaporoptional - Static assets have long-lived cache headers
- Server response time (TTFB) is under 200ms
- No render-blocking scripts in
<head>withoutasync/defer - Core Web Vitals measured and passing thresholds
Conclusion
Web performance is not a one-time task — it’s a continuous discipline. Start with the highest-impact changes (images and JavaScript), measure the results, and iterate. Use tools like Lighthouse, WebPageTest, and Chrome DevTools to guide your efforts.
Remember: every millisecond counts. Your users will notice.