大家好,我是【小奇腾】,相信大家在前端的路上一定会遇到性能瓶颈的问题。可是谈了半天性能瓶颈,但是不知道怎么下手,也不知道从哪里开始。那么今天我和大家一起来探索这个奇妙的过程。也希望小伙伴一起支持加油下!!!
本期详细的视频教程bilibili:《从轮播图实现,学会如何观察页面的“渲染”与“合成”》(附 Performance 实测对比)》
一、 缘起:一个轮播图引发的思考
| 需求 | 效果 |
|---|---|
| 实现一个垂直轮播图, 每隔 2 秒,图片向上滚动一次 |
效果看起来差不多,但是性能却不一样,有两种实现方案:
- 直觉派:修改
margin-top或者top属性,把图片“挤”上去。 - 优化派:使用 CSS3 的
transform: translateY,把图片“移”上去。
二、 选手入场:两种代码实现 (代码见 -> 代码附录 最底部)
为了控制变量,我准备了两个简单的 Demo,HTML 结构完全一致,唯独驱动动画的方式不同。
方案 A:使用 Margin-top(低性能模拟)
// 修改 margin-top,触发文档流变化
wrapper.style.marginTop = `${offset}px`;
方案 B:使用 Transform(高性能推荐)
// 修改 transform,开启硬件加速
wrapper.style.transform = `translateY(${offset}px)`;
接下来,我们用数据说话。
三、 第一关:视觉观测(Rendering 面板)
Chrome 的开发者工具里隐藏着一个神器叫 Rendering(渲染) 。
如何打开:
F12->Cmd/Ctrl + Shift + P-> 输入Rendering-> 选择Show Rendering。
我勾选了 Paint flashing (突出显示绘制区域) / Enable automatic dark mode (暗黑模式) ,这个功能的作用是:只要页面上有像素被重绘,就闪烁绿色。
| 设置 | 效果 |
|---|---|
1. 观察 Margin-top 方案
当图片滚动时,我被吓了一跳: (在此处插入你那张【有绿色大边框】的截图)
现象:每次轮播切换,整个容器甚至周边区域都疯狂闪烁绿光。
结论:这说明浏览器在进行大面积的重绘 (Repaint) 。CPU 正在辛苦地重新计算每一个像素的颜色。
2. 观察 Transform 方案
接着我切换到 Transform 写法: (在此处插入你那张【完全没有绿框】的截图)
现象:画面静如止水,图片在动,但没有绿光闪烁(或者只有极微小的区域)。
结论:浏览器没有重绘!它偷懒了?不,它是变聪明了。
|
四、 第二关:深度扫描(Performance 面板)
如果说绿光只是表象,那 Performance 面板就是“实锤”证据。我开启了 CPU 4x Slowdown(模拟低端设备),分别录制了两次滚动的过程。
1. 打开 CPU 4x slowdown
2. 打开 performance中的录制功能
红色那个圆圈的就是录制按钮,录制7秒左右就可以了,然后点击stop,看结果
3. 关键看 Main 的部份
- 观察 Margin-top 方案
大家看上图的 Main (主线程) 区域:
- 在Main的部份圈红色的 密密麻麻的“绿色波峰”是什么?鼠标放上去,全是 Layout (布局) / Update Layer Tree / Paint。
- 因为修改
margin会改变元素的几何位置,浏览器必须重新计算布局(Reflow),这会阻塞主线程。
- 观察 Transform 方案
- Main 线程几乎是一条直线!
- 没有 Layout,没有 Paint,只有零星的动画帧触发。
- 这是因为
transform将元素提升为了合成层 (Composite Layer) ,动画的平移工作直接交给了 GPU 处理,主线程完全解放了出来。
关于性能更多内容(顶部的视频链接)有具体的演示.
五、 总结:为什么 Transform 性能更好?
通过这次观测,我们验证了浏览器渲染流水线的核心差异:
| 维度 | Margin-top / Top | Transform |
|---|---|---|
| 触发机制 | 修改几何属性 | 修改合成属性 |
| 重排 (Reflow) | ✅ 触发 (计算布局,最慢) | ❌ 不触发 |
| 重绘 (Repaint) | ✅ 触发 (重新画像素,慢) | ❌ 不触发 |
| 执行位置 | CPU (主线程) | GPU (合成线程) |
| 性能表现 | 容易卡顿 | 丝滑流畅 |
简单理解:
-
Margin-top 就像是你要搬家,你把房子拆了(Reflow),然后再一块块砖砌到新位置(Repaint)。
-
Transform 就像是你拍了一张房子的照片,然后用幻灯片把这张照片投影到新位置。房子没动,只是投影动了,所以极快。
六、代码附录
1. Margin-Top方案(低性能)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>垂直轮播图演示 - Margin-Top方案(低性能)</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { display: flex; justify-content: center; align-items: center; height: 100vh; background: #f0f2f5; }
.viewport {
width: 300px;
height: 200px;
border: 5px solid #333;
border-radius: 10px;
overflow: hidden;
position: relative;
background: #fff;
/* 注意:这里不需要 will-change 了,因为我们就是在模拟没有优化的情况 */
}
.wrapper {
width: 100%;
/* ❌ 性能杀手:这里我们要改变 margin-top */
/* 浏览器必须重新计算布局,因为 margin 改变会影响文档流 */
transition: margin-top 0.5s ease-in-out;
}
.slide {
width: 100%;
height: 200px;
display: flex;
justify-content: center;
align-items: center;
font-size: 40px;
color: white;
font-weight: bold;
}
.slide:nth-child(1) { background-color: #FF5733; }
.slide:nth-child(2) { background-color: #33FF57; }
.slide:nth-child(3) { background-color: #3357FF; }
</style>
</head>
<body>
<div class="viewport">
<div class="wrapper">
<div class="slide">1</div>
<div class="slide">2</div>
<div class="slide">3</div>
</div>
</div>
<script>
const wrapper = document.querySelector('.wrapper');
const imgHeight = 200;
const totalSlides = 3;
let currentIndex = 0;
setInterval(() => {
currentIndex++;
if (currentIndex >= totalSlides) {
currentIndex = 0;
}
const offset = -(currentIndex * imgHeight);
// ❌ 核心区别在这里:
// 我们修改的是 marginTop,而不是 transform
// 这会触发布局重排 (Reflow)
wrapper.style.marginTop = `${offset}px`;
console.log(`当前使用 margin-top 偏移: ${offset}px`);
}, 2000);
</script>
</body>
</html>
2. Transform方案 (性能好)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>垂直轮播图演示 - Transform方案</title>
<style>
/* 简单的重置 */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { display: flex; justify-content: center; align-items: center; height: 100vh; background: #f0f2f5; }
/* 1. 视口:固定高度,像一个相框 */
.viewport {
width: 300px;
height: 200px; /* 视口高度 */
border: 5px solid #333;
border-radius: 10px;
overflow: hidden; /* 关键:把超出的部分切掉 */
position: relative;
background: #fff;
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}
/* 2. 渲染层/包裹层:用来移动的长条 */
.wrapper {
width: 100%;
/* 这里不需要设置高度,内容撑开即可 */
/* 关键性能优化:过渡动画 */
transition: transform 0.5s ease-in-out;
/* 提示浏览器开启 GPU 加速 */
will-change: transform;
}
/* 3. 图片样式:为了演示方便,我用带颜色的div代替图片 */
.slide {
width: 100%;
height: 200px; /* 必须和视口高度一致 */
display: flex;
justify-content: center;
align-items: center;
font-size: 40px;
color: white;
font-weight: bold;
}
.slide:nth-child(1) { background-color: #FF5733; } /* 红 */
.slide:nth-child(2) { background-color: #33FF57; } /* 绿 */
.slide:nth-child(3) { background-color: #3357FF; } /* 蓝 */
</style>
</head>
<body>
<div class="viewport">
<div class="wrapper">
<div class="slide">1</div>
<div class="slide">2</div>
<div class="slide">3</div>
</div>
</div>
<script>
const wrapper = document.querySelector('.wrapper');
const imgHeight = 200; // 单张图片高度
const totalSlides = 3;
let currentIndex = 0;
setInterval(() => {
// 1. 逻辑计算:下一次是第几张?
currentIndex++;
// 简单的回滚逻辑:如果到底了,就瞬间跳回第一张
if (currentIndex >= totalSlides) {
currentIndex = 0;
}
// 2. 核心计算:偏移量 = 索引 * 单张高度
// 加上负号是因为由于坐标系原点在左上角,往上移动是负方向
const offset = -(currentIndex * imgHeight);
// 3. 渲染:应用 Transform
// 注意:这里完全没有用到 scrollTop,只用了数学计算
wrapper.style.transform = `translateY(${offset}px)`;
console.log(`当前索引: ${currentIndex}, 偏移量: ${offset}px`);
}, 2000);
</script>
</body>
</html>