Building Performant Web Applications
Building Performant Web Applications
Performance is crucial for user experience and business success. Slow websites lose users, hurt conversions, and rank lower in search results. Let’s explore comprehensive strategies for building fast, performant web applications.
Understanding Web Performance
Core Web Vitals
Google’s Core Web Vitals are essential metrics for measuring user experience:
- Largest Contentful Paint (LCP) - Loading performance (< 2.5s)
- First Input Delay (FID) - Interactivity (< 100ms)
- Cumulative Layout Shift (CLS) - Visual stability (< 0.1)
Performance Budget
Set performance budgets to maintain fast loading times:
// Example performance budget
const performanceBudget = {
'bundle.js': '250KB',
'vendor.js': '500KB',
'styles.css': '50KB',
'images': '2MB',
'totalPageWeight': '3MB',
'httpRequests': 50
};
Optimization Strategies
1. Code Splitting and Lazy Loading
Split your code to load only what’s needed:
// Dynamic imports for code splitting
const LazyComponent = lazy(() => import('./LazyComponent'));
// Route-based code splitting
const routes = [
{
path: '/',
component: lazy(() => import('./pages/Home'))
},
{
path: '/about',
component: lazy(() => import('./pages/About'))
}
];
// Image lazy loading
<img
src="placeholder.jpg"
data-src="actual-image.jpg"
loading="lazy"
alt="Description"
/>
2. Resource Optimization
Image Optimization
<!-- Responsive images -->
<picture>
<source media="(min-width: 800px)" srcset="large.webp" type="image/webp">
<source media="(min-width: 400px)" srcset="medium.webp" type="image/webp">
<source srcset="small.webp" type="image/webp">
<img src="fallback.jpg" alt="Description" loading="lazy">
</picture>
<!-- Modern image formats -->
<img
src="image.avif"
alt="Description"
width="400"
height="300"
loading="lazy"
decoding="async"
/>
Font Optimization
/* Preload critical fonts */
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
/* Font display strategy */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: swap; /* or fallback, optional */
font-weight: 100 900;
}
/* Variable fonts for smaller file sizes */
@font-face {
font-family: 'Inter Variable';
src: url('/fonts/inter-variable.woff2') format('woff2-variations');
font-weight: 100 900;
font-style: normal;
}
3. Critical Resource Loading
Critical CSS
<!-- Inline critical CSS -->
<style>
/* Above-the-fold styles */
body { font-family: Inter, sans-serif; }
.header { background: #fff; padding: 1rem; }
.hero { min-height: 50vh; }
</style>
<!-- Preload non-critical CSS -->
<link rel="preload" href="/styles/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
Resource Hints
<!-- DNS prefetch for external domains -->
<link rel="dns-prefetch" href="//fonts.googleapis.com">
<link rel="dns-prefetch" href="//api.example.com">
<!-- Preconnect for critical third-party resources -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Prefetch resources for next page -->
<link rel="prefetch" href="/next-page.html">
<!-- Preload critical resources -->
<link rel="preload" href="/hero-image.jpg" as="image">
<link rel="preload" href="/api/critical-data" as="fetch" crossorigin>
JavaScript Performance
Bundle Optimization
// Webpack optimization
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
reuseExistingChunk: true,
},
common: {
name: 'common',
minChunks: 2,
priority: 5,
reuseExistingChunk: true,
}
}
},
usedExports: true,
sideEffects: false
}
};
// Tree shaking example
// Instead of importing entire library
import _ from 'lodash'; // ❌ Imports everything
// Import only what you need
import { debounce } from 'lodash'; // ✅ Tree-shakeable
Runtime Performance
// Efficient DOM manipulation
const fragment = document.createDocumentFragment();
items.forEach(item => {
const element = createElement(item);
fragment.appendChild(element);
});
container.appendChild(fragment); // Single DOM update
// Debounce expensive operations
const debouncedSearch = debounce((query) => {
performSearch(query);
}, 300);
// Use requestAnimationFrame for animations
function animate() {
// Animation logic
requestAnimationFrame(animate);
}
// Optimize event listeners
// Use passive listeners when possible
element.addEventListener('scroll', handler, { passive: true });
// Use event delegation
container.addEventListener('click', (e) => {
if (e.target.matches('.button')) {
handleButtonClick(e);
}
});
Caching Strategies
Browser Caching
// Service Worker caching
self.addEventListener('fetch', (event) => {
if (event.request.destination === 'image') {
event.respondWith(
caches.open('images').then(cache => {
return cache.match(event.request).then(response => {
return response || fetch(event.request).then(fetchResponse => {
cache.put(event.request, fetchResponse.clone());
return fetchResponse;
});
});
})
);
}
});
// HTTP caching headers
const cacheConfig = {
'static-assets': {
'Cache-Control': 'public, max-age=31536000', // 1 year
'Expires': new Date(Date.now() + 31536000000)
},
'html-pages': {
'Cache-Control': 'public, max-age=3600', // 1 hour
'ETag': '"version-hash"'
}
};
Application-Level Caching
// Memory cache with LRU eviction
class LRUCache {
constructor(maxSize = 100) {
this.maxSize = maxSize;
this.cache = new Map();
}
get(key) {
if (this.cache.has(key)) {
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value); // Move to end
return value;
}
return null;
}
set(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key);
} else if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
}
}
// React Query for server state caching
const { data, isLoading } = useQuery(
['posts', page],
() => fetchPosts(page),
{
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
}
);
Network Optimization
HTTP/2 and HTTP/3
// Server Push (HTTP/2)
app.get('/', (req, res) => {
// Push critical resources
res.push('/styles/critical.css');
res.push('/scripts/app.js');
res.push('/images/hero.jpg');
res.sendFile('index.html');
});
// Preload resources for HTTP/2
<link rel="preload" href="/api/data" as="fetch" crossorigin>
Compression
// Gzip/Brotli compression
app.use(compression({
level: 6,
threshold: 1024,
filter: (req, res) => {
return compression.filter(req, res);
}
}));
// Dynamic imports with compression
const loadModule = async (moduleName) => {
const module = await import(
/* webpackChunkName: "[request]" */
/* webpackMode: "lazy" */
`./modules/${moduleName}`
);
return module;
};
Monitoring and Measurement
Performance APIs
// Navigation Timing
const navTiming = performance.getEntriesByType('navigation')[0];
console.log('DNS Lookup:', navTiming.domainLookupEnd - navTiming.domainLookupStart);
console.log('TCP Connect:', navTiming.connectEnd - navTiming.connectStart);
console.log('DOM Ready:', navTiming.domContentLoadedEventEnd - navTiming.navigationStart);
// Resource Timing
const resources = performance.getEntriesByType('resource');
resources.forEach(resource => {
console.log(`${resource.name}: ${resource.duration}ms`);
});
// User Timing
performance.mark('feature-start');
// ... feature implementation
performance.mark('feature-end');
performance.measure('feature-duration', 'feature-start', 'feature-end');
// Long Tasks API
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
console.log('Long task detected:', entry.duration);
});
});
observer.observe({ entryTypes: ['longtask'] });
Web Vitals
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
// Measure Core Web Vitals
getCLS(console.log);
getFID(console.log);
getLCP(console.log);
// Additional metrics
getFCP(console.log); // First Contentful Paint
getTTFB(console.log); // Time to First Byte
// Send to analytics
function sendToAnalytics(metric) {
fetch('/analytics', {
method: 'POST',
body: JSON.stringify(metric),
headers: { 'Content-Type': 'application/json' }
});
}
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);
Tools and Testing
Performance Testing Tools
# Lighthouse CI
npm install -g @lhci/cli
lhci autorun
# WebPageTest API
curl "https://www.webpagetest.org/runtest.php?url=https://example.com&k=API_KEY"
# Bundlephobia for package size analysis
npx bundlephobia package-name
Performance Budget Enforcement
// Webpack Bundle Analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
generateStatsFile: true
})
],
performance: {
maxAssetSize: 250000,
maxEntrypointSize: 250000,
hints: 'error'
}
};
// Size limit with CI integration
// package.json
{
"scripts": {
"size": "size-limit",
"size:why": "size-limit --why"
},
"size-limit": [
{
"path": "dist/app.js",
"limit": "250 KB"
}
]
}
Advanced Techniques
Progressive Web Apps
// Service Worker registration
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => console.log('SW registered'))
.catch(error => console.log('SW registration failed'));
}
// App Shell architecture
const CACHE_NAME = 'app-shell-v1';
const APP_SHELL_FILES = [
'/',
'/styles/app.css',
'/scripts/app.js',
'/images/logo.svg'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(APP_SHELL_FILES))
);
});
Server-Side Rendering
// Next.js with performance optimizations
export async function getStaticProps() {
const data = await fetchData();
return {
props: { data },
revalidate: 3600 // ISR: regenerate every hour
};
}
// Streaming SSR
import { renderToReadableStream } from 'react-dom/server';
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/client.js'],
onError: (error) => console.error(error)
});
return new Response(stream, {
headers: { 'Content-Type': 'text/html' }
});
Conclusion
Building performant web applications requires a holistic approach covering code optimization, resource management, caching strategies, and continuous monitoring.
Key strategies:
- Optimize Critical Rendering Path - Deliver essential content first
- Implement Smart Caching - Reduce unnecessary network requests
- Monitor Real User Metrics - Track actual user experience
- Automate Performance Testing - Catch regressions early
- Set Performance Budgets - Maintain standards over time
Remember: Performance is not a one-time optimization but an ongoing process. Regular monitoring, testing, and optimization ensure your applications remain fast as they grow and evolve.