1. 什么是回流与重绘?
回流(Reflow):浏览器的大工程
回流是当渲染树(RenderTree)中部分或全部元素的尺寸、结构或某些属性发生改变时,浏览器重新计算元素位置和几何属性的过程。简单来说,就是浏览器需要重新计算页面的布局。
// 触发回流的常见操作
document.body.appendChild(newElement); // DOM结构变化
element.style.width = "100px"; // 直接修改样式
element.classList.add("new-class"); // 可能改变元素尺寸的类
重绘(Repaint):浏览器的小工程
当页面元素样式改变,但不影响其在文档流中的位置时(如颜色、背景、visibility 等),浏览器只需要重新绘制该元素,这个过程就叫重绘。
// 只触发重绘的操作
element.style.color = "red"; // 颜色变化
element.style.backgroundColor = "#f5f5f5"; // 背景色变化
element.style.visibility = "hidden"; // 可见性变化(但仍占空间)
2. 回流比重绘更消耗性能,为什么?
是不是好奇为什么大家都说回流比重绘更消耗性能?我们来看看浏览器渲染页面的完整过程:
- 解析 HTML,构建 DOM 树
- 解析 CSS,构建 CSSOM 树
- 将 DOM 树和 CSSOM 树结合,形成渲染树(RenderTree)
- 布局(Layout):计算每个节点在屏幕上的确切位置和大小
- 绘制(Paint):将计算好的节点绘制到屏幕上
回流会触发布局和绘制两个步骤,而重绘只触发绘制步骤。所以回流的代价更高!
3. 触发回流的方式有哪些?
以下这些操作都会触发回流,开发时要特别注意:
1. 页面首次渲染
<!DOCTYPE html>
<html>
<head>
<title>首次渲染</title>
<script>
// 记录开始时间
const startTime = performance.now();
window.onload = function () {
// 计算渲染耗时
const loadTime = performance.now() - startTime;
console.log(`页面渲染耗时: ${loadTime.toFixed(2)}ms`);
// 网页渲染速度直接关系到用户体验和留存率
// 据统计,页面每慢0.1秒,可能损失1000万用户!
};
</script>
</head>
<body>
<div>大量内容...</div>
<!-- 页面内容 -->
</body>
</html>
2. 浏览器窗口大小改变
// 监听窗口大小变化,会触发回流
window.addEventListener("resize", function () {
// 这里的代码会在每次窗口大小变化时执行
// 如果在这里操作DOM,可能会导致性能问题
console.log("窗口大小改变,触发回流");
// 优化方案:使用防抖函数限制回流频率
clearTimeout(window.resizeTimer);
window.resizeTimer = setTimeout(function () {
console.log("窗口大小调整完毕,执行一次回流操作");
// 在这里执行真正需要的DOM操作
}, 250);
});
3. 元素尺寸或位置发生改变
const box = document.getElementById("box");
// 不好的方式:多次触发回流
function badAnimation() {
for (let i = 0; i < 100; i++) {
box.style.left = i + "px"; // 每次循环都会触发一次回流
}
}
// 好的方式:使用CSS transitions,只触发一次回流,后续由GPU处理
function goodAnimation() {
box.style.transition = "left 1s ease";
box.style.left = "100px"; // 只触发一次回流
}
// 更好的方式:使用transform,可能完全不触发回流(走单独的图层)
function bestAnimation() {
box.style.transform = "translateX(100px)"; // 可能不触发主文档回流
}
4. 元素内容的变化
const container = document.querySelector(".container");
// 糟糕写法:每次添加子元素都会触发回流
function badAppend() {
for (let i = 0; i < 1000; i++) {
const div = document.createElement("div");
div.textContent = `Item ${i}`;
container.appendChild(div); // 每次都触发回流!
}
}
// 优化写法:使用文档片段,只触发一次回流
function goodAppend() {
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const div = document.createElement("div");
div.textContent = `Item ${i}`;
fragment.appendChild(div);
}
container.appendChild(fragment); // 只触发一次回流
}
5. display: none 与 display: block 之间的切换
const modal = document.getElementById("modal");
// 触发回流的显示/隐藏方式
function toggleWithReflow() {
if (modal.style.display === "none") {
modal.style.display = "block"; // 触发回流
} else {
modal.style.display = "none"; // 同样触发回流
}
}
// 减少回流的方式
function toggleWithoutReflow() {
// 通过提前计算和缓存元素尺寸,减少回流次数
if (!modal.cached) {
modal.cached = {
width: modal.offsetWidth,
height: modal.offsetHeight,
};
}
if (modal.style.display === "none") {
modal.style.display = "block";
// 直接设置缓存的尺寸,避免浏览器重新计算
modal.style.width = modal.cached.width + "px";
modal.style.height = modal.cached.height + "px";
} else {
modal.style.display = "none";
}
}
6. 字体大小的变化
const article = document.querySelector(".article");
// 不好的方式:直接修改字体大小,触发整篇文章的回流
function changeFontSize(size) {
article.style.fontSize = size + "px"; // 触发回流
}
// 更好的方式:使用CSS类切换
function changeFontSizeWithClass(size) {
// 先移除所有字体大小类
article.classList.remove("font-small", "font-medium", "font-large");
// 添加对应大小的类
article.classList.add(`font-${size}`);
}
// CSS定义
/*
.font-small { font-size: 12px; }
.font-medium { font-size: 16px; }
.font-large { font-size: 20px; }
*/
7. 激活 CSS 伪类
<style>
.button {
padding: 10px 20px;
background-color: #3498db;
color: white;
transition: transform 0.2s;
}
/* 这种hover效果会触发回流,因为改变了元素尺寸 */
.button:hover {
padding: 15px 25px; /* 改变了尺寸,触发回流 */
}
/* 这种hover效果只会触发重绘,性能更好 */
.button-better:hover {
background-color: #2980b9; /* 只改变颜色,只触发重绘 */
}
/* 使用transform的hover效果,可能完全不触发回流 */
.button-best:hover {
transform: scale(1.1); /* 使用GPU加速,可能不触发回流 */
}
</style>
<button class="button">按钮1(会触发回流)</button>
<button class="button-better">按钮2(只触发重绘)</button>
<button class="button-best">按钮3(可能不触发回流)</button>
8. 查询某些属性或调用某些方法
const image = document.querySelector(".product-image");
// 下面的代码会强制浏览器回流,因为需要获取最新的布局信息
function badMeasure() {
// 频繁读取会导致强制回流
console.log(image.offsetWidth);
doSomething();
console.log(image.offsetHeight);
doSomethingElse();
console.log(image.getBoundingClientRect());
}
// 更好的方式:缓存布局信息,避免多次回流
function goodMeasure() {
// 读取一次,缓存所有需要的值
const width = image.offsetWidth;
const height = image.offsetHeight;
const rect = image.getBoundingClientRect();
// 使用缓存的值进行后续操作
doSomething(width);
doSomethingElse(height);
moreOperations(rect);
}
4. table 布局为什么不推荐使用?
咱们来探讨一个实际案例。看这段代码:
<table>
<tr>
<td class="sidebar">左侧边栏</td>
<td class="main">主侧内容</td>
<td class="sidebar">右侧边栏</td>
</tr>
</table>
<script>
// 表格中的一个小改动会导致整个表格回流
function updateTableContent() {
document.querySelector(".main").textContent = "新的内容";
// 这个小改动会导致整个表格重新计算布局!
}
// 更现代的布局方式:Flexbox
/*
<div class="flex-container">
<div class="sidebar">左侧边栏</div>
<div class="main">主侧内容</div>
<div class="sidebar">右侧边栏</div>
</div>
*/
</script>
这种布局方式现在很少使用,为什么呢?
- 回流成本高:table 中任何一个单元格的改变,都会导致整个表格的重新布局
- 语义不合适:table 本应用于表格数据,不是用来做页面布局的
- 灵活性差:难以响应式适配不同设备
5. 性能优化实战技巧
说干就干,直接上干货,这些技巧可以立即应用到你的项目中:
技巧 1:使用 CSS3 硬件加速
.accelerated {
transform: translateZ(0);
will-change: transform;
}
transform、opacity 等属性在单独的图层中,不会触发主文档的回流。
技巧 2:避免频繁操作样式
// 糟糕
for (let i = 0; i < 100; i++) {
element.style.top = i + "px"; // 100次回流!
}
// 优秀
let fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
let child = document.createElement("div");
fragment.appendChild(child);
}
element.appendChild(fragment); // 只有一次回流
技巧 3:使用 visibility 而非 display
<!-- 对比两种隐藏方式 -->
<div class="box vis_hid">使用visibility:hidden,只触发重绘</div>
<div class="box dis_none">使用display:none,会触发回流</div>
visibility:hidden 只会导致重绘,而 display:none 会触发回流,因为它会改变页面布局。
6. 浏览器的渲染过程:从输入 URL 到像素呈现
想要真正掌握回流与重绘,我们必须深入理解浏览器的完整渲染流程。这个过程比很多人想象的要复杂得多,让我们一步步剖析:
第一阶段:资源获取与解析
1. 输入 URL,浏览器发起请求
// 这一步在浏览器内部进行
GET https://example.com HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 ...
2. 下载 HTML 文档
// 网络请求示例
fetch("https://example.com")
.then((response) => response.text())
.then((htmlString) => {
// 此时获取到的是原始字节,需要转换为字符串
console.log("HTML字节数:", new Blob([htmlString]).size);
// 浏览器会根据Content-Type或<meta charset>将字节转为字符
});
3. HTML 解析
<!-- 浏览器会解析这些标签和属性 -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div class="container">...</div>
<script src="app.js"></script>
</body>
</html>
第二阶段:构建渲染树
4. CSS 处理流程
/* 下载的CSS文件会经过以下处理 */
body {
margin: 0;
}
.container {
display: flex;
}
/* 1. 下载字节码 (Content-Type: text/css) */
/* 2. 使用UTF-8等编码解析成文本 */
/* 3. 词法分析,分解为token */
/* 4. 构建CSS规则节点 */
/* 5. 最终形成CSSOM树 */
5. 构建渲染树(RenderTree)
// 伪代码表示DOM和CSSOM如何合并成渲染树
function createRenderTree(domNode, styleRules) {
// 跳过不可见元素,如<head>或display:none的元素
if (isNotVisible(domNode, styleRules)) {
return null;
}
// 应用样式规则到DOM节点
const renderNode = {
domNode: domNode,
computedStyle: computeStyles(domNode, styleRules),
children: [],
};
// 递归处理子节点
for (let child of domNode.children) {
const childRenderNode = createRenderTree(child, styleRules);
if (childRenderNode) {
renderNode.children.push(childRenderNode);
}
}
return renderNode;
}
// DOM树 + CSSOM树 = 渲染树
const renderTree = createRenderTree(document.documentElement, cssomRules);
第三阶段:布局与分层
6. 布局(Layout)计算
// 伪代码示意布局过程
function calculateLayout(renderNode, parentBounds) {
// 计算盒模型尺寸
renderNode.box = {
width: calculateWidth(renderNode, parentBounds),
height: calculateHeight(renderNode, parentBounds),
x: calculateXPosition(renderNode, parentBounds),
y: calculateYPosition(renderNode, parentBounds)
};
// 递归计算子节点的布局
const childBounds = {
width: renderNode.box.width,
height: renderNode.box.height,
x: renderNode.box.x,
y: renderNode.box.y
};
for (let child of renderNode.children) {
calculateLayout(child, childBounds);
}
}
// 生成Layout树
calculateLayout(renderTree, viewport);
7. 图层(Layer)创建
/* 以下CSS属性可能会创建新的图层 */
/* z-index较高的元素 */
.modal {
z-index: 999;
}
/* position:fixed的元素 */
.header {
position: fixed;
top: 0;
}
/* CSS3动画和变换 */
.animated {
transition: transform 0.3s;
transform: translateZ(0); /* 强制创建新图层 */
}
/* 使用will-change提示浏览器 */
.optimized {
will-change: transform, opacity; /* 告诉浏览器这些属性会变化 */
}
8. GPU 加速与合成优化
/* GPU加速示例 - 这些CSS属性通常会由GPU处理 */
.gpu-accelerated {
/* 使用3D变换触发GPU加速 */
transform: translate3d(0, 0, 0);
/* 或使用其他3D变换 */
transform: translateZ(0);
transform: rotate3d(0, 0, 1, 45deg);
/* opacity变化也通常由GPU处理 */
transition: opacity 0.3s ease;
}
/* 真实项目中的动画优化 */
@keyframes slide-in {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
.optimized-animation {
animation: slide-in 0.5s ease forwards;
/* 这种动画会在单独的合成层中进行,不触发主文档的回流 */
}
第四阶段:绘制与合成
9. 图层绘制与合成
// 伪代码示意图层处理过程
function paintLayers(layers) {
// 每个图层分别绘制
const paintedLayers = layers.map((layer) => {
return paintLayer(layer);
});
// 合成图层(考虑z-index等)
return compositeLayers(paintedLayers);
}
// 在真实浏览器中,这一步由渲染引擎完成
// 比如Chrome的Blink,Firefox的Gecko
// 最终输出像素到屏幕
实际示例:单图层 vs 多图层
单图层渲染(可能频繁触发回流):
<div class="container">
<div class="box" id="animatedBox">我会导致回流</div>
</div>
<script>
const box = document.getElementById("animatedBox");
// 这种动画会触发主文档回流
setInterval(() => {
box.style.width = parseInt(getComputedStyle(box).width) + 1 + "px";
}, 16); // 约60fps
</script>
多图层渲染(避免主文档回流):
<div class="container">
<div class="box gpu-layer" id="optimizedBox">我不会导致回流</div>
</div>
<style>
.gpu-layer {
transform: translateZ(0); /* 创建单独图层 */
}
</style>
<script>
const box = document.getElementById("optimizedBox");
// 这种动画不会触发主文档回流,由GPU直接处理
setInterval(() => {
// transform由GPU在单独图层处理,主线程可以专注其他任务
const currentX = parseFloat(
getComputedStyle(box).transform.split(",")[4] || 0
);
box.style.transform = `translateZ(0) translateX(${currentX + 1}px)`;
}, 16);
</script>
这个复杂的渲染流程展示了为什么我们需要关注回流与重绘。当你修改 DOM 或 CSS 时,浏览器可能需要重新执行这一系列昂贵的操作。合理利用图层、GPU 加速和其他优化手段,可以显著提高页面性能和用户体验。
总结
通过深入理解浏览器的渲染机制,我们可以更有针对性地优化前端性能。回流和重绘是性能优化的关键瓶颈,减少它们的发生是提升用户体验的重要手段。
记住一句话:每少一次回流,用户体验就多一分流畅;每多用一次 GPU 加速,动画就多一分丝滑。
你们的项目中是否遇到过因回流导致的性能问题?有什么独特的解决方案?欢迎在评论区分享讨论!