《从轮播图实现,学会如何观察页面的“渲染”与“合成”》(附 Performance 实测对比)》

40 阅读6分钟

大家好,我是【小奇腾】,相信大家在前端的路上一定会遇到性能瓶颈的问题。可是谈了半天性能瓶颈,但是不知道怎么下手,也不知道从哪里开始。那么今天我和大家一起来探索这个奇妙的过程。也希望小伙伴一起支持加油下!!!

本期详细的视频教程bilibili:《从轮播图实现,学会如何观察页面的“渲染”与“合成”》(附 Performance 实测对比)》

一、 缘起:一个轮播图引发的思考

需求效果
实现一个垂直轮播图,
每隔 2 秒,图片向上滚动一次

效果看起来差不多,但是性能却不一样,有两种实现方案:

  1. 直觉派:修改 margin-top 或者 top 属性,把图片“挤”上去。
  2. 优化派:使用 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 的部份

  1. 观察 Margin-top 方案

大家看上图的 Main (主线程) 区域:

  • 在Main的部份圈红色的 密密麻麻的“绿色波峰”是什么?鼠标放上去,全是 Layout (布局) / Update Layer Tree / Paint
  • 因为修改 margin 会改变元素的几何位置,浏览器必须重新计算布局(Reflow),这会阻塞主线程。
  1. 观察 Transform 方案

image.png

  • Main 线程几乎是一条直线!
  • 没有 Layout,没有 Paint,只有零星的动画帧触发。
  • 这是因为 transform 将元素提升为了合成层 (Composite Layer) ,动画的平移工作直接交给了 GPU 处理,主线程完全解放了出来。

关于性能更多内容(顶部的视频链接)有具体的演示.

五、 总结:为什么 Transform 性能更好?

通过这次观测,我们验证了浏览器渲染流水线的核心差异:

维度Margin-top / TopTransform
触发机制修改几何属性修改合成属性
重排 (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>