Speed Is a Feature: Practical Performance Optimization for the Modern Web
When we build a website, we think about design, functionality, and content — but speed is the feature that underpins all of it. A one-second delay in page load can drop conversions by 7%. Google uses Core Web Vitals as a ranking signal. And on mobile, a slow site isn't just annoying — it loses the user entirely. Here's how we approach performance optimization on every project we ship.
Start With What You're Sending
The fastest request is the one you never make. Before touching a single line of optimization code, we audit what's actually being sent down the wire. Oversized images are almost always the biggest culprit. We convert to WebP or AVIF, set explicit dimensions, and let the browser do the rest with the loading attribute:
<img
src="hero.webp"
alt="Team at work"
width="1200"
height="630"
loading="lazy"
decoding="async"
/>
Lazy loading defers off-screen images until the user scrolls near them — no JavaScript required. For above-the-fold images, skip lazy and add fetchpriority="high" instead so the browser prioritizes them early in the load sequence.
Cache Aggressively, Invalidate Cleanly
Caching is one of the highest-leverage tools we have. The goal is to serve static assets from the browser cache on repeat visits while still pushing updates instantly when files change. We do this by fingerprinting filenames at build time and pairing them with long-lived Cache-Control headers.
# Nginx — long-lived cache for fingerprinted assets
location ~* \.(js|css|woff2|webp)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
# Short cache for HTML — always revalidate
location ~* \.html$ {
add_header Cache-Control "no-cache";
}
The immutable directive tells the browser the file will never change at this URL — it won't even send a revalidation request. When we deploy a new build, the filename hash changes, so the browser fetches the new file with no stale-cache risk. HTML stays at no-cache so users always get the latest page, which then references the correct hashed assets.
Cut JavaScript Down to Size
JavaScript is the most expensive resource on most pages — not just to download, but to parse and execute. We follow a few rules on every project:
- Tree-shake ruthlessly. Modern bundlers like Vite and esbuild eliminate dead code automatically, but only if you import named exports rather than entire libraries. Use
import { debounce } from 'lodash-es', notimport _ from 'lodash'. - Code-split by route. Ship only the JavaScript a user actually needs for the page they're on. Dynamic imports handle this cleanly:
const Chart = await import('./Chart.js')loads the charting library only when the dashboard route mounts. - Defer non-critical scripts. Third-party analytics, chat widgets, and ad scripts should never block rendering. Add
deferorasyncto script tags, or load them after theloadevent fires.
Leverage the Network Layer
Even perfectly optimized assets benefit from delivery improvements. We use a CDN with edge locations close to users for all static files, which cuts latency before any code runs. For dynamic responses, ETag and Last-Modified headers let the server return a lightweight 304 Not Modified instead of the full payload when content hasn't changed.
HTTP/2 multiplexing means we no longer need to bundle every CSS file into one — the browser can fetch several small files in parallel over a single connection. We still bundle for parse efficiency, but we don't over-engineer it to work around HTTP/1.1 limits that no longer apply.
For fonts, we self-host rather than loading from Google Fonts. This eliminates a third-party DNS lookup and lets us set our own cache headers. We preload the most critical font variant in the <head> so it doesn't block text rendering:
<link
rel="preload"
href="/fonts/inter-var.woff2"
as="font"
type="font/woff2"
crossorigin
/>
Measure, Then Optimize
We don't guess at performance problems — we measure them. Lighthouse in Chrome DevTools gives us a baseline and flags the highest-impact issues. WebPageTest lets us simulate real-world network conditions and see waterfall charts that reveal exactly where time is being spent. For production monitoring, we track Core Web Vitals — LCP, INP, and CLS — using the browser's PerformanceObserver API or a lightweight RUM library.
The most important habit we've built is running a performance audit before launch, not after a client complains. By the time a slow site ships, the fix often requires rearchitecting the asset pipeline — a much harder conversation than catching it in staging.
Performance Is Ongoing, Not a Checkbox
Every dependency added, every new marketing pixel, every high-resolution image uploaded by a content editor is a potential regression. We build performance budgets into CI pipelines so a pull request that bloats the bundle beyond a threshold fails the build. It keeps performance a team habit rather than a post-launch fire drill.
If your site is slower than it should be — or you're not sure — we'd be glad to take a look. A quick audit often surfaces the two or three changes that account for most of the gains.