Frontend performance is critical for user experience and SEO. This guide covers comprehensive optimization techniques for modern web applications.
Core Web Vitals
LCP (Largest Contentful Paint)
Measures loading performance - when is the largest content visible?
<!-- Optimize LCP -->
<!-- Preload hero image -->
<link rel="preload" as="image" href="hero.webp">
<!-- Preload critical fonts -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<!-- Inline critical CSS -->
<style>
/* Critical CSS only */
header { ... }
.hero { ... }
</style>
FID (First Input Delay)
Measures interactivity - how quickly can users interact?
// Defer non-critical JavaScript
<script defer src="analytics.js"></script>
<script async src="ads.js"></script>
// Break up long tasks
function processItems(items) {
const chunkSize = 10;
function processChunk(start) {
const end = Math.min(start + chunkSize, items.length);
for (let i = start; i < end; i++) {
processItem(items[i]);
}
if (end < items.length) {
// Schedule next chunk
setTimeout(() => processChunk(end), 0);
}
}
processChunk(0);
}
CLS (Cumulative Layout Shift)
Measures visual stability - does content shift unexpectedly?
/* Reserve space for images */
img {
width: 100%;
height: auto;
aspect-ratio: 16 / 9;
}
/* Reserve space for ads/banners */
.ad-banner {
min-height: 250px;
background: #f0f0f0;
}
/* Use font-display: optional or swap */
@font-face {
font-family: 'MyFont';
src: url('font.woff2') format('woff2');
font-display: swap;
}
Code Splitting
Dynamic Imports
// Instead of
import { HeavyComponent } from './HeavyComponent';
// Use
const HeavyComponent = React.lazy(() =>
import('./HeavyComponent')
);
// With suspense
function App() {
return (
<Suspense fallback={<Loading />}>
<HeavyComponent />
</Suspense>
);
}
Route-Based Splitting
// React Router
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Profile = lazy(() => import('./pages/Profile'));
function App() {
return (
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/profile" element={<Profile />} />
</Routes>
);
}
Webpack Code Splitting
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
common: {
minChunks: 2,
priority: -10,
reuseExistingChunk: true,
}
}
}
}
};
Image Optimization
Responsive Images
<img
srcset="image-400.jpg 400w,
image-800.jpg 800w,
image-1200.jpg 1200w"
sizes="(max-width: 600px) 400px,
(max-width: 1200px) 800px,
1200px"
src="image-800.jpg"
alt="Description"
loading="lazy"
decoding="async"
>
Modern Formats
<!-- Use WebP with fallbacks -->
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Description" loading="lazy">
</picture>
Lazy Loading
// Native lazy loading
<img src="image.jpg" loading="lazy" alt="...">
// Intersection Observer for background images
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const el = entry.target;
el.style.backgroundImage = `url(${el.dataset.src})`;
observer.unobserve(el);
}
});
});
document.querySelectorAll('[data-src]').forEach(el => {
observer.observe(el);
});
JavaScript Optimization
Tree Shaking
// webpack.config.js
module.exports = {
mode: 'production',
optimization: {
usedExports: true,
}
};
// Use ES modules - webpack will tree shake
import { cloneDeep, debounce } from 'lodash';
// Bad - imports everything
import _ from 'lodash';
// Good - named imports
import debounce from 'lodash/debounce';
import cloneDeep from 'lodash/cloneDeep';
// Better - use specific libraries
import debounce from 'debounce';
import cloneDeep from 'lodash-es/cloneDeep';
Compression
// Gzip or Brotli
// Server configuration (nginx)
gzip on;
gzip_types text/plain text/css application/json
application/javascript text/xml application/xml;
// Brotli (better compression)
brotli on;
brotli_types text/plain text/css application/json
application/javascript text/xml;
Minification
// webpack production build automatically minifies
// terser for JS, css-minimizer for CSS
// package.json
{
"scripts": {
"build": "webpack --mode production",
"build:analyze": "webpack --mode production --analyze"
}
}
CSS Optimization
Critical CSS
<!-- Inline critical CSS in <head> -->
<head>
<style>
/* Only styles needed for above-the-fold content */
header { background: white; }
.hero { min-height: 100vh; }
.nav { display: flex; }
</style>
<!-- Defer non-critical CSS -->
<link rel="preload" href="styles.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<noscript>
<link rel="stylesheet" href="styles.css">
</noscript>
</head>
Remove Unused CSS
// webpack.config.js with purgecss
const PurgeCSSPlugin = require('purgecss-webpack-plugin');
const glob = require('glob');
module.exports = {
plugins: [
new PurgeCSSPlugin({
paths: glob.sync([
path.join(__dirname, 'src/**/*'),
path.join(__dirname, 'public/**/*.html'),
]),
safelist: {
standard: [/^modal/, /^nav-/],
}
})
]
};
Caching Strategies
Service Worker Caching
// sw.js
const CACHE_NAME = 'v1';
const urlsToCache = ['/', '/index.html', '/styles.css', '/script.js'];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then(response => {
// Return cache or fetch
return response || fetch(event.request);
})
);
});
// Cache-first for static assets
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/static/')) {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
);
}
});
HTTP Caching
# .htaccess / Apache
<IfModule mod_expires.c>
ExpiresActive On
# Images - 1 year
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
# CSS/JS - 1 month
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
# HTML - no cache
ExpiresByType text/html "access plus 0 seconds"
</IfModule>
Bundle Size Analysis
Source Map Explorer
// Install
npm install --save-dev source-map-explorer
// Usage
{
"scripts": {
"analyze": "source-map-explorer build/**/*.js"
}
}
// webpack configuration
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
.BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: 'bundle-report.html'
})
]
};
Performance Budgets
// webpack.config.js
module.exports = {
performance: {
maxEntrypointSize: 512000,
maxAssetSize: 512000,
hints: 'warning'
}
};
// Or in package.json
{
"name": "my-app",
"size-limit": [
{
"path": "dist/**/*.js",
"limit": "500 kB"
}
]
}
Performance Monitoring
Web Vitals Library
import { getCLS, getFID, getLCP } from 'web-vitals';
function sendToAnalytics({ name, delta, id }) {
ga('send', 'event', {
eventCategory: 'Web Vitals',
eventAction: name,
eventValue: Math.round(name === 'CLS' ? delta * 1000 : delta),
eventLabel: id,
nonInteraction: true,
});
}
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);
React Profiler
function App() {
const handleRender = (id, phase, actualDuration) => {
console.log(`${id} ${phase}: ${actualDuration}ms`);
};
return (
<Profiler id="App" onRender={handleRender}>
<Router>
<Routes>
<Route path="/" element={<Home />} />
</Routes>
</Router>
</Profiler>
);
}
Quick Wins
<!-- 1. Preconnect to origins -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- 2. Prefetch critical resources -->
<link rel="prefetch" href="/page.js">
<!-- 3. DNS prefetch -->
<link rel="dns-prefetch" href="https://api.example.com">
<!-- 4. Defer JavaScript -->
<script src="app.js" defer></script>
<!-- 5. Async JavaScript -->
<script src="analytics.js" async></script>
Summary
Key performance optimizations:
-
Core Web Vitals
- LCP: Preload critical resources
- FID: Defer JavaScript
- CLS: Reserve space, stable fonts
-
Code Splitting
- Route-based splitting
- Dynamic imports
- Tree shaking
-
Images
- Responsive images
- Modern formats (WebP, AVIF)
- Lazy loading
-
Caching
- Service workers
- HTTP caching
- CDN
-
Monitoring
- Web Vitals
- Real user monitoring
- Performance budgets
Optimize early and monitor continuously!
Comments