Why Performance Matters
I built a beautiful portfolio with smooth animations, high-res images, custom fonts. On 3G? 12-second load time. Hero image took 6 seconds. Interactive elements took 9 seconds.
Performance isn’t a feature. It’s a requirement.
Business impact:
- 53% of mobile users abandon sites loading >3 seconds
- 100ms delay costs Amazon 1% in sales
- 2-second delay increases bounce rates by 103%
Core Web Vitals (3 Metrics That Matter)
1. Largest Contentful Paint (LCP) - Loading
- Target: <2.5 seconds
- Fix: Optimize images, reduce server response time, eliminate render-blocking JS/CSS
2. Interaction to Next Paint (INP) - Responsiveness
- Target: <200ms
- Fix: Eliminate heavy JS, code splitting, defer non-critical work
3. Cumulative Layout Shift (CLS) - Stability
- Target: <0.1
- Fix: Set image dimensions, avoid injecting content without space, web fonts with font-display: swap
Image Optimization: The Biggest Win
Images are 50-80% of page weight. Optimize them and you’ve won half the battle.
<!-- Modern: WebP/AVIF with fallback -->
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Description" width="400" height="300">
</picture>
<!-- Lazy load below-the-fold images -->
<img src="below-fold.jpg" loading="lazy">
Size savings: JPEG 100KB → WebP 50KB → AVIF 30KB.
JavaScript Optimization
Code splitting: Don’t ship entire app on first load.
// Lazy load heavy components
const Chart = lazy(() => import('./chart-library'));
function Dashboard() {
return (
<Suspense fallback={<Loading />}>
{showChart && <Chart />}
</Suspense>
);
}
Tree shaking: Import only what you need.
// Bad: import lodash@70KB
import _ from 'lodash';
const debouncedFn = _.debounce(fn, 300);
// Good: import debounce@2KB
import debounce from 'lodash/debounce';
Bundle analysis: Use bundle analyzer to find bloat.
Font & CSS Optimization
Fonts: Use font-display: swap to show fallback immediately. Preload critical fonts. Subset fonts to used characters.
CSS: Remove unused CSS with PurgeCSS. Inline critical CSS for above-the-fold. Use CSS containment to scope recalculations.
Caching Strategy
Set proper cache headers:
- Static assets:
Cache-Control: public, immutable, 1 year - API responses:
Cache-Control: no-cache
Use Service Workers to cache assets and enable offline support.
Performance Monitoring
Lighthouse: Built into Chrome DevTools. Run audits regularly.
Real User Monitoring (RUM):
import { getCLS, getLCP, getFID } from 'web-vitals';
function sendMetrics(metric) {
fetch('/analytics', { method: 'POST', body: JSON.stringify(metric) });
}
getCLS(sendMetrics);
getLCP(sendMetrics);
getFID(sendMetrics);
Performance budget: Set hard limits and fail builds exceeding them.
Quick Wins (Do These Today)
- Enable Gzip/Brotli compression
- Use WebP/AVIF images
- Add
widthandheightto images (fixes CLS) - Minify CSS and JavaScript
- Remove unused dependencies
- Use CDN for static assets
- Lazy load images (
loading="lazy") - Defer non-critical JavaScript
- Use
font-display: swap - Run Lighthouse and fix issues
- Set up monitoring
The 80/20 Rule
If you only do these 5 things, you’ll be faster than 80% of sites:
- Optimize images (WebP, lazy loading, responsive)
- Code split (don’t ship everything upfront)
- Use a CDN (edge locations)
- Set cache headers (don’t re-download)
- Measure and monitor (catch regressions)
Common Mistakes
- Optimizing wrong things (spend a week on 10KB JS when images are 2MB)
- Over-optimizing (50ms gains nobody notices)
- Not measuring (“I think this is faster” isn’t strategy)
- Ignoring mobile (test on slow devices and 3G)
Tools
- Lighthouse: Audits
- WebPageTest: Detailed analysis
- Chrome DevTools: Bottleneck identification
- Bundle analyzers: Find bloat
- web.dev/measure: Quick Core Web Vitals
Performance is continuous, not one-time:
- Measure (know baseline)
- Optimize (fix biggest problems)
- Monitor (catch regressions)
- Repeat
Users won’t remember your fancy animations. They’ll remember if your site felt fast.
Make it fast.