Performance Optimization Implementation
1. Core Web Vitals LCP CLS FID
| Metric | Measures | Good Target | Optimization |
|---|---|---|---|
| LCP (Largest Contentful Paint) | Loading performance | < 2.5s | Optimize images, server response, render-blocking resources |
| CLS (Cumulative Layout Shift) | Visual stability | < 0.1 | Set image/video dimensions, avoid inserting content, use CSS transforms |
| FID (First Input Delay) | Interactivity | < 100ms | Reduce JS execution time, code splitting, web workers |
| INP (Interaction to Next Paint) | Responsiveness (replaces FID) | < 200ms | Optimize event handlers, reduce main thread work |
| TTFB (Time to First Byte) | Server response | < 800ms | CDN, server optimization, caching |
| FCP (First Contentful Paint) | First render | < 1.8s | Inline critical CSS, preload fonts, optimize server |
Example: Core Web Vitals measurement and optimization
// Measure Core Web Vitals with web-vitals library
npm install web-vitals
import { onCLS, onFID, onLCP, onINP, onFCP, onTTFB } from 'web-vitals';
// Send to analytics
function sendToAnalytics(metric) {
const body = JSON.stringify(metric);
// Use sendBeacon if available (doesn't block page unload)
if (navigator.sendBeacon) {
navigator.sendBeacon('/analytics', body);
} else {
fetch('/analytics', { body, method: 'POST', keepalive: true });
}
}
// Measure all Core Web Vitals
onCLS(sendToAnalytics);
onFID(sendToAnalytics);
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);
// React hook for Core Web Vitals
import { useEffect } from 'react';
function useWebVitals(onMetric) {
useEffect(() => {
import('web-vitals').then(({ onCLS, onFID, onLCP }) => {
onCLS(onMetric);
onFID(onMetric);
onLCP(onMetric);
});
}, [onMetric]);
}
// Usage
function App() {
useWebVitals((metric) => {
console.log(metric.name, metric.value);
});
return <div>App</div>;
}
// Optimize LCP - Preload critical resources
<head>
{/* Preload hero image */}
<link rel="preload" as="image" href="/hero.jpg" />
{/* Preload critical fonts */}
<link
rel="preload"
as="font"
href="/fonts/inter.woff2"
type="font/woff2"
crossOrigin="anonymous"
/>
{/* Preconnect to external domains */}
<link rel="preconnect" href="https://api.example.com" />
<link rel="dns-prefetch" href="https://cdn.example.com" />
</head>
// Optimize CLS - Reserve space for images
<img
src="image.jpg"
alt="Description"
width="800"
height="600"
style={{ aspectRatio: '800/600' }}
/>
// Use CSS aspect-ratio
.image-container {
aspect-ratio: 16 / 9;
}
// Avoid layout shifts from dynamic content
.ad-container {
min-height: 250px; /* Reserve space */
}
// Use CSS transform instead of top/left
// Bad (causes layout shift)
.element {
top: 100px;
transition: top 0.3s;
}
// Good (no layout shift)
.element {
transform: translateY(100px);
transition: transform 0.3s;
}
// Optimize FID/INP - Code splitting
import { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<Loading />}>
<HeavyComponent />
</Suspense>
);
}
// Use Web Workers for heavy computation
// worker.js
self.addEventListener('message', (e) => {
const result = heavyComputation(e.data);
self.postMessage(result);
});
// main.js
const worker = new Worker('worker.js');
worker.postMessage(data);
worker.addEventListener('message', (e) => {
console.log('Result:', e.data);
});
// Optimize TTFB - Next.js with CDN
// next.config.js
module.exports = {
images: {
domains: ['cdn.example.com'],
},
headers: async () => [
{
source: '/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
],
};
// Lighthouse CI for continuous monitoring
// .lighthouserc.js
module.exports = {
ci: {
collect: {
url: ['http://localhost:3000'],
numberOfRuns: 3,
},
assert: {
preset: 'lighthouse:recommended',
assertions: {
'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
'first-input-delay': ['error', { maxNumericValue: 100 }],
},
},
upload: {
target: 'temporary-public-storage',
},
},
};
// Performance Observer API
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry.name, entry.startTime, entry.duration);
}
});
// Observe different entry types
observer.observe({ entryTypes: ['measure', 'navigation', 'resource'] });
// Custom performance marks
performance.mark('start-api-call');
await fetchData();
performance.mark('end-api-call');
performance.measure('api-call-duration', 'start-api-call', 'end-api-call');
// Next.js built-in Web Vitals reporting
// pages/_app.js
export function reportWebVitals(metric) {
console.log(metric);
// Send to analytics service
if (metric.label === 'web-vital') {
gtag('event', metric.name, {
value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
event_label: metric.id,
non_interaction: true,
});
}
}
2. Code Splitting React.lazy Suspense
| Technique | Implementation | Description | Use Case |
|---|---|---|---|
| React.lazy | lazy(() => import('./Component')) |
Dynamic import for components | Route-based splitting |
| Suspense | <Suspense fallback={...}> |
Loading boundary for lazy components | Show loading state |
| Route-based | Split by routes | Load route code on navigation | Multi-page apps |
| Component-based | Split heavy components | Load on demand (modal, chart) | Conditional features |
| Webpack magic comments | /* webpackChunkName: "name" */ |
Name chunks, prefetch/preload | Better caching |
| Dynamic import | import('module').then() |
Load modules conditionally | Polyfills, libraries |
Example: Code splitting strategies
// Basic React.lazy with Suspense
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
// Route-based code splitting with React Router
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { lazy, Suspense } from 'react';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
<Route path="/dashboard/*" element={<Dashboard />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
// Component-based splitting (modal, heavy chart)
function ProductPage() {
const [showModal, setShowModal] = useState(false);
// Only load modal when needed
const Modal = lazy(() => import('./Modal'));
return (
<div>
<button onClick={() => setShowModal(true)}>Open</button>
{showModal && (
<Suspense fallback={<ModalSkeleton />}>
<Modal onClose={() => setShowModal(false)} />
</Suspense>
)}
</div>
);
}
// Webpack magic comments for chunk naming
const Dashboard = lazy(() =>
import(/* webpackChunkName: "dashboard" */ './Dashboard')
);
// Prefetch on hover (load before needed)
const Dashboard = lazy(() =>
import(/* webpackPrefetch: true */ './Dashboard')
);
// Preload (load in parallel with parent)
const Dashboard = lazy(() =>
import(/* webpackPreload: true */ './Dashboard')
);
// Conditional loading based on feature flags
async function loadFeature() {
if (featureFlags.newDashboard) {
const { NewDashboard } = await import('./NewDashboard');
return NewDashboard;
} else {
const { OldDashboard } = await import('./OldDashboard');
return OldDashboard;
}
}
// Library splitting (load polyfills conditionally)
async function loadPolyfills() {
if (!window.IntersectionObserver) {
await import('intersection-observer');
}
if (!window.fetch) {
await import('whatwg-fetch');
}
}
// Multiple suspense boundaries
function App() {
return (
<div>
<Header />
{/* Critical content */}
<Suspense fallback={<HeroSkeleton />}>
<Hero />
</Suspense>
{/* Non-critical content */}
<Suspense fallback={<ChartSkeleton />}>
<AnalyticsChart />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<DataTable />
</Suspense>
</div>
);
}
// Error boundary with lazy loading
import { ErrorBoundary } from 'react-error-boundary';
function LazyRoute({ path, component }) {
const Component = lazy(component);
return (
<ErrorBoundary fallback={<ErrorPage />}>
<Suspense fallback={<Loading />}>
<Component />
</Suspense>
</ErrorBoundary>
);
}
// Next.js dynamic imports
import dynamic from 'next/dynamic';
const DynamicComponent = dynamic(() => import('./Component'), {
loading: () => <p>Loading...</p>,
ssr: false, // Disable server-side rendering
});
// With named export
const DynamicChart = dynamic(() =>
import('./Charts').then(mod => mod.LineChart)
);
// Vite code splitting
// Vite automatically splits by dynamic imports
const AdminPanel = () => import('./AdminPanel.vue');
// Manual chunk configuration (vite.config.js)
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
router: ['react-router-dom'],
ui: ['@mui/material', '@emotion/react'],
},
},
},
},
};
3. React.memo useMemo useCallback
| Hook/HOC | Purpose | When to Use | Caveat |
|---|---|---|---|
| React.memo | Memoize component | Expensive renders, pure components | Shallow prop comparison |
| useMemo | Memoize computed value | Expensive calculations | Don't overuse, memory cost |
| useCallback | Memoize function | Pass to memoized children, deps | Useful with React.memo |
| useMemo vs useCallback | Value vs function | useMemo(() => fn) = useCallback(fn) | useCallback is shorthand |
| Custom compare | React.memo(Comp, areEqual) | Deep comparison, specific props | Complex prop structures |
Example: Memoization techniques in React
// React.memo - prevent unnecessary re-renders
import { memo } from 'react';
const ExpensiveComponent = memo(({ data, onClick }) => {
console.log('Rendering ExpensiveComponent');
return (
<div>
{data.map(item => (
<div key={item.id} onClick={() => onClick(item)}>
{item.name}
</div>
))}
</div>
);
});
// Parent component
function Parent() {
const [count, setCount] = useState(0);
const [data] = useState([/* large array */]);
// Without memo, ExpensiveComponent re-renders when count changes
// With memo, it only re-renders when data or onClick changes
return (
<div>
<button onClick={() => setCount(count + 1)}>{count}</button>
<ExpensiveComponent data={data} onClick={handleClick} />
</div>
);
}
// useCallback - memoize function reference
function Parent() {
const [count, setCount] = useState(0);
const [items, setItems] = useState([]);
// Without useCallback, new function on every render
// const handleClick = (item) => {
// console.log(item);
// };
// With useCallback, same function reference
const handleClick = useCallback((item) => {
console.log(item);
// Use items from closure
}, []); // Dependencies
return (
<div>
<button onClick={() => setCount(count + 1)}>{count}</button>
<ExpensiveComponent items={items} onClick={handleClick} />
</div>
);
}
// useMemo - memoize expensive computation
function DataProcessor({ rawData }) {
// Without useMemo, processes on every render
// const processedData = expensiveProcessing(rawData);
// With useMemo, only recomputes when rawData changes
const processedData = useMemo(() => {
console.log('Processing data...');
return expensiveProcessing(rawData);
}, [rawData]);
return <div>{processedData.length} items</div>;
}
// Expensive computation example
const filteredAndSortedList = useMemo(() => {
return items
.filter(item => item.active)
.sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
// Custom comparison function for React.memo
const ComplexComponent = memo(
({ user, settings }) => {
return <div>{user.name}</div>;
},
(prevProps, nextProps) => {
// Return true if props are equal (skip re-render)
return (
prevProps.user.id === nextProps.user.id &&
prevProps.settings.theme === nextProps.settings.theme
);
}
);
// Real-world example: Search with debounce
function SearchComponent({ onSearch }) {
const [query, setQuery] = useState('');
// Memoize debounced function
const debouncedSearch = useMemo(
() => debounce((value) => onSearch(value), 300),
[onSearch]
);
const handleChange = (e) => {
const value = e.target.value;
setQuery(value);
debouncedSearch(value);
};
return <input value={query} onChange={handleChange} />;
}
// Context with memoization
const UserContext = createContext();
function UserProvider({ children }) {
const [user, setUser] = useState(null);
// Memoize context value to prevent unnecessary re-renders
const value = useMemo(
() => ({
user,
setUser,
isAuthenticated: !!user,
}),
[user]
);
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}
// When NOT to use memoization
// ❌ Don't memoize everything
function SimpleComponent({ name }) {
// This is fine without useMemo
const greeting = `Hello, ${name}!`;
return <div>{greeting}</div>;
}
// ❌ Primitive props don't benefit from React.memo
const SimpleChild = memo(({ text, count }) => {
// React already optimizes primitive comparisons
return <div>{text} {count}</div>;
});
// ✅ DO use for expensive renders or large prop objects
const DataGrid = memo(({ data, columns, onSort }) => {
// Expensive table rendering with thousands of rows
return <table>...</table>;
});
// Combined example: Optimized list component
const ListItem = memo(({ item, onDelete }) => {
return (
<li>
{item.name}
<button onClick={() => onDelete(item.id)}>Delete</button>
</li>
);
});
function List({ items }) {
const [selectedId, setSelectedId] = useState(null);
// Memoize callback to prevent ListItem re-renders
const handleDelete = useCallback((id) => {
// Delete logic
}, []);
// Memoize filtered items
const filteredItems = useMemo(
() => items.filter(item => item.visible),
[items]
);
return (
<ul>
{filteredItems.map(item => (
<ListItem key={item.id} item={item} onDelete={handleDelete} />
))}
</ul>
);
}
4. Webpack Bundle Analyzer Optimization
| Tool | Purpose | Installation | Insight |
|---|---|---|---|
| webpack-bundle-analyzer | Visualize bundle size | npm i -D webpack-bundle-analyzer |
Interactive treemap |
| source-map-explorer | Analyze with source maps | npm i -D source-map-explorer |
See original file sizes |
| Bundle Buddy | Find duplicate code | Web tool | Identify optimization opportunities |
| Tree Shaking | Remove dead code | Built into Webpack 4+ | Requires ES6 modules |
| Code Splitting | Split into chunks | Webpack config | Parallel loading |
Example: Bundle analysis and optimization
// Install webpack-bundle-analyzer
npm install --save-dev webpack-bundle-analyzer
// Webpack configuration
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: 'bundle-report.html',
openAnalyzer: false,
generateStatsFile: true,
statsFilename: 'bundle-stats.json',
}),
],
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
},
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true,
},
},
},
},
};
// package.json scripts
{
"scripts": {
"build": "webpack --mode production",
"analyze": "webpack --mode production --profile --json > stats.json && webpack-bundle-analyzer stats.json"
}
}
// Create React App with analyzer
npm install --save-dev cra-bundle-analyzer
// package.json
{
"scripts": {
"analyze": "npm run build && npx cra-bundle-analyzer"
}
}
// Next.js with bundle analyzer
npm install @next/bundle-analyzer
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({
// Next.js config
});
// package.json
{
"scripts": {
"analyze": "ANALYZE=true npm run build"
}
}
// Vite with rollup-plugin-visualizer
npm install --save-dev rollup-plugin-visualizer
// vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';
export default {
plugins: [
visualizer({
open: true,
gzipSize: true,
brotliSize: true,
}),
],
};
// Source map explorer
npm install --save-dev source-map-explorer
// package.json
{
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'"
}
}
// Optimization strategies based on bundle analysis
// 1. Replace heavy libraries
// ❌ moment.js (288KB)
import moment from 'moment';
const date = moment().format('YYYY-MM-DD');
// ✅ date-fns (13KB with tree shaking)
import { format } from 'date-fns';
const date = format(new Date(), 'yyyy-MM-dd');
// 2. Import only what you need
// ❌ Import entire library
import _ from 'lodash';
const result = _.debounce(fn, 300);
// ✅ Import specific function
import debounce from 'lodash/debounce';
const result = debounce(fn, 300);
// 3. Use dynamic imports for large libraries
// ❌ Import upfront
import { Chart } from 'chart.js';
// ✅ Load on demand
const loadChart = async () => {
const { Chart } = await import('chart.js');
return Chart;
};
// 4. Configure externals (CDN)
// webpack.config.js
module.exports = {
externals: {
react: 'React',
'react-dom': 'ReactDOM',
},
};
// HTML
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
// 5. Optimize images
// Use next/image or similar
import Image from 'next/image';
<Image
src="/hero.jpg"
width={800}
height={600}
alt="Hero"
placeholder="blur"
/>
// 6. Remove unused CSS
// Use PurgeCSS or Tailwind's built-in purge
// tailwind.config.js
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
// CSS not used in these files will be removed
};
// 7. Minify and compress
// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // Remove console.log
},
},
}),
],
},
plugins: [
new CompressionPlugin({
algorithm: 'brotliCompress',
test: /\.(js|css|html|svg)$/,
}),
],
};
// 8. Analyze and set performance budgets
// webpack.config.js
module.exports = {
performance: {
maxAssetSize: 244 * 1024, // 244 KB
maxEntrypointSize: 244 * 1024,
hints: 'error', // or 'warning'
},
};
// Custom bundle size check
const fs = require('fs');
const path = require('path');
const buildFolder = path.join(__dirname, 'build/static/js');
const files = fs.readdirSync(buildFolder);
let totalSize = 0;
files.forEach(file => {
if (file.endsWith('.js')) {
const stats = fs.statSync(path.join(buildFolder, file));
totalSize += stats.size;
}
});
console.log(`Total JS bundle size: ${(totalSize / 1024).toFixed(2)} KB`);
if (totalSize > 500 * 1024) {
console.error('Bundle size exceeds 500KB!');
process.exit(1);
}
5. Image Optimization next/image WebP
| Technique | Implementation | Benefit | Browser Support |
|---|---|---|---|
| WebP Format | Modern image format | 25-35% smaller than JPEG | 96%+ (with fallback) |
| AVIF Format | Newest format | 50% smaller than JPEG | 90%+ (newer browsers) |
| Lazy Loading | loading="lazy" |
Load images as needed | Native browser support |
| Responsive Images | srcset, sizes |
Load appropriate size | Universal support |
| next/image | Next.js Image component | Automatic optimization | Works everywhere |
| CDN | Image transformation API | On-the-fly optimization | Cloudinary, Imgix |
Example: Modern image optimization techniques
// Next.js Image component (automatic optimization)
import Image from 'next/image';
function Hero() {
return (
<Image
src="/hero.jpg"
alt="Hero image"
width={1200}
height={600}
priority // Load immediately (above fold)
placeholder="blur" // Show blur while loading
blurDataURL="data:image/jpeg;base64,..." // Low-res placeholder
/>
);
}
// Responsive images with next/image
<Image
src="/product.jpg"
alt="Product"
width={800}
height={600}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
// Automatically generates srcset
/>
// External images (configure in next.config.js)
// next.config.js
module.exports = {
images: {
domains: ['cdn.example.com', 'images.unsplash.com'],
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
};
// Native responsive images with picture element
<picture>
<source
type="image/avif"
srcSet="
/image-small.avif 400w,
/image-medium.avif 800w,
/image-large.avif 1200w
"
sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
/>
<source
type="image/webp"
srcSet="
/image-small.webp 400w,
/image-medium.webp 800w,
/image-large.webp 1200w
"
sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
/>
<img
src="/image-large.jpg"
alt="Description"
loading="lazy"
decoding="async"
/>
</picture>
// Native lazy loading
<img
src="image.jpg"
alt="Description"
loading="lazy"
decoding="async"
width="800"
height="600"
/>
// Blur placeholder with CSS
.image-container {
background: linear-gradient(to bottom, #f0f0f0, #e0e0e0);
position: relative;
}
.image-container::before {
content: '';
position: absolute;
inset: 0;
background-image: url('data:image/jpeg;base64,...'); // Tiny base64
background-size: cover;
filter: blur(10px);
transition: opacity 0.3s;
}
.image-container.loaded::before {
opacity: 0;
}
// Convert images to WebP/AVIF with sharp
npm install sharp
// generateImages.js
const sharp = require('sharp');
const fs = require('fs');
const path = require('path');
async function convertImage(inputPath, outputDir) {
const filename = path.basename(inputPath, path.extname(inputPath));
// Generate WebP
await sharp(inputPath)
.webp({ quality: 80 })
.toFile(path.join(outputDir, `${filename}.webp`));
// Generate AVIF
await sharp(inputPath)
.avif({ quality: 65 })
.toFile(path.join(outputDir, `${filename}.avif`));
// Generate responsive sizes
const sizes = [400, 800, 1200];
for (const size of sizes) {
await sharp(inputPath)
.resize(size)
.webp({ quality: 80 })
.toFile(path.join(outputDir, `${filename}-${size}.webp`));
}
}
// Cloudinary integration
<img
src="https://res.cloudinary.com/demo/image/upload/w_400,f_auto,q_auto/sample.jpg"
alt="Optimized"
loading="lazy"
/>
// w_400: width 400px
// f_auto: automatic format (WebP/AVIF)
// q_auto: automatic quality
// React component for optimized images
function OptimizedImage({ src, alt, width, height }) {
const [loaded, setLoaded] = useState(false);
return (
<div className="image-wrapper">
<picture>
<source
type="image/avif"
srcSet={`${src}.avif`}
/>
<source
type="image/webp"
srcSet={`${src}.webp`}
/>
<img
src={`${src}.jpg`}
alt={alt}
width={width}
height={height}
loading="lazy"
decoding="async"
onLoad={() => setLoaded(true)}
className={loaded ? 'loaded' : ''}
/>
</picture>
</div>
);
}
// Intersection Observer for custom lazy loading
function LazyImage({ src, alt }) {
const [isLoaded, setIsLoaded] = useState(false);
const [isInView, setIsInView] = useState(false);
const imgRef = useRef();
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsInView(true);
observer.disconnect();
}
},
{ rootMargin: '50px' }
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => observer.disconnect();
}, []);
return (
<img
ref={imgRef}
src={isInView ? src : ''}
alt={alt}
onLoad={() => setIsLoaded(true)}
style={{ opacity: isLoaded ? 1 : 0 }}
/>
);
}
// Progressive JPEG loading
// Use progressive JPEG encoding for better perceived performance
// ImageMagick: convert input.jpg -interlace Plane output.jpg
// Sharp: sharp(input).jpeg({ progressive: true }).toFile(output)
// Image compression before upload
async function compressImage(file) {
const options = {
maxSizeMB: 1,
maxWidthOrHeight: 1920,
useWebWorker: true,
};
const compressedFile = await imageCompression(file, options);
return compressedFile;
}
6. Service Worker Caching Workbox
| Strategy | Description | Use Case | Workbox Method |
|---|---|---|---|
| Cache First | Cache, fallback to network | Static assets (CSS, JS, images) | CacheFirst |
| Network First | Network, fallback to cache | API calls, dynamic content | NetworkFirst |
| Stale While Revalidate | Cache first, update in background | Frequent updates (avatars, feeds) | StaleWhileRevalidate |
| Network Only | Always use network | Real-time data | NetworkOnly |
| Cache Only | Only use cache | Pre-cached resources | CacheOnly |
| Precaching | Cache during install | App shell, critical resources | precacheAndRoute |
Example: Service Worker with Workbox caching strategies
// Install Workbox
npm install workbox-webpack-plugin
// webpack.config.js
const { GenerateSW } = require('workbox-webpack-plugin');
module.exports = {
plugins: [
new GenerateSW({
clientsClaim: true,
skipWaiting: true,
runtimeCaching: [
{
urlPattern: /\.(?:png|jpg|jpeg|svg|gif)$/,
handler: 'CacheFirst',
options: {
cacheName: 'images',
expiration: {
maxEntries: 50,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
},
},
},
{
urlPattern: /^https:\/\/api\.example\.com/,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
networkTimeoutSeconds: 10,
expiration: {
maxEntries: 50,
maxAgeSeconds: 5 * 60, // 5 minutes
},
},
},
],
}),
],
};
// Custom service worker with Workbox
// service-worker.js
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
// Precache generated assets
precacheAndRoute(self.__WB_MANIFEST);
// Cache images with CacheFirst strategy
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'images',
plugins: [
new ExpirationPlugin({
maxEntries: 60,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
}),
],
})
);
// Cache CSS and JavaScript with StaleWhileRevalidate
registerRoute(
({ request }) =>
request.destination === 'style' ||
request.destination === 'script',
new StaleWhileRevalidate({
cacheName: 'static-resources',
})
);
// Cache API responses with NetworkFirst
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new NetworkFirst({
cacheName: 'api-cache',
networkTimeoutSeconds: 10,
plugins: [
new CacheableResponsePlugin({
statuses: [0, 200],
}),
new ExpirationPlugin({
maxEntries: 50,
maxAgeSeconds: 5 * 60, // 5 minutes
}),
],
})
);
// Cache Google Fonts
registerRoute(
({ url }) => url.origin === 'https://fonts.googleapis.com',
new StaleWhileRevalidate({
cacheName: 'google-fonts-stylesheets',
})
);
registerRoute(
({ url }) => url.origin === 'https://fonts.gstatic.com',
new CacheFirst({
cacheName: 'google-fonts-webfonts',
plugins: [
new CacheableResponsePlugin({
statuses: [0, 200],
}),
new ExpirationPlugin({
maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year
maxEntries: 30,
}),
],
})
);
// Register service worker
// main.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/service-worker.js')
.then(registration => {
console.log('SW registered:', registration);
})
.catch(error => {
console.log('SW registration failed:', error);
});
});
}
// Create React App with Workbox
// src/service-worker.js
import { clientsClaim } from 'workbox-core';
import { ExpirationPlugin } from 'workbox-expiration';
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate } from 'workbox-strategies';
clientsClaim();
precacheAndRoute(self.__WB_MANIFEST);
const fileExtensionRegexp = new RegExp("/[^/?]+\\.[^/]+$");
registerRoute(
({ request, url }) => {
if (request.mode !== 'navigate') return false;
if (url.pathname.startsWith('/_')) return false;
if (url.pathname.match(fileExtensionRegexp)) return false;
return true;
},
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
);
registerRoute(
({ url }) =>
url.origin === self.location.origin && url.pathname.endsWith('.png'),
new StaleWhileRevalidate({
cacheName: 'images',
plugins: [new ExpirationPlugin({ maxEntries: 50 })],
})
);
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
// Register in index.js
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
serviceWorkerRegistration.register();
// Next.js with next-pwa
npm install next-pwa
// next.config.js
const withPWA = require('next-pwa')({
dest: 'public',
register: true,
skipWaiting: true,
runtimeCaching: [
{
urlPattern: /^https:\/\/fonts\.(?:googleapis|gstatic)\.com\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'google-fonts',
expiration: {
maxEntries: 4,
maxAgeSeconds: 365 * 24 * 60 * 60, // 365 days
},
},
},
{
urlPattern: /\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'static-font-assets',
expiration: {
maxEntries: 4,
maxAgeSeconds: 7 * 24 * 60 * 60, // 7 days
},
},
},
{
urlPattern: /\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'static-image-assets',
expiration: {
maxEntries: 64,
maxAgeSeconds: 24 * 60 * 60, // 24 hours
},
},
},
],
});
module.exports = withPWA({
// Next.js config
});
// Offline fallback page
// service-worker.js
import { offlineFallback } from 'workbox-recipes';
offlineFallback({
pageFallback: '/offline.html',
});
// Background sync for failed requests
import { BackgroundSyncPlugin } from 'workbox-background-sync';
const bgSyncPlugin = new BackgroundSyncPlugin('api-queue', {
maxRetentionTime: 24 * 60, // Retry for up to 24 hours (in minutes)
});
registerRoute(
/\/api\/.*\/*.json/,
new NetworkOnly({
plugins: [bgSyncPlugin],
}),
'POST'
);
// Cache analytics requests
import { Queue } from 'workbox-background-sync';
const queue = new Queue('analytics-queue');
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/analytics')) {
const promiseChain = fetch(event.request.clone()).catch(() => {
return queue.pushRequest({ request: event.request });
});
event.waitUntil(promiseChain);
}
});
Performance Optimization Summary
- Core Web Vitals - Measure LCP (<2.5s), CLS (<0.1), FID/INP (<100ms/200ms) using web-vitals library, optimize with preload, proper dimensions, code splitting
- Code Splitting - Use React.lazy + Suspense for route/component splitting, webpack magic comments for chunk naming, prefetch/preload for predictive loading
- Memoization - React.memo for expensive component renders, useMemo for computed values, useCallback for function references passed to children
- Bundle Analysis - Use webpack-bundle-analyzer or rollup visualizer, identify large dependencies, replace heavy libraries, tree shake unused code
- Image Optimization - Use WebP/AVIF formats, next/image for automatic optimization, responsive images with srcset, lazy loading, blur placeholders
- Service Workers - Implement caching strategies with Workbox: CacheFirst for static assets, NetworkFirst for API, StaleWhileRevalidate for frequently updated content
- Best Practices - Set performance budgets, monitor with Lighthouse CI, compress assets (gzip/brotli), use CDN, minimize main thread work