浏览器渲染流水线
理解浏览器如何将 HTML/CSS/JS 变成像素,是做性能优化的基础。
渲染流水线全景
HTML → DOM Tree ─┐
├→ Render Tree → Layout → Paint → Composite → 屏幕
CSS → CSSOM ─┘
详细步骤:
1. Parse HTML → DOM Tree
2. Parse CSS → CSSOM Tree
3. Combine → Render Tree(只含可见节点)
4. Layout → 计算每个节点的几何信息(位置、大小)
5. Paint → 生成绘制指令(不是真正绘制)
6. Composite → 合成层,GPU 渲染到屏幕关键渲染路径
HTML 解析(Parser)
├── 遇到 <script>(无 async/defer)→ 阻塞解析,下载并执行 JS
├── 遇到 <link rel="stylesheet"> → 不阻塞解析,但阻塞渲染
└── 遇到 <img> → 异步下载,不阻塞
优化关键渲染路径:
<link rel="stylesheet"> 放 <head>(尽早加载 CSS)
<script> 放 </body> 前,或使用 defer/async
减少关键资源数量和大小script 加载策略
html
<!-- 阻塞解析:下载 + 执行完才继续解析 HTML -->
<script src="app.js"></script>
<!-- async:下载不阻塞,下载完立即执行(执行时阻塞解析)-->
<!-- 适合:独立脚本,如统计代码 -->
<script async src="analytics.js"></script>
<!-- defer:下载不阻塞,HTML 解析完后按顺序执行 -->
<!-- 适合:依赖 DOM 的脚本,保证执行顺序 -->
<script defer src="app.js"></script>
<!-- type="module":默认 defer 行为 -->
<script type="module" src="app.js"></script>重排(Reflow)与重绘(Repaint)
重排(Layout):几何属性变化,需要重新计算布局
触发:width/height/margin/padding/position/display/font-size...
代价:最高(影响整个渲染树)
重绘(Paint):外观变化,不影响布局
触发:color/background/border-color/visibility/box-shadow...
代价:中等
合成(Composite):只在合成层操作
触发:transform/opacity(在合成层上)
代价:最低(GPU 处理,不影响主线程)强制同步布局(Layout Thrashing)
js
// ❌ 读写交替:每次读取都强制触发重排
for (const el of elements) {
const width = el.offsetWidth // 读:强制重排
el.style.width = width * 2 + 'px' // 写:标记脏
}
// 每次循环都触发重排,性能极差
// ✅ 批量读,批量写
const widths = elements.map(el => el.offsetWidth) // 批量读
elements.forEach((el, i) => {
el.style.width = widths[i] * 2 + 'px' // 批量写
})
// 只触发一次重排
// ✅ 使用 requestAnimationFrame 批处理
function updateLayout() {
requestAnimationFrame(() => {
// 在下一帧统一处理 DOM 操作
elements.forEach(el => { /* ... */ })
})
}合成层(Composite Layer)
css
/* 触发合成层的属性 */
.will-animate {
/* 提升为合成层,transform/opacity 动画不触发重排重绘 */
will-change: transform;
/* 或 */
transform: translateZ(0); /* 老方法,hack */
}
/* ✅ 高性能动画:只用 transform 和 opacity */
.slide-in {
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from { transform: translateX(-100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
/* ❌ 低性能动画:触发重排 */
.bad-animation {
animation: badSlide 0.3s ease;
}
@keyframes badSlide {
from { left: -100px; } /* 触发重排! */
to { left: 0; }
}虚拟滚动
jsx
// 长列表优化:只渲染可视区域内的元素
function VirtualList({ items, itemHeight = 50, containerHeight = 500 }) {
const [scrollTop, setScrollTop] = useState(0)
const startIndex = Math.floor(scrollTop / itemHeight)
const endIndex = Math.min(
startIndex + Math.ceil(containerHeight / itemHeight) + 1,
items.length
)
const visibleItems = items.slice(startIndex, endIndex)
const totalHeight = items.length * itemHeight
const offsetY = startIndex * itemHeight
return (
<div
style={{ height: containerHeight, overflow: 'auto' }}
onScroll={e => setScrollTop(e.target.scrollTop)}
>
{/* 撑开滚动高度 */}
<div style={{ height: totalHeight, position: 'relative' }}>
{/* 只渲染可见项 */}
<div style={{ transform: `translateY(${offsetY}px)` }}>
{visibleItems.map((item, i) => (
<div key={startIndex + i} style={{ height: itemHeight }}>
{item.content}
</div>
))}
</div>
</div>
</div>
)
}Web Vitals 核心指标
LCP(Largest Contentful Paint)— 最大内容绘制
目标:< 2.5s
优化:预加载关键资源、优化图片、减少服务器响应时间
FID(First Input Delay)→ INP(Interaction to Next Paint,2024 替代)
目标:INP < 200ms
优化:减少长任务、代码分割、Web Worker
CLS(Cumulative Layout Shift)— 累积布局偏移
目标:< 0.1
优化:为图片/视频指定尺寸、避免动态插入内容
FCP(First Contentful Paint)— 首次内容绘制
目标:< 1.8s
优化:减少渲染阻塞资源、内联关键 CSS
TTFB(Time to First Byte)— 首字节时间
目标:< 800ms
优化:CDN、服务器优化、缓存策略总结
- 渲染流水线:Parse → Style → Layout → Paint → Composite
- 重排代价最高,重绘次之,合成层操作最快
- 动画只用
transform和opacity,触发合成而非重排 - 避免强制同步布局:批量读取,批量写入
- 关注 Core Web Vitals:LCP、INP、CLS