June 30, 2025
12 min read
Web Performance Optimization: The Changes That Actually Matter
Not all performance optimizations are equal. Some save milliseconds. These ones save seconds.
Performance optimization can be a rabbit hole. You can spend weeks shaving milliseconds off already-fast operations while users are waiting 4 seconds for your page to render because of an uncompressed hero image. The trick is knowing which optimizations actually move the needle. Here are the high-impact ones, in roughly the order you should do them.
Measure first
Before you optimize anything, measure. Run Lighthouse in Chrome DevTools. Check your Core Web Vitals in Google Search Console. Look at real user data in your analytics tool. Optimization without measurement is guessing.
The three Core Web Vitals that Google uses for ranking: Largest Contentful Paint (LCP) - how fast the main content appears, should be under 2.5 seconds. Interaction to Next Paint (INP) - how responsive the page is to user input, should be under 200ms. Cumulative Layout Shift (CLS) - how much the layout jumps around as the page loads, should be under 0.1.
Images (the biggest win, every time)
Unoptimized images are the number one cause of slow websites. A single 3MB JPEG hero image costs more than your entire JavaScript bundle.
- Convert to WebP or AVIF. WebP is 25-35% smaller than JPEG at the same quality. AVIF is even smaller
- Serve responsive sizes with srcset and the sizes attribute. Don't send a 2400px image to a phone with a 390px screen
- Lazy-load images below the fold with loading="lazy". Don't lazy-load the hero image
- Set explicit width and height attributes to prevent layout shift as images load
- Use Next.js Image component or a similar optimization layer that handles this automatically
- Use a CDN. Serving images from S3 in us-east-1 to a user in Tokyo adds hundreds of milliseconds
Reduce JavaScript bundle size
After images, JavaScript is usually the second-biggest performance problem. Every kilobyte of JS has to be downloaded, parsed, and executed before the page is interactive.
- Code split by route - users shouldn't download the admin panel JS on the landing page
- Lazy load heavy components (charts, maps, rich text editors) until they're needed
- Audit your dependencies - run npx bundlephobia or check bundle size in your build output. One bloated library (like moment.js at 300KB) can add more than all your application code
- Use tree-shaking-friendly imports. import { debounce } from 'lodash-es' instead of import _ from 'lodash'
- Check for duplicate packages in your bundle. Two versions of the same library is more common than you'd think
Server-side rendering and static generation
For content-heavy pages, server-side rendering (SSR) or static generation (SSG) gives users content immediately instead of showing a blank page while JavaScript loads. The difference between a 3-second blank page and instant content is the difference between a bounce and a conversion.
Next.js makes this straightforward. Use static generation for pages that don't change often (marketing pages, blog posts). Use server-side rendering for pages with dynamic, user-specific content. Use streaming SSR to show content progressively as data loads.
Fonts
Custom fonts are a common source of layout shift and render blocking. Use font-display: swap so text is visible immediately (in a fallback font) while the custom font loads. Self-host fonts instead of loading from Google Fonts - it eliminates a DNS lookup and connection to an external server. Subset your fonts to only include the characters you need.
Caching
Set proper cache headers for static assets. Use content-hashed filenames (main.a3f8b2.js) with long cache durations (1 year). The browser will cache the file, and when you deploy a new version, the filename changes so users get the new file automatically. Use a CDN for global distribution. Cache API responses where freshness isn't critical.
Database queries
If your server response time is slow, the database is usually the bottleneck. Add indexes on columns you filter and sort by. Use EXPLAIN to check query plans for slow queries. Avoid N+1 queries (fetching a list, then making a separate query for each item). Use connection pooling so you're not opening a new database connection for every request.
The diminishing returns point
Once your LCP is under 2 seconds and your page weight is under 500KB, you're in good shape. Further optimization has diminishing returns unless you have specific performance requirements (real-time apps, high-traffic pages, mobile-first markets with slow connections). Focus on features, not on shaving another 50ms off an already-fast page.

Ben Arledge
CEO & CTO, CloudOwlNeed help building this?
No sales pitch, just an honest conversation about what you're building.
See our AI capabilities, React/Next.js work, or full service list.
