性能优化的整体框架#
性能优化不是孤立的技巧堆砌,而是一个系统化的工程。我们需要先建立完整的认知框架:
优化的三个层次:
- 度量层:如何衡量性能(Core Web Vitals、性能指标)
- 分析层:如何定位问题(DevTools、Performance API)
- 优化层:如何解决问题(渲染优化、资源优化、代码优化)
本文将按照”先理解原理 → 再学会度量 → 最后掌握优化”的顺序展开。
一、浏览器渲染原理#
1.1 关键渲染路径#
理解浏览器如何渲染页面是优化的基础:
HTML → DOM Tree
CSS → CSSOM Tree
↓
Render Tree → Layout → Paint → Composite
↑
JavaScriptplaintext完整的渲染流程:
JavaScript → Style → Layout (布局) → Paint (绘制) → Composite (合成)plaintext性能关键点:
- Layout(布局):计算元素的几何属性(位置、尺寸),开销大
- Paint(绘制):填充像素,将元素绘制成位图,开销中等
- Composite(合成):将多个图层合成最终图像,开销小,可由 GPU 加速
1.2 回流与重绘#
回流 (Reflow):元素的几何属性变化,触发 Layout → Paint → Composite,开销最大。
触发回流的操作:
- 修改
width、height、padding、margin、border - 修改
position、top、left - 添加/删除 DOM 节点
- 修改
font-size、font-family - 窗口
resize - 读取布局属性:
offsetWidth、scrollTop、clientHeight、getComputedStyle()等
重绘 (Repaint):元素的视觉样式变化但几何属性不变,跳过 Layout,只触发 Paint → Composite,开销较小。
触发重绘的操作:
- 修改
color、background-color、visibility、box-shadow
优化原则: 避免回流 > 减少重绘 > 只触发合成
1.3 图层与合成#
浏览器将页面分成多个图层(Layer),类似 Photoshop 的图层概念。每个图层独立绘制,最后由 GPU 合成。
触发图层提升的条件:
- 3D 变换:
transform: translate3d(),rotate3d() will-change属性<video>、<canvas>、<iframe>元素position: fixedopacity或transform动画filter属性
transform 和 opacity 的性能秘密:
当元素在独立图层上使用 transform 或 opacity 时,渲染路径缩短为:
JavaScript → Style → Compositeplaintext完全跳过 Layout 和 Paint,由 GPU 直接处理图层变换,轻松达到 60fps。
二、性能度量体系#
2.1 Core Web Vitals(核心 Web 指标)#
Google 提出的三个关键用户体验指标:
LCP (Largest Contentful Paint) - 最大内容绘制
- 含义:视口内最大内容元素的渲染时间
- 目标:≤ 2.5 秒
- 优化方向:优化资源加载、减少渲染阻塞
INP (Interaction to Next Paint) - 交互响应
- 含义:用户交互到下一帧绘制的时间
- 目标:≤ 200 毫秒
- 优化方向:拆分长任务、优化 JavaScript 执行
CLS (Cumulative Layout Shift) - 累积布局偏移
- 含义:页面生命周期内所有意外布局偏移的累积分数
- 目标:≤ 0.1
- 优化方向:为图片/视频设置尺寸、避免动态插入内容
2.2 其他重要指标#
FCP (First Contentful Paint):首次内容绘制,目标 ≤ 1.8 秒
TTI (Time to Interactive):页面完全可交互时间,目标 ≤ 3.8 秒
TBT (Total Blocking Time):总阻塞时间,目标 ≤ 200 毫秒
TTFB (Time to First Byte):首字节时间,目标 ≤ 800 毫秒
2.3 性能分析工具#
Lighthouse - 自动化审计
快速生成性能报告,适合发现问题:
- 打开 DevTools → Lighthouse 面板
- 选择 Performance + Mobile
- 点击 Analyze page load
报告包含:性能总分、核心指标、优化建议(Opportunities)
Performance 面板 - 深入分析
记录页面运行时的详细信息,适合定位问题:
- 打开 Performance 面板
- 点击 Record 或 Start profiling and reload page
- 执行操作后点击 Stop
分析火焰图:
- 红色长条:长任务(>50ms),导致卡顿
- 黄色:JavaScript 执行
- 紫色:Layout 和 Rendering
- 绿色:Paint
- Timings 泳道:查看 LCP 标记
- Experience 泳道:查看 Layout Shift
Performance API - 真实用户监控
// 获取导航时间
const navTiming = performance.getEntriesByType('navigation')[0];
console.log('TTFB:', navTiming.responseStart - navTiming.requestStart);
// 自定义性能标记
performance.mark('task-start');
// 执行任务
performance.mark('task-end');
performance.measure('task-duration', 'task-start', 'task-end');
// 监听 LCP
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('LCP:', entry.renderTime || entry.loadTime);
}
});
observer.observe({ entryTypes: ['largest-contentful-paint'] });javascript三、渲染性能优化#
3.1 动画优化:优先使用 transform 和 opacity#
最佳实践:
/* ✓ 推荐:只触发合成 */
.element {
transform: translateX(100px);
opacity: 0.5;
}
/* ✗ 避免:触发回流和重绘 */
.element {
left: 100px;
width: 200px;
}css动画性能对比:
transform/opacity:60fps,GPU 加速left/top/width/height:可能掉帧,CPU 密集
3.2 will-change:提前优化#
will-change 提示浏览器元素即将变化,提前创建图层。
.animating-element {
will-change: transform, opacity;
}css使用原则:
- 按需使用:动画开始前添加,结束后移除
element.addEventListener('mouseenter', () => {
element.style.willChange = 'transform';
});
element.addEventListener('animationend', () => {
element.style.willChange = 'auto';
});javascript- 不要滥用:每个图层消耗内存,过多图层反而降低性能
- 使用 Layers 面板:检查图层是否合理
3.3 避免回流:批量操作与读写分离#
问题:布局抖动(Layout Thrashing)
// ✗ 错误:每次循环都强制回流
elements.forEach(el => {
const height = el.offsetHeight; // 读:强制回流
el.style.height = height + 10 + 'px'; // 写:标记需要回流
});javascript解决:读写分离
// ✓ 正确:先读后写
const heights = elements.map(el => el.offsetHeight);
elements.forEach((el, i) => {
el.style.height = heights[i] + 10 + 'px';
});javascript批量 DOM 操作:使用 DocumentFragment
// ✗ 错误:每次 appendChild 都可能触发回流
for (let i = 0; i < 100; i++) {
const el = document.createElement('p');
container.appendChild(el);
}
// ✓ 正确:只触发一次回流
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const el = document.createElement('p');
fragment.appendChild(el);
}
container.appendChild(fragment);javascript批量样式修改:使用 classList
// ✗ 避免:多次修改 style
element.style.width = '200px';
element.style.height = '200px';
element.style.border = '2px solid blue';
// ✓ 推荐:一次性切换 class
element.classList.add('large-box');javascript3.4 CSS Containment#
使用 contain 属性隔离元素,限制浏览器的样式、布局和绘制计算范围。
.widget {
contain: layout style paint;
}css四、资源加载优化#
4.1 关键渲染路径优化#
内联关键 CSS
将首屏 CSS 内联到 HTML,避免阻塞渲染:
<head>
<style>
/* 关键 CSS */
.header { background: #fff; height: 60px; }
</style>
<!-- 非关键 CSS 延迟加载 -->
<link rel="preload" href="styles.css" as="style" onload="this.rel='stylesheet'">
</head>html延迟非关键 JavaScript
<!-- 关键 JS:正常加载 -->
<script src="critical.js"></script>
<!-- 非关键 JS:延迟加载 -->
<script src="analytics.js" defer></script>
<script src="widgets.js" async></script>html4.2 预加载技术#
根据资源的重要性和使用时机选择合适的预加载策略:
<!-- preload:当前页面必需,高优先级 -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<!-- prefetch:未来页面可能需要,低优先级 -->
<link rel="prefetch" href="/next-page.js">
<!-- preconnect:提前建立连接(DNS + TCP + TLS) -->
<link rel="preconnect" href="https://cdn.example.com">
<!-- dns-prefetch:仅 DNS 预解析 -->
<link rel="dns-prefetch" href="https://api.example.com">
<!-- modulepreload:预加载 ES 模块 -->
<link rel="modulepreload" href="/app.js">html资源优先级调整:
<img src="hero.jpg" fetchpriority="high">
<script src="analytics.js" fetchpriority="low"></script>html4.3 图片优化#
现代图片格式
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="描述" loading="lazy">
</picture>html响应式图片
<img
srcset="small.jpg 480w, medium.jpg 800w, large.jpg 1200w"
sizes="(max-width: 600px) 480px, (max-width: 900px) 800px, 1200px"
src="medium.jpg"
alt="描述"
loading="lazy">html优化策略:
- 使用 WebP/AVIF 格式,减小 30-50% 体积
- 使用
loading="lazy"懒加载非首屏图片 - 为图片设置明确的
width和height,避免 CLS - 使用 CDN 进行图片优化和压缩
4.4 字体优化#
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2');
font-display: swap; /* 立即显示后备字体 */
}css<!-- 预加载关键字体 -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>htmlfont-display 策略:
swap:立即显示后备字体,加载完成后切换(推荐)optional:短时间内未加载完成则放弃fallback:短暂阻塞后显示后备字体block:阻塞渲染直到字体加载
字体子集化:只包含需要的字符,减小文件大小
4.5 缓存策略#
HTTP 缓存
# 静态资源:强缓存
Cache-Control: public, max-age=31536000, immutable
# HTML:协商缓存
Cache-Control: no-cache
ETag: "abc123"plaintextService Worker 缓存
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
);
});javascript4.6 网络优化#
HTTP/2 优势:
- 多路复用:单连接并行传输
- 头部压缩:减少请求开销
- 服务器推送:主动推送资源
资源压缩:
# Gzip 压缩
gzip on;
gzip_types text/plain text/css application/json application/javascript;
# Brotli 压缩(更高效)
brotli on;
brotli_types text/plain text/css application/json application/javascript;nginx五、JavaScript 执行优化#
5.1 代码分割与懒加载#
动态导入
// 按需加载模块
button.addEventListener('click', async () => {
const module = await import('./heavy-module.js');
module.doSomething();
});javascriptTree Shaking
// package.json
{
"sideEffects": false
}json// 使用 ES6 模块
import { specificFunction } from 'library'; // ✓
const library = require('library'); // ✗javascript5.2 长任务拆分#
任何执行时间超过 50ms 的任务都会阻塞主线程,导致页面无法响应。
使用 setTimeout 拆分
function processLargeArray(array) {
let index = 0;
const chunkSize = 100;
function processChunk() {
const end = Math.min(index + chunkSize, array.length);
for (let i = index; i < end; i++) {
processItem(array[i]);
}
index = end;
if (index < array.length) {
setTimeout(processChunk, 0);
}
}
processChunk();
}javascript使用 requestIdleCallback
requestIdleCallback(deadline => {
while (deadline.timeRemaining() > 0 && tasks.length > 0) {
const task = tasks.shift();
task();
}
});javascript5.3 Web Workers#
将计算密集型任务移到 Worker 线程:
// main.js
const worker = new Worker('worker.js');
worker.postMessage({ data: largeDataSet });
worker.onmessage = (e) => {
console.log('结果:', e.data);
};
// worker.js
self.onmessage = (e) => {
const result = heavyComputation(e.data);
self.postMessage(result);
};javascript5.4 高频事件优化#
防抖(Debounce):延迟执行,重复触发则重新计时
function debounce(fn, delay) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
input.addEventListener('input', debounce(handleInput, 300));javascript节流(Throttle):固定间隔执行
function throttle(fn, interval) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
fn.apply(this, args);
}
};
}
window.addEventListener('scroll', throttle(handleScroll, 100));javascriptrequestAnimationFrame:动画优化
function animate() {
element.style.transform = `translateX(${x}px)`;
if (shouldContinue) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);javascript5.5 虚拟滚动#
长列表优化的核心技术,只渲染可视区域:
class VirtualScroll {
constructor(container, items, itemHeight) {
this.container = container;
this.items = items;
this.itemHeight = itemHeight;
this.visibleCount = Math.ceil(container.clientHeight / itemHeight);
this.startIndex = 0;
this.render();
container.addEventListener('scroll', () => this.onScroll());
}
onScroll() {
this.startIndex = Math.floor(this.container.scrollTop / this.itemHeight);
this.render();
}
render() {
const endIndex = this.startIndex + this.visibleCount;
const visibleItems = this.items.slice(this.startIndex, endIndex);
this.container.innerHTML = visibleItems.map((item, i) =>
`<div style="position: absolute; top: ${(this.startIndex + i) * this.itemHeight}px">
${item}
</div>`
).join('');
this.container.style.height = this.items.length * this.itemHeight + 'px';
}
}javascript六、内存优化#
6.1 常见内存泄漏场景#
// 1. 未清理的定时器
const timer = setInterval(() => {}, 1000);
clearInterval(timer); // 记得清理
// 2. 未解绑的事件监听器
element.addEventListener('click', handler);
element.removeEventListener('click', handler); // 记得解绑
// 3. 闭包引用大对象
function createClosure() {
const largeData = new Array(1000000);
return function() {
console.log(largeData[0]); // largeData 无法被回收
};
}
// 4. 分离的 DOM 引用
let detachedElement = document.getElementById('element');
element.remove();
detachedElement = null; // 手动释放引用javascript6.2 垃圾回收机制#
- 标记清除:现代浏览器主要使用,标记可达对象,清除不可达对象
- 引用计数:旧版 IE 使用,存在循环引用问题
七、移动端优化#
7.1 触摸事件优化#
// 使用 passive 优化滚动性能
element.addEventListener('touchstart', handler, { passive: true });javascript7.2 避免 300ms 点击延迟#
<meta name="viewport" content="width=device-width">htmlhtml {
touch-action: manipulation;
}css八、构建优化#
8.1 Bundle 分析#
# Webpack
npm install --save-dev webpack-bundle-analyzer
# Vite
npm install --save-dev rollup-plugin-visualizerbash8.2 代码分割策略#
- 路由级别分割:每个路由单独打包
- 组件级别分割:大组件按需加载
- 第三方库分割:vendor bundle 单独缓存
九、实战优化场景#
9.1 优化首屏加载#
- 内联关键 CSS,延迟非关键 CSS
- 使用 preload 预加载关键资源
- 图片使用 WebP/AVIF + 懒加载
- 代码分割,只加载首屏必需代码
- 启用 Gzip/Brotli 压缩
- 使用 CDN 加速静态资源
- 服务端渲染(SSR)或静态生成(SSG)
9.2 优化卡顿页面#
- 使用 Performance 面板定位长任务
- 拆分长任务(setTimeout / requestIdleCallback)
- 使用 Web Workers 处理计算密集任务
- 动画使用 transform 和 opacity
- 高频事件使用防抖/节流
- 长列表使用虚拟滚动
9.3 优化无限滚动列表#
- 实现虚拟滚动,只渲染可见元素
- 使用 IntersectionObserver 检测可见性
- 图片懒加载
- 使用 requestAnimationFrame 优化滚动
- 防抖滚动事件处理
9.4 优化大量 DOM 操作#
- 使用 DocumentFragment 批量操作
- 读写分离,避免布局抖动
- 使用 classList 而非直接修改 style
- 隐藏元素后操作,完成后再显示
- 考虑使用虚拟 DOM 库(React、Vue)
总结#
性能优化是一个系统工程,需要:
- 建立度量体系:关注 Core Web Vitals,使用 Lighthouse 和 Performance 面板
- 理解渲染原理:掌握回流、重绘、合成的区别,优先使用 transform 和 opacity
- 优化关键路径:减少阻塞资源,优化首屏加载
- 优化 JavaScript:拆分长任务,使用 Web Workers,代码分割
- 持续监控:使用 Performance API 收集真实用户数据
过早优化是万恶之源。先度量,找到瓶颈,再针对性优化。