March 26, 20269 min read

Web Performance: What Actually Makes Your Site Faster

A practical guide to web performance optimization. Covers what matters most, how to measure performance, image optimization, code splitting, caching strategies, Core Web Vitals, and the changes that give you the biggest wins.

performance web-development javascript frontend
Ad 336x280

Web performance advice is everywhere, and most of it is either outdated, irrelevant for your use case, or micro-optimization that doesn't move the needle. Shaving 2 bytes off a CSS selector won't make your site fast. Sending 4 MB of unoptimized images will make it slow. Let's focus on the changes that actually make a difference.

The core principle is simple: send less stuff, send it faster, and don't block the browser from rendering while you do it. Everything else is a variation on this theme.

Measuring First, Optimizing Second

The biggest mistake in performance work is optimizing based on intuition. You think the JavaScript bundle is the bottleneck, but actually it's a 3 MB hero image. You optimize database queries, but the real delay is DNS resolution. Always measure before you fix.

Lighthouse -- built into Chrome DevTools. Open DevTools > Lighthouse tab > Run. Gives you scores for Performance, Accessibility, Best Practices, and SEO, with specific recommendations. Chrome DevTools Performance tab -- records a timeline of exactly what the browser does when loading your page. Shows you where time is spent: parsing HTML, downloading resources, executing JavaScript, painting pixels. WebPageTest (webpagetest.org) -- tests your site from real browsers in real locations. Shows filmstrip views, waterfall charts, and detailed timing breakdowns. More realistic than local testing. Core Web Vitals -- Google's three metrics that matter:
  • LCP (Largest Contentful Paint) -- how long until the biggest visible element renders. Target: under 2.5 seconds.
  • INP (Interaction to Next Paint) -- how long the browser takes to respond after a user interaction. Target: under 200 milliseconds.
  • CLS (Cumulative Layout Shift) -- how much stuff moves around while loading. Target: under 0.1.
These metrics reflect what users actually experience. A fast Time to First Byte means nothing if the page takes 8 seconds to become interactive.

The Biggest Wins (In Order of Impact)

1. Optimize Images

Images are usually the heaviest resources on a page. A single unoptimized photo can be larger than all your JavaScript combined.

Use modern formats. WebP is 25-35% smaller than JPEG at equivalent quality. AVIF is even smaller (30-50% vs JPEG). All modern browsers support both.
<picture>
  <source srcset="hero.avif" type="image/avif" />
  <source srcset="hero.webp" type="image/webp" />
  <img src="hero.jpg" alt="Hero image" width="1200" height="600" />
</picture>
Resize images. Don't serve a 4000x3000 photo when the element is 800px wide. Create multiple sizes and use srcset:
<img
  srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1200.webp 1200w"
  sizes="(max-width: 600px) 400px, (max-width: 1000px) 800px, 1200px"
  src="hero-800.webp"
  alt="Hero image"
  width="1200"
  height="600"
  loading="lazy"
/>
Lazy load off-screen images. loading="lazy" tells the browser to load images only when they're about to scroll into view. Use it on every image except the one visible above the fold. Always specify dimensions. Without width and height, the browser doesn't know how much space to reserve, causing layout shifts (bad CLS). Compress aggressively. Most images look fine at 75-80% quality. Use tools like Sharp, Squoosh, or ImageMagick:
# Convert to WebP at 80% quality
cwebp -q 80 input.jpg -o output.webp

# Using Sharp (Node.js)
sharp("input.jpg").webp({ quality: 80 }).toFile("output.webp");

2. Reduce JavaScript

Every kilobyte of JavaScript is more expensive than a kilobyte of images or CSS. JavaScript must be downloaded, parsed, compiled, and executed -- all of which block interactivity.

Code split. Don't load your entire app upfront. Split by route so users only download the code for the page they're viewing.
// React with lazy loading
import { lazy, Suspense } from "react";

const Dashboard = lazy(() => import("./pages/Dashboard"));
const Settings = lazy(() => import("./pages/Settings"));

function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}

Audit your dependencies. Run npx bundlephobia or check bundlephobia.com before adding a library. moment.js is 300KB; dayjs does the same thing in 7KB. lodash is 70KB; you probably only use 3 functions.
# See what's in your bundle
npx vite-bundle-visualizer    # Vite
npx @next/bundle-analyzer     # Next.js
npx webpack-bundle-analyzer   # Webpack
Tree shake. Import only what you use:
// Bad: imports the entire library
import _ from "lodash";
_.debounce(fn, 300);

// Good: imports only debounce
import debounce from "lodash/debounce";
debounce(fn, 300);

Defer non-critical scripts:
<!-- Blocks rendering -->
<script src="analytics.js"></script>

<!-- Downloads in parallel, executes after HTML parsing -->
<script src="analytics.js" defer></script>

<!-- Downloads in parallel, executes as soon as downloaded -->
<script src="analytics.js" async></script>

Use defer for scripts that need the DOM. Use async for scripts that are independent (analytics, ads).

3. Optimize CSS

CSS blocks rendering -- the browser won't paint anything until it has parsed all CSS in the . Keep your critical CSS small.

Inline critical CSS. The CSS needed for above-the-fold content should be inlined in the . Everything else can load asynchronously.
<head>
  <style>
    / Critical CSS: layout, header, hero section /
    body { margin: 0; font-family: system-ui; }
    .header { height: 64px; background: #fff; }
    .hero { padding: 2rem; }
  </style>
  <link rel="preload" href="/styles/full.css" as="style"
        onload="this.onload=null;this.rel='stylesheet'" />
</head>
Remove unused CSS. Tools like PurgeCSS scan your HTML/JS and strip unused selectors. If you're using Tailwind CSS, it does this automatically in production builds. Prefer system-ui for body text. Custom fonts add network requests and can cause FOIT (Flash of Invisible Text) or FOUT (Flash of Unstyled Text).
/ No network request, renders instantly /
body {
  font-family: system-ui, -apple-system, sans-serif;
}

If you must use custom fonts, use font-display: swap so text is visible immediately:

@font-face {
  font-family: "CustomFont";
  src: url("/fonts/custom.woff2") format("woff2");
  font-display: swap;
}

4. Leverage Caching

Proper caching means returning visitors don't re-download resources they already have.

Set Cache-Control headers:
# Static assets with content hashing (app.a1b2c3.js)
Cache-Control: public, max-age=31536000, immutable

# HTML pages (need to re-validate)
Cache-Control: public, max-age=0, must-revalidate

# API responses (cache for 5 minutes)
Cache-Control: public, max-age=300

The pattern: hash your static assets (most build tools do this automatically), set them to cache forever (they'll get a new URL when they change), and set HTML/API responses to short or no cache so users always get the latest content.

Use a CDN. A CDN serves your static files from servers close to the user. If your server is in Virginia and your user is in Tokyo, a CDN cuts latency from ~200ms to ~20ms. Cloudflare, AWS CloudFront, and Vercel's Edge Network all do this automatically.

5. Optimize Loading Priority

The browser has to decide what to download first. You can help it make better decisions.

Preload critical resources:
<!-- Tell the browser to fetch this early -->
<link rel="preload" href="/fonts/main.woff2" as="font"
      type="font/woff2" crossorigin />
<link rel="preload" href="/hero.webp" as="image" />
Preconnect to external domains:
<!-- Establish connections early for third-party resources -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://api.example.com" />
Avoid render-blocking resources. Move non-critical CSS and JavaScript out of the . Inline the critical bits, defer the rest.

6. Optimize Server Response

Everything above is about what happens in the browser. The server matters too.

Compress responses. Enable gzip or Brotli compression on your server. Brotli compresses 15-20% better than gzip for text content.
# Nginx Brotli compression
brotli on;
brotli_types text/html text/css application/javascript application/json;
Use HTTP/2 or HTTP/3. HTTP/2 multiplexes requests over a single connection, eliminating the need for hacks like domain sharding and sprite sheets. HTTP/3 uses QUIC for even faster connections. Most CDNs and modern servers support both. Reduce Time to First Byte (TTFB). If your server takes 2 seconds to respond, nothing else matters. Common culprits: slow database queries, missing indexes, no caching layer, server too far from users.

What Not to Waste Time On

Minifying HTML. Saves a few hundred bytes. Compression handles this better. Not worth the tooling complexity. Obsessing over CSS selector performance. The difference between .class and .parent .child .class is measured in microseconds. Unless you have 50,000 DOM elements, this isn't your bottleneck. Inlining all JavaScript. Inlining prevents caching. A cached external file is faster on repeat visits than an inlined script that must be re-downloaded with every page. Premature optimization. Don't spend a week optimizing a site that loads in 1.5 seconds. Focus your effort where load times actually hurt users -- mobile on 3G, regions with high latency, pages with heavy content.

A Performance Checklist

When someone asks "how do I make my site faster?", go through this list:

  1. Run Lighthouse. Look at the specific recommendations.
  2. Check images. Are they in modern formats? Properly sized? Lazy loaded?
  3. Check JavaScript bundle size. Can you code-split? Drop heavy dependencies?
  4. Check for render-blocking resources. Is non-critical CSS/JS in the head?
  5. Check caching headers. Are static assets cached with long TTLs?
  6. Check TTFB. Is the server responding quickly?
  7. Check third-party scripts. Analytics, ads, widgets -- each one adds load time.
  8. Are you using a CDN?
In my experience, fixing items 2 and 3 alone handles 80% of performance problems on most sites.

Measuring the Impact

After making changes, quantify the improvement:

# Lighthouse CLI for consistent measurements
npx lighthouse https://yoursite.com --output json --output-path report.json

# Compare before and after
# Run 3-5 times and average the results (Lighthouse scores vary slightly)

Track Core Web Vitals in production with the web-vitals library:

import { onLCP, onINP, onCLS } from "web-vitals";

onLCP((metric) => sendToAnalytics("LCP", metric.value));
onINP((metric) => sendToAnalytics("INP", metric.value));
onCLS((metric) => sendToAnalytics("CLS", metric.value));

Lab measurements (Lighthouse) show potential. Field measurements (real user metrics) show reality. Both matter, but field data tells you what your actual users experience.

If you're building web projects and want to develop the skills that lead to naturally performant code, CodeUp helps you learn the fundamentals that make performance intuition second nature.

Ad 728x90