Performance Optimization: Making the Web Faster
Practical strategies and techniques for optimizing web application performance, from bundle splitting to lazy loading.
Piotr Wislowski
Performance Optimization: Making the Web Faster
Web performance isn’t just about making your site feel snappy—it directly impacts user experience, conversion rates, and search engine rankings. Every millisecond matters. Users expect fast-loading pages, and Google’s Core Web Vitals have made performance a ranking factor.
Why Performance Matters
The statistics are compelling:
- 53% of mobile users abandon sites that take longer than 3 seconds to load
- 100ms delay can hurt conversion rates by up to 7%
- 2-second delay in load time increases bounce rates by 103%
Core Web Vitals: The New Standard
Google’s Core Web Vitals focus on three key metrics:
Largest Contentful Paint (LCP)
Measures loading performance. Should occur within 2.5 seconds of when the page first starts loading.
// Measure LCP
new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
const lastEntry = entries[entries.length - 1];
console.log('LCP:', lastEntry.startTime);
}).observe({ type: 'largest-contentful-paint', buffered: true }); First Input Delay (FID)
Measures interactivity. Should be less than 100 milliseconds.
// Measure FID
new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
entries.forEach((entry) => {
console.log('FID:', entry.processingStart - entry.startTime);
});
}).observe({ type: 'first-input', buffered: true }); Cumulative Layout Shift (CLS)
Measures visual stability. Should maintain a CLS of less than 0.1.
// Measure CLS
let clsValue = 0;
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
if (!entry.hadRecentInput) {
clsValue += entry.value;
}
}
console.log('CLS:', clsValue);
}).observe({ type: 'layout-shift', buffered: true }); Loading Performance Optimization
Code Splitting
Split your JavaScript bundles to load only what’s needed:
// Route-based splitting
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));
function App() {
return (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</Suspense>
</Router>
);
}
// Component-based splitting
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function Page() {
const [showHeavy, setShowHeavy] = useState(false);
return (
<div>
<button onClick={() => setShowHeavy(true)}>
Load Heavy Component
</button>
{showHeavy && (
<Suspense fallback={<div>Loading component...</div>}>
<HeavyComponent />
</Suspense>
)}
</div>
);
} Resource Prioritization
Use resource hints to optimize loading:
<!-- Preload critical resources -->
<link rel="preload" href="/fonts/critical.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/critical.css" as="style">
<link rel="preload" href="/hero-image.jpg" as="image">
<!-- Prefetch resources for next page -->
<link rel="prefetch" href="/about.js">
<link rel="prefetch" href="/contact.js">
<!-- DNS prefetch for external domains -->
<link rel="dns-prefetch" href="//fonts.googleapis.com">
<link rel="dns-prefetch" href="//api.example.com">
<!-- Preconnect to critical third parties -->
<link rel="preconnect" href="//fonts.gstatic.com" crossorigin> Image Optimization
Images often account for the majority of page weight:
<!-- Use modern formats with fallbacks -->
<picture>
<source srcset="hero.avif" type="image/avif">
<source srcset="hero.webp" type="image/webp">
<img src="hero.jpg" alt="Hero image" loading="lazy">
</picture>
<!-- Responsive images -->
<img
srcset="
small.jpg 300w,
medium.jpg 600w,
large.jpg 1200w
"
sizes="
(max-width: 300px) 100vw,
(max-width: 600px) 50vw,
33vw
"
src="medium.jpg"
alt="Responsive image"
loading="lazy"
> Service Worker Caching
Implement strategic caching with service workers:
// service-worker.js
const CACHE_NAME = 'app-cache-v1';
const STATIC_ASSETS = [
'/',
'/styles.css',
'/app.js',
'/offline.html'
];
// Install event - cache static assets
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(STATIC_ASSETS))
.then(() => self.skipWaiting())
);
});
// Fetch event - serve from cache, fallback to network
self.addEventListener('fetch', (event) => {
if (event.request.destination === 'image') {
// Cache-first strategy for images
event.respondWith(
caches.match(event.request)
.then(response => {
return response || fetch(event.request)
.then(fetchResponse => {
const responseClone = fetchResponse.clone();
caches.open(CACHE_NAME)
.then(cache => cache.put(event.request, responseClone));
return fetchResponse;
});
})
);
} else if (event.request.url.includes('/api/')) {
// Network-first strategy for API calls
event.respondWith(
fetch(event.request)
.then(response => {
if (response.ok) {
const responseClone = response.clone();
caches.open(CACHE_NAME)
.then(cache => cache.put(event.request, responseClone));
}
return response;
})
.catch(() => caches.match(event.request))
);
}
}); Runtime Performance Optimization
Efficient JavaScript
Write performant JavaScript that doesn’t block the main thread:
// Use requestAnimationFrame for animations
function smoothAnimation() {
let start = null;
function animate(timestamp) {
if (!start) start = timestamp;
const progress = timestamp - start;
// Update animation
element.style.transform = `translateX(${progress / 10}px)`;
if (progress < 2000) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
}
// Debounce expensive operations
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
const expensiveSearch = debounce((query) => {
// Expensive search operation
performSearch(query);
}, 300);
// Use Web Workers for heavy computations
// main.js
const worker = new Worker('heavy-computation.js');
worker.postMessage({ data: largeDataset });
worker.onmessage = (event) => {
console.log('Result:', event.data);
};
// heavy-computation.js
self.onmessage = function(event) {
const result = heavyComputation(event.data);
self.postMessage(result);
}; Memory Management
Prevent memory leaks and optimize memory usage:
// Clean up event listeners
class Component {
constructor() {
this.handleClick = this.handleClick.bind(this);
this.handleResize = this.handleResize.bind(this);
}
mount() {
document.addEventListener('click', this.handleClick);
window.addEventListener('resize', this.handleResize);
}
unmount() {
// Always clean up!
document.removeEventListener('click', this.handleClick);
window.removeEventListener('resize', this.handleResize);
}
handleClick(event) {
// Handle click
}
handleResize(event) {
// Handle resize
}
}
// Use WeakMap for private data to avoid memory leaks
const privateData = new WeakMap();
class MyClass {
constructor() {
privateData.set(this, { sensitive: 'data' });
}
getPrivateData() {
return privateData.get(this);
}
}
// Clear intervals and timeouts
class Timer {
start() {
this.intervalId = setInterval(() => {
console.log('Timer tick');
}, 1000);
}
stop() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
} CSS Performance
Critical CSS Inlining
Load critical styles immediately and defer non-critical CSS:
<head>
<style>
/* Critical CSS inlined here */
body { font-family: system-ui; }
.hero { background: #007bff; color: white; }
</style>
<!-- Load non-critical CSS asynchronously -->
<link rel="preload" href="/non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/non-critical.css"></noscript>
</head> Efficient CSS Selectors
Write performant CSS selectors:
/* ❌ Inefficient - complex selectors */
body div.container ul li a.active {
color: red;
}
/* ✅ Efficient - simple, specific selectors */
.active-link {
color: red;
}
/* ❌ Avoid universal selectors */
* {
box-sizing: border-box;
}
/* ✅ Be more specific */
html {
box-sizing: border-box;
}
*, *::before, *::after {
box-sizing: inherit;
} CSS Containment
Use CSS containment to limit reflow and repaint:
.card {
/* Contain layout and style recalculations */
contain: layout style;
}
.isolated-component {
/* Contain everything */
contain: strict;
}
/* Use content-visibility for off-screen content */
.lazy-section {
content-visibility: auto;
contain-intrinsic-size: 200px;
} Network Performance
HTTP/2 and HTTP/3 Optimization
Leverage modern protocol features:
// Server push (HTTP/2)
// Push critical resources before they're requested
app.get('/', (req, res) => {
// Push CSS and JS
res.push('/critical.css');
res.push('/app.js');
res.render('index');
});
// Multiplexing allows many requests over one connection
// No need to concatenate files in HTTP/2+ Compression
Enable proper compression:
// Node.js with compression middleware
const compression = require('compression');
app.use(compression({
level: 6,
threshold: 1024,
filter: (req, res) => {
if (req.headers['x-no-compression']) {
return false;
}
return compression.filter(req, res);
}
}));
// Brotli compression (better than gzip)
app.use(compression({
brotli: {
enabled: true,
zlib: {}
}
})); Monitoring and Measurement
Real User Monitoring (RUM)
Track actual user performance:
// Basic RUM implementation
function sendPerformanceData() {
if ('performance' in window) {
const navigation = performance.getEntriesByType('navigation')[0];
const paint = performance.getEntriesByType('paint');
const metrics = {
// Navigation timing
dns: navigation.domainLookupEnd - navigation.domainLookupStart,
connection: navigation.connectEnd - navigation.connectStart,
request: navigation.responseStart - navigation.requestStart,
response: navigation.responseEnd - navigation.responseStart,
dom: navigation.domContentLoadedEventEnd - navigation.responseEnd,
// Paint timing
fcp: paint.find(p => p.name === 'first-contentful-paint')?.startTime,
// Custom metrics
timeToInteractive: Date.now() - performance.timing.navigationStart
};
// Send to analytics
fetch('/api/performance', {
method: 'POST',
body: JSON.stringify(metrics),
headers: { 'Content-Type': 'application/json' }
});
}
}
// Send data when page is about to unload
window.addEventListener('beforeunload', sendPerformanceData); Performance Budget
Set and monitor performance budgets:
// webpack-bundle-analyzer configuration
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'bundle-report.html'
})
],
performance: {
maxAssetSize: 250000,
maxEntrypointSize: 250000,
hints: 'error'
}
};
// Lighthouse CI for automated monitoring
// lighthouserc.js
module.exports = {
ci: {
collect: {
url: ['http://localhost:3000'],
numberOfRuns: 3
},
assert: {
assertions: {
'categories:performance': ['error', { minScore: 0.8 }],
'categories:accessibility': ['error', { minScore: 0.9 }],
'first-contentful-paint': ['error', { maxNumericValue: 2000 }],
'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }]
}
}
}
}; Framework-Specific Optimizations
React Performance
Optimize React applications:
// Use React.memo for component optimization
const ExpensiveComponent = React.memo(({ data, onAction }) => {
return <div>{/* Complex rendering */}</div>;
}, (prevProps, nextProps) => {
// Custom comparison
return prevProps.data.id === nextProps.data.id;
});
// Use useMemo and useCallback
function OptimizedComponent({ items, filter }) {
const filteredItems = useMemo(() => {
return items.filter(item => item.category === filter);
}, [items, filter]);
const handleItemClick = useCallback((id) => {
console.log('Clicked item:', id);
}, []);
return (
<div>
{filteredItems.map(item => (
<Item key={item.id} item={item} onClick={handleItemClick} />
))}
</div>
);
}
// Virtualize long lists
import { FixedSizeList as List } from 'react-window';
function VirtualizedList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
);
return (
<List
height={600}
itemCount={items.length}
itemSize={35}
>
{Row}
</List>
);
} SvelteKit Performance
Leverage SvelteKit’s built-in optimizations:
// Preload data on hover
<a href="/slow-page" data-sveltekit-preload-data="hover">
Slow Page
</a>
// Disable JavaScript for static pages
export const csr = false;
export const prerender = true;
// Optimize images
import { enhance } from '$app/forms';
<img
src="/image.jpg"
alt="Description"
loading="lazy"
decoding="async"
/> Performance Testing Tools
Automated Testing
Integrate performance testing into your CI/CD:
// Playwright performance test
const { test, expect } = require('@playwright/test');
test('page loads within performance budget', async ({ page }) => {
await page.goto('https://example.com');
const performanceMetrics = await page.evaluate(() => {
return JSON.stringify(performance.getEntriesByType('navigation')[0]);
});
const metrics = JSON.parse(performanceMetrics);
const loadTime = metrics.loadEventEnd - metrics.navigationStart;
expect(loadTime).toBeLessThan(3000); // Page should load in under 3 seconds
});
// Puppeteer performance monitoring
const puppeteer = require('puppeteer');
async function auditPage(url) {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle2' });
const metrics = await page.metrics();
const performanceTiming = JSON.parse(
await page.evaluate(() => JSON.stringify(performance.timing))
);
console.log('Performance Metrics:', {
...metrics,
loadTime: performanceTiming.loadEventEnd - performanceTiming.navigationStart
});
await browser.close();
} Conclusion
Web performance optimization is an ongoing process that requires attention at every stage of development. Key takeaways:
- Measure first - Use tools like Lighthouse, WebPageTest, and RUM to establish baselines
- Optimize loading - Implement code splitting, lazy loading, and resource prioritization
- Minimize main thread work - Use Web Workers and efficient algorithms
- Cache strategically - Implement service workers and HTTP caching
- Monitor continuously - Set up performance budgets and automated monitoring
Remember: performance is a feature, not an afterthought. Build performance considerations into your development workflow from day one, and your users will thank you with better engagement, conversion rates, and overall satisfaction.
The web is fast by default—it’s our job as developers to keep it that way.