前端性能优化¶
📚 章节目标¶
本章节将全面介绍前端性能优化的各种技术和策略,包括代码分割、懒加载、缓存策略、渲染优化等,帮助学习者掌握前端性能优化的核心方法。
学习目标¶
- 理解前端性能优化的核心指标
- 掌握代码分割和懒加载技术
- 掌握缓存策略优化
- 掌握渲染性能优化
- 掌握网络性能优化
- 掌握性能监控和分析工具
📊 性能指标¶
1. Core Web Vitals¶
1.1 LCP (Largest Contentful Paint)¶
JavaScript
// LCP - 最大内容绘制
// 衡量页面主要内容加载完成的时间
// 目标:< 2.5s
// 监控LCP
import { onLCP } from 'web-vitals';
onLCP((metric) => {
console.log('LCP:', metric.value);
// 发送到分析服务
analytics.track('LCP', { value: metric.value });
});
// 优化LCP
// 1. 优化关键资源加载
// 2. 预加载重要资源
// 3. 优化服务器响应时间
// 4. 使用CDN加速
// 5. 压缩和优化图片
// 预加载关键资源
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/images/hero.jpg" as="image">
<link rel="preload" href="/styles/main.css" as="style">
1.2 INP (Interaction to Next Paint)¶
JavaScript
// INP - 交互到下一次绘制(已于 2024年3月取代 FID 成为 Core Web Vital)
// 衡量用户任意交互到视觉响应的时间
// 目标:< 200ms
// 监控INP
import { onINP } from 'web-vitals';
onINP((metric) => {
console.log('INP:', metric.value);
analytics.track('INP', { value: metric.value });
});
// 优化INP
// 1. 减少JavaScript执行时间
// 2. 代码分割和懒加载
// 3. 减少主线程工作
// 4. 使用Web Workers
// 5. 优化事件处理程序
// 使用Web Worker
const worker = new Worker('worker.js');
worker.postMessage({ data: largeData });
worker.onmessage = (e) => {
console.log('Result:', e.data);
};
1.3 CLS (Cumulative Layout Shift)¶
JavaScript
// CLS - 累积布局偏移
// 衡量页面布局稳定性
// 目标:< 0.1
// 监控CLS
import { onCLS } from 'web-vitals';
onCLS((metric) => {
console.log('CLS:', metric.value);
analytics.track('CLS', { value: metric.value });
});
// 优化CLS
// 1. 为图片和视频设置尺寸
// 2. 为动态内容预留空间
// 3. 避免在现有内容上方插入内容
// 4. 使用CSS transform和opacity进行动画
// 为图片设置尺寸
<img
src="image.jpg"
width="800"
height="600"
loading="lazy"
alt="Description"
>
// 为动态内容预留空间
<div style="min-height: 200px;">
<!-- 动态加载的内容 -->
</div>
2. 其他性能指标¶
2.1 FCP (First Contentful Paint)¶
JavaScript
// FCP - 首次内容绘制
// 衡量浏览器首次渲染内容的时间
// 目标:< 1.8s
import { onFCP } from 'web-vitals';
onFCP((metric) => {
console.log('FCP:', metric.value);
});
// 优化FCP
// 1. 减少阻塞渲染的资源
// 2. 内联关键CSS
// 3. 优化关键渲染路径
// 4. 使用预加载
// 内联关键CSS
<style>
/* 关键CSS */
body { margin: 0; padding: 0; }
.header { background: #333; color: #fff; }
</style>
<link rel="preload" href="/styles/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
2.2 TTI (Time to Interactive)¶
JavaScript
// TTI - 可交互时间
// 衡量页面完全可交互的时间
// 目标:< 3.8s
// 注意:TTI 是 Lighthouse 实验室指标,web-vitals 库不提供 TTI
// 可通过 Lighthouse API 或 PerformanceObserver 测量
// 优化TTI
// 1. 减少JavaScript执行时间
// 2. 延迟加载非关键JavaScript
// 3. 优化第三方脚本
// 4. 使用代码分割
📦 代码分割与懒加载¶
1. 代码分割¶
1.1 路由级代码分割¶
JavaScript
// React Router代码分割
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// 懒加载组件
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
// Vue Router代码分割
const routes = [
{
path: '/',
component: () => import('./views/Home.vue'),
},
{
path: '/about',
component: () => import('./views/About.vue'),
},
{
path: '/contact',
component: () => import('./views/Contact.vue'),
},
];
// Angular路由懒加载
const routes: Routes = [
{
path: '',
loadChildren: () => import('./home/home.module').then(m => m.HomeModule),
},
{
path: 'about',
loadChildren: () => import('./about/about.module').then(m => m.AboutModule),
},
];
1.2 组件级代码分割¶
JavaScript
// React组件懒加载
import { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
const [showHeavy, setShowHeavy] = useState(false);
return (
<div>
<button onClick={() => setShowHeavy(true)}>
Load Heavy Component
</button>
{showHeavy && (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
)}
</div>
);
}
// Vue组件懒加载
<script setup>
import { defineAsyncComponent } from 'vue';
const HeavyComponent = defineAsyncComponent(() =>
import('./HeavyComponent.vue')
);
</script>
<template>
<div>
<button @click="showHeavy = true">Load Heavy Component</button>
<Suspense v-if="showHeavy">
<template #default>
<HeavyComponent />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</div>
</template>
2. 动态导入¶
2.1 基础动态导入¶
JavaScript
// 动态导入模块
button.addEventListener('click', async () => {
const module = await import('./heavy-module.js');
module.doSomething();
});
// 条件动态导入
async function loadFeature() {
if (user.hasPremium) {
const premiumFeature = await import('./premium-feature.js');
premiumFeature.init();
}
}
// 动态导入带参数
const loadModule = async (moduleName) => {
const module = await import(`./modules/${moduleName}.js`);
return module.default;
};
// 使用
loadModule('featureA').then(FeatureA => {
new FeatureA();
});
2.2 预加载¶
JavaScript
// 预加载模块
const preloadModule = (modulePath) => {
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'script';
link.href = modulePath;
document.head.appendChild(link);
};
// 预加载
preloadModule('/heavy-module.js');
// 后续使用
button.addEventListener('click', async () => {
const module = await import('/heavy-module.js');
module.doSomething();
});
3. Tree Shaking¶
3.1 配置Tree Shaking¶
JavaScript
// Webpack配置
module.exports = {
mode: 'production',
optimization: {
usedExports: true,
sideEffects: false,
},
};
// package.json配置
{
"sideEffects": false
}
// 或者指定有副作用的文件
{
"sideEffects": [
"*.css",
"./src/polyfills.js"
]
}
// 使用ES模块
// 好的做法 - 支持Tree Shaking
import { debounce } from 'lodash-es';
// 不好的做法 - 不支持Tree Shaking
import _ from 'lodash';
3.2 优化导入¶
JavaScript
// 好的做法 - 按需导入
import { Button, Input } from 'antd';
// 不好的做法 - 导入整个库
import antd from 'antd';
// 好的做法 - 使用Tree Shaking
import { map, filter, reduce } from 'lodash-es';
// 不好的做法 - 导入整个lodash
import _ from 'lodash';
🚀 缓存策略¶
1. 浏览器缓存¶
1.1 HTTP缓存¶
JavaScript
// 服务端设置缓存头
// Express.js示例
app.use(express.static('public', {
maxAge: '1y', // 1年
etag: true,
lastModified: true,
}));
// Nginx配置
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
// 缓存策略
// 1. 静态资源:长期缓存(1年)
// 2. HTML文件:不缓存或短时间缓存
// 3. API响应:根据内容设置缓存时间
// 4. 版本化资源:使用内容哈希
1.2 Service Worker缓存¶
JavaScript
// 注册Service Worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').then(registration => {
console.log('SW registered:', registration);
}).catch(error => {
console.log('SW registration failed:', error);
});
});
}
// Service Worker实现
const CACHE_NAME = 'v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/scripts/main.js',
'/images/logo.png',
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
if (response) {
return response;
}
return fetch(event.request);
})
);
});
// Workbox简化Service Worker
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { CacheFirst, StaleWhileRevalidate } from 'workbox-strategies';
// 预缓存静态资源
precacheAndRoute(self.__WB_MANIFEST);
// 缓存策略
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'images',
})
);
registerRoute(
({ request }) => request.destination === 'script',
new StaleWhileRevalidate({
cacheName: 'scripts',
})
);
2. 应用缓存¶
2.1 LocalStorage缓存¶
JavaScript
// 缓存数据
function cacheData(key, data, ttl = 3600000) {
const item = {
data,
timestamp: Date.now(),
ttl,
};
localStorage.setItem(key, JSON.stringify(item));
}
// 获取缓存数据
function getCachedData(key) {
const item = localStorage.getItem(key);
if (!item) return null;
const { data, timestamp, ttl } = JSON.parse(item);
const isExpired = Date.now() - timestamp > ttl;
if (isExpired) {
localStorage.removeItem(key);
return null;
}
return data;
}
// 使用示例
async function fetchUsers() {
const cached = getCachedData('users');
if (cached) {
return cached;
}
const response = await fetch('/api/users');
const users = await response.json();
cacheData('users', users, 600000); // 缓存10分钟
return users;
}
2.2 IndexedDB缓存¶
JavaScript
// 使用IndexedDB
import { openDB } from 'idb';
async function initDB() {
return openDB('my-app', 1, {
upgrade(db) {
if (!db.objectStoreNames.contains('users')) {
db.createObjectStore('users', { keyPath: 'id' });
}
},
});
}
async function cacheUsers(users) {
const db = await initDB();
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');
for (const user of users) {
await store.put(user);
}
await tx.done;
}
async function getCachedUsers() {
const db = await initDB();
return db.getAll('users');
}
🎨 渲染性能优化¶
1. DOM优化¶
1.1 减少重排和重绘¶
JavaScript
// 不好的做法 - 多次修改DOM
const element = document.getElementById('myElement');
element.style.width = '100px';
element.style.height = '100px';
element.style.backgroundColor = 'red';
// 好的做法 - 批量修改
const element = document.getElementById('myElement');
element.style.cssText = 'width: 100px; height: 100px; background-color: red;';
// 或者使用类
element.className = 'styled-element';
// 使用DocumentFragment批量添加元素
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
fragment.appendChild(li);
}
document.getElementById('list').appendChild(fragment);
1.2 虚拟DOM优化¶
JavaScript
// React - 使用key优化列表
function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => ( // map转换每个元素;filter筛选;reduce累积
<li key={todo.id}>
{todo.text}
</li>
))}
</ul>
);
}
// 使用React.memo避免不必要的渲染
const TodoItem = React.memo(function TodoItem({ todo }) {
return <li>{todo.text}</li>;
});
// 使用useMemo缓存计算结果
function ExpensiveComponent({ data }) {
const processedData = useMemo(() => {
return data.map(item => ({
...item, // ...展开运算符:展开数组/对象
value: item.value * 2,
}));
}, [data]);
return <div>{processedData.map(item => <div key={item.id}>{item.value}</div>)}</div>;
}
2. CSS优化¶
2.1 CSS选择器优化¶
CSS
/* 不好的做法 - 过深的嵌套 */
.container .sidebar .menu .item .link {
color: blue;
}
/* 好的做法 - 扁平化选择器 */
.menu-link {
color: blue;
}
/* 不好的做法 - 使用通配符 */
* {
margin: 0;
padding: 0;
}
/* 好的做法 - 具体指定 */
body, h1, h2, h3, p {
margin: 0;
padding: 0;
}
2.2 CSS动画优化¶
CSS
/* 使用transform和opacity进行动画 */
.animated-element {
transition: transform 0.3s ease, opacity 0.3s ease;
}
.animated-element:hover {
transform: scale(1.1);
opacity: 0.8;
}
/* 不好的做法 - 使用left/top进行动画 */
.bad-animation {
transition: left 0.3s ease, top 0.3s ease;
}
/* 使用will-change提示浏览器 */
.will-change-element {
will-change: transform, opacity;
}
/* 使用GPU加速 */
.gpu-accelerated {
transform: translateZ(0);
backface-visibility: hidden;
}
3. 图片优化¶
3.1 响应式图片¶
HTML
<!-- 使用srcset提供不同尺寸的图片 -->
<img
src="image-small.jpg"
srcset="
image-small.jpg 400w,
image-medium.jpg 800w,
image-large.jpg 1200w
"
sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
alt="Description"
>
<!-- 使用picture元素提供不同格式 -->
<picture>
<source srcset="image.webp" type="image/webp">
<source srcset="image.jpg" type="image/jpeg">
<img src="image.jpg" alt="Description">
</picture>
<!-- 懒加载图片 -->
<img
src="placeholder.jpg"
data-src="actual-image.jpg"
loading="lazy"
alt="Description"
class="lazy-load"
>
3.2 图片压缩和格式¶
JavaScript
// 使用sharp库压缩图片
const sharp = require('sharp');
async function optimizeImage(inputPath, outputPath) {
await sharp(inputPath)
.resize(800, 600, {
fit: 'inside',
withoutEnlargement: true,
})
.webp({ quality: 80 })
.toFile(outputPath);
}
// 使用现代图片格式
// WebP - 比JPEG小25-35%
// AVIF - 比WebP小50%
// 使用picture元素提供降级方案
🌐 网络性能优化¶
1. HTTP优化¶
1.1 HTTP/2和HTTP/3¶
JavaScript
// 启用HTTP/2
// Nginx配置
listen 443 ssl http2;
// 启用HTTP/3
// Nginx配置
listen 443 quic reuseport;
// 使用HTTP/2 Server Push
// Nginx配置
http {
server {
location / {
http2_push /style.css;
http2_push /script.js;
}
}
}
1.2 预连接¶
HTML
<!-- 预连接到重要域名 -->
<link rel="preconnect" href="https://api.example.com">
<link rel="preconnect" href="https://cdn.example.com">
<!-- DNS预解析 -->
<link rel="dns-prefetch" href="https://example.com">
<!-- 预加载关键资源 -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/styles/main.css" as="style">
<link rel="preload" href="/scripts/main.js" as="script">
2. 资源优化¶
2.1 资源压缩¶
JavaScript
// Webpack配置压缩
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
},
},
}),
new CssMinimizerPlugin(),
],
},
};
// Gzip压缩
const compression = require('compression');
app.use(compression());
// Brotli压缩(Node.js 11.7+ 内置 zlib.brotliCompress)
// 使用 shrink-ray-current 中间件同时支持 Gzip 和 Brotli
const shrinkRay = require('shrink-ray-current');
app.use(shrinkRay());
// 或在 Nginx 层配置:brotli on; brotli_types text/html text/css application/javascript;
2.2 CDN加速¶
JavaScript
// 使用CDN加速静态资源
const CDN_URL = 'https://cdn.example.com';
// 在HTML中使用
<link rel="stylesheet" href="${CDN_URL}/styles/main.css">
<script src="${CDN_URL}/scripts/main.js"></script>
// 在代码中使用
const imageUrl = `${CDN_URL}/images/logo.png`;
📈 性能监控¶
1. Web Vitals监控¶
JavaScript
// 监控Core Web Vitals(web-vitals v4+ API)
import { onCLS, onINP, onLCP } from 'web-vitals';
function sendToAnalytics(metric) {
// 发送到分析服务
analytics.track('Web Vitals', {
name: metric.name,
value: metric.value,
id: metric.id,
});
}
onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);
// 自定义性能指标
function measureCustomMetric() {
const start = performance.now();
// 执行操作
doSomething();
const end = performance.now();
const duration = end - start;
analytics.track('Custom Metric', { duration });
}
2. 性能分析工具¶
2.1 Lighthouse¶
JavaScript
// 使用Lighthouse CLI
lighthouse https://example.com --output html --output-path report.html
// 使用Lighthouse API
// ⚠️ 注意:Lighthouse 12+ 已改用 ESM 导入:import lighthouse from 'lighthouse';
// 且 startFlow / navigation 模式取代了直接调用 lighthouse(url, options)。
// 以下为旧版 API 写法,仅供参考。详见:https://github.com/GoogleChrome/lighthouse/blob/main/docs/user-flows.md
const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');
async function runLighthouse(url) { // async定义异步函数;await等待Promise完成
const chrome = await chromeLauncher.launch({ chromeFlags: ['--headless'] }); // await等待异步操作完成
const options = {
logLevel: 'info',
output: 'html',
onlyCategories: ['performance'],
port: chrome.port,
};
const runnerResult = await lighthouse(url, options);
await chrome.kill();
return runnerResult;
}
runLighthouse('https://example.com').then(results => {
console.log('Performance score:', results.lhr.categories.performance.score * 100);
});
2.2 Chrome DevTools¶
JavaScript
// 使用Performance API(Navigation Timing Level 2)
// 测量页面加载时间
window.addEventListener('load', () => {
const [navEntry] = performance.getEntriesByType('navigation'); // 解构赋值:从对象/数组提取值
if (navEntry) {
const pageLoadTime = navEntry.loadEventEnd - navEntry.startTime;
console.log('Page load time:', pageLoadTime);
}
});
// 使用User Timing API
performance.mark('startOperation');
// 执行操作
performance.mark('endOperation');
performance.measure('operation', 'startOperation', 'endOperation');
const measure = performance.getEntriesByName('operation')[0];
console.log('Operation duration:', measure.duration);
📝 练习题¶
1. 基础题¶
题目1:实现一个懒加载组件
JavaScript
import { lazy, Suspense } from 'react';
// 实现懒加载组件
const LazyComponent = lazy(() => { // 箭头函数:简洁的函数语法
// 动态导入组件
});
function App() {
return (
<div>
{/* 使用懒加载组件 */}
</div>
);
}
2. 进阶题¶
题目2:实现一个缓存工具
JavaScript
// 实现缓存工具
class Cache {
constructor() {
// 初始化缓存
}
set(key, value, ttl) {
// 设置缓存
}
get(key) {
// 获取缓存
}
clear() {
// 清除缓存
}
}
// 使用示例
const cache = new Cache(); // const不可重新赋值;let块级作用域变量
cache.set('key', 'value', 60000); // 缓存60秒
console.log(cache.get('key'));
3. 面试题¶
题目3:解释重排和重绘的区别
JavaScript
// 答案要点:
// 1. 重排:元素位置、大小变化,需要重新计算布局
// 2. 重绘:元素外观变化,不需要重新计算布局
// 3. 重排必定导致重绘,重绘不一定导致重排
// 4. 优化方法:
// - 批量修改DOM
// - 使用DocumentFragment
// - 使用transform和opacity进行动画
// - 避免使用table布局
🎯 本章总结¶
本章节全面介绍了前端性能优化的各种技术和策略,包括性能指标、代码分割、缓存策略、渲染优化、网络优化、性能监控等。关键要点:
- 性能指标:理解Core Web Vitals和其他性能指标
- 代码分割:掌握路由级和组件级代码分割
- 缓存策略:掌握浏览器缓存、Service Worker缓存、应用缓存
- 渲染优化:掌握DOM优化、CSS优化、图片优化
- 网络优化:掌握HTTP优化、资源优化、CDN加速
- 性能监控:掌握性能监控工具和分析方法
下一步将深入学习前端工程化技术。