name: web-performance-optimization description: 优化Web应用程序性能,使用代码分割、懒加载、缓存、压缩和监控。用于提高核心Web Vitals和用户体验。
Web性能优化
概览
实现性能优化策略,包括懒加载、代码分割、缓存、压缩和监控,以提高核心Web Vitals和用户体验。
何时使用
- 页面加载速度慢
- 最大内容绘制(LCP)高
- 捆绑包尺寸大
- 频繁的累积布局偏移(CLS)
- 移动性能问题
实施示例
1. 代码分割和懒加载(React)
// utils/lazyLoad.ts
import React from 'react';
export const lazyLoad = (importStatement: Promise<any>) => {
return React.lazy(() =>
importStatement.then(module => ({
default: module.default
}))
);
};
// routes.tsx
import { lazyLoad } from './utils/lazyLoad';
export const routes = [
{
path: '/',
component: () => import('./pages/Home'),
lazy: lazyLoad(import('./pages/Home'))
},
{
path: '/dashboard',
lazy: lazyLoad(import('./pages/Dashboard'))
},
{
path: '/users',
lazy: lazyLoad(import('./pages/Users'))
}
];
// App.tsx with Suspense
import { Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
export const App = () => {
return (
<BrowserRouter>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
{routes.map(route => (
<Route key={route.path} path={route.path} element={<route.lazy />} />
))}
</Routes>
</Suspense>
</BrowserRouter>
);
};
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10
},
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true
}
}
}
}
};
2. 图片优化
<!-- Picture元素与srcset响应式图片 -->
<picture>
<source media="(min-width: 1024px)" srcset="image-large.jpg, image-large@2x.jpg 2x" />
<source media="(min-width: 640px)" srcset="image-medium.jpg, image-medium@2x.jpg 2x" />
<source srcset="image-small.jpg, image-small@2x.jpg 2x" />
<img src="image-fallback.jpg" alt="描述" loading="lazy" />
</picture>
<!-- WebP格式与回退 -->
<picture>
<source srcset="image.webp" type="image/webp" />
<img src="image.jpg" alt="描述" loading="lazy" />
</picture>
<!-- TypeScript图片组件 -->
<script lang="typescript">
interface ImageProps {
src: string;
alt: string;
width: number;
height: number;
sizes?: string;
loading?: 'lazy' | 'eager';
}
const OptimizedImage: React.FC<ImageProps> = ({
src,
alt,
width,
height,
sizes = '100vw',
loading = 'lazy'
}) => {
const webpSrc = src.replace(/\.(jpg|png)$/, '.webp');
return (
<picture>
<source srcSet={webpSrc} type="image/webp" />
<img
src={src}
alt={alt}
width={width}
height={height}
sizes={sizes}
loading={loading}
decoding="async"
/>
</picture>
);
};
</script>
3. HTTP缓存和服务工作者
// service-worker.ts
const CACHE_NAME = 'v1';
const ASSETS_TO_CACHE = [
'/',
'/index.html',
'/css/style.css',
'/js/app.js'
];
self.addEventListener('install', (event: ExtendableEvent) => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
return cache.addAll(ASSETS_TO_CACHE);
})
);
});
self.addEventListener('fetch', (event: FetchEvent) => {
// 缓存优先,回退到网络
event.respondWith(
caches.match(event.request).then(response => {
if (response) return response;
return fetch(event.request).then(response => {
// 克隆响应
const cloned = response.clone();
// 缓存成功的响应
if (response.status === 200) {
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, cloned);
});
}
return response;
}).catch(() => {
// 如果有离线页面则返回
return caches.match('/offline.html');
});
})
);
});
// 注册服务工作者
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.catch(err => console.error('SW注册失败:', err));
});
}
4. Gzip压缩和资产优化
// webpack.config.js带压缩
const CompressionPlugin = require('compression-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
mode: 'production',
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true
}
}
})
]
},
plugins: [
new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 8192,
minRatio: 0.8
})
]
};
// .htaccess(Apache)
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript
</IfModule>
# nginx.conf
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_min_length 1000;
gzip_proxied any;
5. 性能监控
// utils/performanceMonitor.ts
interface PerformanceMetrics {
fcp: number; // 第一次内容绘制
lcp: number; // 最大内容绘制
cls: number; // 累积布局偏移
fid: number; // 第一次输入延迟
ttfb: number; // 第一个字节时间
}
export const observeWebVitals = (callback: (metrics: Partial<PerformanceMetrics>) => void) => {
const metrics: Partial<PerformanceMetrics> = {};
// LCP
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
metrics.lcp = lastEntry.renderTime || lastEntry.loadTime;
callback(metrics);
});
try {
lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] });
} catch (e) {
console.warn('LCP观察者不支持');
}
// CLS
const clsObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!(entry as any).hadRecentInput) {
metrics.cls = (metrics.cls || 0) + (entry as any).value;
callback(metrics);
}
}
});
try {
clsObserver.observe({ entryTypes: ['layout-shift'] });
} catch (e) {
console.warn('CLS观察者不支持');
}
// FID通过INP
const inputObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const firstEntry = entries[0];
metrics.fid = firstEntry.processingDuration;
callback(metrics);
});
try {
inputObserver.observe({ entryTypes: ['first-input', 'event'] });
} catch (e) {
console.warn('FID观察者不支持');
}
// TTFB
const navigationTiming = performance.getEntriesByType('navigation')[0];
if (navigationTiming) {
metrics.ttfb = (navigationTiming as any).responseStart - (navigationTiming as any).requestStart;
callback(metrics);
}
};
// 使用
observeWebVitals((metrics) => {
console.log('性能指标:', metrics);
// 发送到分析
fetch('/api/metrics', {
method: 'POST',
body: JSON.stringify(metrics)
});
});
// Chrome DevTools协议性能测试
import puppeteer from 'puppeteer';
async function measurePagePerformance(url: string) {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle2' });
const metrics = JSON.parse(
await page.evaluate(() => JSON.stringify(window.performance))
);
console.log('页面加载时间:', metrics.timing.loadEventEnd - metrics.timing.navigationStart);
console.log('DOM内容已加载:', metrics.timing.domContentLoadedEventEnd - metrics.timing.navigationStart);
await browser.close();
}
最佳实践
- 通过代码分割最小化捆绑包大小
- 使用适当的格式优化图像
- 策略性地实现懒加载
- 使用HTTP缓存头
- 启用gzip/brotli压缩
- 持续监控核心Web Vitals
- 实施服务工作者
- 延迟非关键JavaScript
- 优化关键渲染路径
- 在真实设备和网络上进行测试