Web Performance 101: Making Your Site Fast Without Losing Your Mind
Page speed matters. Users bounce if your site is slow. Google ranks you lower. But performance optimization can feel overwhelming. Let’s focus on the wins that actually matter.
The Metrics That Actually Matter
Forget about all the metrics. Focus on these:
Core Web Vitals:
- LCP (Largest Contentful Paint): < 2.5s
- FID (First Input Delay): < 100ms
- CLS (Cumulative Layout Shift): < 0.1
These are what Google cares about, and they correlate with user experience.
Check them:
- Chrome DevTools > Lighthouse
- https://pagespeed.web.dev
- Real User Monitoring (RUM) data
Quick Win #1: Optimize Images
Images are usually 50%+ of page weight.
Use Modern Formats
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Fallback">
</picture>
WebP is 30% smaller than JPEG. AVIF is even better.
Lazy Load
<img src="hero.jpg" alt="Hero" loading="eager">
<img src="footer.jpg" alt="Footer" loading="lazy">
Only load images when needed. Works in all modern browsers.
Responsive Images
<img
srcset="
image-400.jpg 400w,
image-800.jpg 800w,
image-1200.jpg 1200w
"
sizes="(max-width: 600px) 400px, (max-width: 900px) 800px, 1200px"
src="image-800.jpg"
alt="Responsive"
>
Serve appropriate size based on screen.
Tools
- Squoosh.app - Compress images
- imgix or Cloudinary - CDN with automatic optimization
- next/image - Automatic optimization in Next.js
Quick Win #2: Code Splitting
Don’t ship code users don’t need.
Route-Based Splitting
// Instead of this
import Dashboard from './Dashboard';
import Settings from './Settings';
// Do this
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));
Only load the route they visit.
Component-Based Splitting
// Heavy component used conditionally
const Chart = lazy(() => import('./Chart'));
function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<>
<button onClick={() => setShowChart(true)}>Show Chart</button>
{showChart && (
<Suspense fallback={<div>Loading...</div>}>
<Chart />
</Suspense>
)}
</>
);
}
Quick Win #3: Reduce JavaScript
Less JS = faster page.
Check Your Bundle Size
npm install -g webpack-bundle-analyzer
# In your project
npx webpack-bundle-analyzer dist/stats.json
Visualize what’s taking up space.
Tree Shaking
// ❌ Imports entire library
import _ from 'lodash';
// ✅ Imports only what you need
import debounce from 'lodash/debounce';
Use Smaller Alternatives
date-fnsinstead ofmoment.jspreactinstead ofreact(for simple projects)- Native APIs instead of jQuery
Quick Win #4: Caching Strategy
Make the browser do the work.
Set Cache Headers
# Immutable assets (with hash in filename)
location /static/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# HTML (always revalidate)
location / {
add_header Cache-Control "no-cache";
}
Service Worker (PWA)
// Cache-first strategy
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request);
})
);
});
Quick Win #5: Fonts
Web fonts can block rendering.
Font Display
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom.woff2') format('woff2');
font-display: swap; /* Show fallback immediately */
}
Preload Critical Fonts
<link
rel="preload"
href="/fonts/custom.woff2"
as="font"
type="font/woff2"
crossorigin
>
Use System Fonts
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
Zero network request, instant rendering.
Quick Win #6: Critical CSS
Inline critical CSS in <head> to avoid render-blocking.
<head>
<style>
/* Critical CSS for above-the-fold content */
.hero { ... }
.nav { ... }
</style>
<link rel="stylesheet" href="/styles.css">
</head>
Tools:
- critical npm package
- Critters (built into Next.js)
Quick Win #7: Reduce Third-Party Scripts
Every analytics and tracking script slows you down.
Audit Scripts
# Check what's loading
Chrome DevTools > Network > Filter: JS
Defer Non-Critical Scripts
<!-- Bad -->
<script src="analytics.js"></script>
<!-- Good -->
<script defer src="analytics.js"></script>
<!-- Better -->
<script>
// Load after page is interactive
window.addEventListener('load', () => {
const script = document.createElement('script');
script.src = 'analytics.js';
document.body.appendChild(script);
});
</script>
Self-Host When Possible
<!-- Slower - extra DNS lookup -->
<script src="https://cdn.example.com/lib.js"></script>
<!-- Faster - same origin -->
<script src="/vendor/lib.js"></script>
Quick Win #8: Database & API Optimization
Frontend speed doesn’t matter if the backend is slow.
Add Indexes
-- Find slow queries
EXPLAIN SELECT * FROM users WHERE email = '[email protected]';
-- Add index
CREATE INDEX idx_users_email ON users(email);
Use Caching
// Redis for frequently accessed data
const user = await redis.get(`user:${id}`) ||
await db.getUser(id).then(u => {
redis.set(`user:${id}`, u, 'EX', 3600);
return u;
});
Pagination
// ❌ Don't load everything
const posts = await db.query('SELECT * FROM posts');
// ✅ Paginate
const posts = await db.query(
'SELECT * FROM posts LIMIT 20 OFFSET ?',
[page * 20]
);
Quick Win #9: CDN
Serve static assets from a CDN.
Benefits:
- Faster delivery (closer to users)
- Reduced server load
- Better caching
Options:
- Cloudflare (free!)
- AWS CloudFront
- Vercel (automatic for Next.js)
- Netlify (automatic)
Quick Win #10: Measure Everything
You can’t improve what you don’t measure.
Real User Monitoring
// Web Vitals
import { getCLS, getFID, getLCP } from 'web-vitals';
getCLS(console.log);
getFID(console.log);
getLCP(console.log);
Performance API
// Measure specific operations
performance.mark('fetch-start');
await fetchData();
performance.mark('fetch-end');
performance.measure('fetch-duration', 'fetch-start', 'fetch-end');
const measure = performance.getEntriesByName('fetch-duration')[0];
console.log(`Fetch took ${measure.duration}ms`);
Tools
- Lighthouse CI - Automated performance testing
- SpeedCurve - Historical performance tracking
- Google Analytics - Page load times
The Performance Checklist
Before launch, check:
- Images optimized and lazy loaded
- Code split by route
- Bundle size analyzed and optimized
- Fonts using
font-display: swap - Critical CSS inlined
- Third-party scripts deferred
- Cache headers set correctly
- Using a CDN
- Performance monitoring in place
When to Stop Optimizing
You’re done when:
- Core Web Vitals are green
- Lighthouse score > 90
- Page loads in < 3s on 4G
- Users aren’t complaining
Perfect is the enemy of good. Ship and iterate.
My Performance Stack
- Next.js - Automatic optimization
- Vercel - Edge functions and CDN
- Cloudflare - DNS and additional CDN
- imgix - Image optimization
- Sentry - Performance monitoring
Conclusion
Performance optimization is a journey, not a destination. Start with the quick wins:
- Optimize images
- Split code
- Cache aggressively
- Defer non-critical JS
These alone will get you 80% of the way there.
The remaining 20% requires profiling and targeted fixes. But most sites never need to go that deep.
Now go make your site fast! 🚀