Web performance has never been more crucial. With users expecting near-instant loading times and smooth interactions, optimizing your web application's performance is no longer optional. In this comprehensive guide, we'll explore modern techniques to boost your web app's performance.
// Measure LCP
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
console.log('LCP:', entry.startTime);
}
}).observe({ entryTypes: ['largest-contentful-paint'] });
// Measure FID
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
console.log('FID:', entry.processingStart - entry.startTime);
}
}).observe({ entryTypes: ['first-input'] });
// Measure CLS
let cls = 0;
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
if (!entry.hadRecentInput) {
cls += entry.value;
}
}
}).observe({ entryTypes: ['layout-shift'] });
// React lazy loading example
const HomePage = React.lazy(() => import('./pages/Home'));
const AboutPage = React.lazy(() => import('./pages/About'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
</Routes>
</Suspense>
);
}
// Bad - imports entire library
import _ from 'lodash';
// Good - imports only what's needed
import { debounce } from 'lodash/debounce';
# Using source-map-explorer
npm install source-map-explorer
source-map-explorer dist/main.js
# Using webpack-bundle-analyzer
npm install webpack-bundle-analyzer
<source type="image/webp" srcset="image.webp">
<source type="image/avif" srcset="image.avif">
<img src="image.jpg" alt="Optimized image" loading="lazy">
</picture>
srcset="
image-300.jpg 300w,
image-600.jpg 600w,
image-900.jpg 900w"
sizes="(max-width: 600px) 300px,
(max-width: 900px) 600px,
900px"
src="image-900.jpg"
alt="Responsive image"
loading="lazy"
>
const CACHE_NAME = 'app-cache-v1';
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll([
'/',
'/styles/main.css',
'/scripts/app.js',
'/images/logo.png'
]);
})
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});
location /static/ {
expires 1y;
add_header Cache-Control "public, no-transform";
}
location /api/ {
add_header Cache-Control "no-cache";
proxy_pass http://api_backend;
}
const worker = new Worker('worker.js');
worker.postMessage({ data: complexData });
worker.onmessage = (event) => {
console.log('Processed data:', event.data);
};
// worker.js
self.onmessage = (event) => {
const result = performHeavyComputation(event.data);
self.postMessage(result);
};
componentDidMount() {
// Bad - potential memory leak
window.addEventListener('resize', this.handleResize);
// Good - clean up listeners
this.handleResize = this.handleResize.bind(this);
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
}
}
app.get('/api/posts', (req, res) => {
const { page = 1, limit = 10 } = req.query;
const skip = (page - 1) * limit;
const posts = await Post.find()
.skip(skip)
.limit(limit)
.select('title excerpt') // Select only needed fields
.lean(); // Convert to plain objects
res.json(posts);
});
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
staleTime: 5 * 60 * 1000, // 5 minutes
prefetchOnHover: true
});
import Image from 'next/image';
function ProductCard({ product }) {
return (
<Image
src={product.image}
alt={product.name}
width={300}
height={300}
placeholder="blur"
blurDataURL={product.thumbnailUrl}
/>
);
}
const ProductDetails = dynamic(() => import('@/components/ProductDetails'), {
loading: () => <ProductSkeleton />,
ssr: false
});
const getProduct = async (id) => {
const cacheKey = `product:${id}`;
let product = await redis.get(cacheKey);
if (!product) {
product = await db.products.findUnique({ where: { id } });
await redis.set(cacheKey, JSON.stringify(product), 'EX', 3600);
}
return product;
};
import { onCLS, onFID, onLCP } from 'web-vitals';
function sendToAnalytics({ name, delta, value, id }) {
analytics.send({
metric: name,
value: delta,
eventId: id
});
}
onCLS(sendToAnalytics);
onFID(sendToAnalytics);
onLCP(sendToAnalytics);
Understanding Core Web Vitals
Before diving into optimization techniques, let's understand what we're measuring:Largest Contentful Paint (LCP)
Target: Under 2.5 seconds// Measure LCP
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
console.log('LCP:', entry.startTime);
}
}).observe({ entryTypes: ['largest-contentful-paint'] });
First Input Delay (FID)
Target: Under 100ms// Measure FID
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
console.log('FID:', entry.processingStart - entry.startTime);
}
}).observe({ entryTypes: ['first-input'] });
Cumulative Layout Shift (CLS)
Target: Under 0.1// Measure CLS
let cls = 0;
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
if (!entry.hadRecentInput) {
cls += entry.value;
}
}
}).observe({ entryTypes: ['layout-shift'] });
Bundle Size Optimization
Code Splitting
Modern bundlers like Webpack and Vite make it easy to split your code:// React lazy loading example
const HomePage = React.lazy(() => import('./pages/Home'));
const AboutPage = React.lazy(() => import('./pages/About'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
</Routes>
</Suspense>
);
}
Tree Shaking
Ensure your bundler can eliminate unused code:// Bad - imports entire library
import _ from 'lodash';
// Good - imports only what's needed
import { debounce } from 'lodash/debounce';
Module Analysis
Use tools to analyze your bundle:# Using source-map-explorer
npm install source-map-explorer
source-map-explorer dist/main.js
# Using webpack-bundle-analyzer
npm install webpack-bundle-analyzer
Image Optimization
Modern Image Formats
<picture><source type="image/webp" srcset="image.webp">
<source type="image/avif" srcset="image.avif">
<img src="image.jpg" alt="Optimized image" loading="lazy">
</picture>
Responsive Images
<imgsrcset="
image-300.jpg 300w,
image-600.jpg 600w,
image-900.jpg 900w"
sizes="(max-width: 600px) 300px,
(max-width: 900px) 600px,
900px"
src="image-900.jpg"
alt="Responsive image"
loading="lazy"
>
Caching Strategies
Service Worker Implementation
// service-worker.jsconst CACHE_NAME = 'app-cache-v1';
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll([
'/',
'/styles/main.css',
'/scripts/app.js',
'/images/logo.png'
]);
})
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});
Browser Caching Headers
# Nginx configurationlocation /static/ {
expires 1y;
add_header Cache-Control "public, no-transform";
}
location /api/ {
add_header Cache-Control "no-cache";
proxy_pass http://api_backend;
}
JavaScript Performance
Web Workers for Heavy Computations
// main.jsconst worker = new Worker('worker.js');
worker.postMessage({ data: complexData });
worker.onmessage = (event) => {
console.log('Processed data:', event.data);
};
// worker.js
self.onmessage = (event) => {
const result = performHeavyComputation(event.data);
self.postMessage(result);
};
Memory Leak Prevention
class Component extends React.Component {componentDidMount() {
// Bad - potential memory leak
window.addEventListener('resize', this.handleResize);
// Good - clean up listeners
this.handleResize = this.handleResize.bind(this);
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
}
}
Network Optimization
API Response Optimization
// Implementing paginationapp.get('/api/posts', (req, res) => {
const { page = 1, limit = 10 } = req.query;
const skip = (page - 1) * limit;
const posts = await Post.find()
.skip(skip)
.limit(limit)
.select('title excerpt') // Select only needed fields
.lean(); // Convert to plain objects
res.json(posts);
});
Data Prefetching
// Using React Query for smart prefetchingconst { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
staleTime: 5 * 60 * 1000, // 5 minutes
prefetchOnHover: true
});
Real-World Case Study: E-commerce Site Optimization
Let's look at how an e-commerce site improved their performance metrics:Initial Problems
- LCP: 4.2s
- FID: 150ms
- CLS: 0.25
Solutions Implemented
- Image Optimization
import Image from 'next/image';
function ProductCard({ product }) {
return (
<Image
src={product.image}
alt={product.name}
width={300}
height={300}
placeholder="blur"
blurDataURL={product.thumbnailUrl}
/>
);
}
- Code Splitting
const ProductDetails = dynamic(() => import('@/components/ProductDetails'), {
loading: () => <ProductSkeleton />,
ssr: false
});
- API Optimization
const getProduct = async (id) => {
const cacheKey = `product:${id}`;
let product = await redis.get(cacheKey);
if (!product) {
product = await db.products.findUnique({ where: { id } });
await redis.set(cacheKey, JSON.stringify(product), 'EX', 3600);
}
return product;
};
Results
- LCP: 1.8s (-57%)
- FID: 70ms (-53%)
- CLS: 0.05 (-80%)
Monitoring and Maintenance
Performance Monitoring Setup
// Using web-vitals libraryimport { onCLS, onFID, onLCP } from 'web-vitals';
function sendToAnalytics({ name, delta, value, id }) {
analytics.send({
metric: name,
value: delta,
eventId: id
});
}
onCLS(sendToAnalytics);
onFID(sendToAnalytics);
onLCP(sendToAnalytics);
Conclusion
Performance optimization is an ongoing process. Key takeaways:- Measure before optimizing
- Focus on Core Web Vitals
- Implement progressive enhancements
- Monitor constantly
- Test on real devices