不合理的重排重绘

98 阅读6分钟

start

2025年,回首过往,思绪如窗前的苍蝇,嗡嗡作响。此刻,我打开记忆的窗,倒一倒脑子,看看那些岁月里留下了什么。

基本详情

  • 不合理的重排与重绘会导致浏览器性能下降,尤其在页面结构复杂或操作频繁时,以下是日常开发中常见的不合理重排与重绘情况汇总:

例1:逐个修改样式属性

  • 问题代码:
const ele = document.querySelector('div');
ele.style.width = '100px'; // 触发重排
ele.style.height = '100px'; // 再次触发重排
ele.style.backgroundColor = 'red'; // 触发重绘
  • 问题描述:每次修改样式属性时,浏览器都会触发重排或重绘。上述代码中,width 和 height 的修改分别触发了两次重排。
  • 优化代码:
const ele = document.querySelector("div");
ele.style.cssText = "width: 100px; height: 100px; background-color: red;";
  • 优化描述:从多次修改样式属性,改成一次性修改多个样式属性,减少重排和重绘的次数。

例2:频繁读取布局属性

  • 问题代码:
const ele = document.querySelector('div');
ele.style.left = 1 + ele.offsetLeft + 'px'; // 计算元素布局信息,触发重排
ele.style.top = 1 + ele.offsetTop + 'px'; // 再次计算元素布局信息,再次触发重排
  • 问题描述:每次读取 offsetLeft 或 offsetTop 时,浏览器都会强制刷新渲染队列以获取最新值,从而触发重排。
  • 优化代码:
const ele = document.querySelector('div');
const currentLeft = ele.offsetLeft;
const currentTop = ele.offsetTop;
ele.style.left = 100 + currentLeft + 'px';
ele.style.top = 100 + currentTop + 'px';
  • 优化描述:
    • 将布局信息缓存到局部变量中,避免重复读取,减少计算元素布局信息,减少重排次数。
    • 上面两段代码,优化了两点:
      • 相同点:由于都是独立改变元素信息,所以也都触发了两次重排;
      • 优化一:第二段代码的优点除了继续使用可以得到优化之外
      • 优化二:更重要的是减少了浏览器重复计算布局的次数,从而提升了性能

例3:在循环中频繁修改DOM

  • 问题代码:
for (let i = 0; i < 1000; i++) {
	const ele = document.createElement('div');
	ele.style.width = '10px';
	ele.style.height = '10px';
	document.body.appendChild(ele); // 每次插入都会触发重排和重绘
}
  • 问题描述:在循环中直接向文档中插入元素,会导致浏览器频繁触发重排和重绘。
  • 优化代码:
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
	const ele = document.createElement('div');
	ele.style.width = '10px';
	ele.style.height = '10px';
	fragment.appendChild(ele); // 先将元素添加到文档碎片
}
document.body.appendChild(fragment); // 一次性插入文档
  • 优化描述:使用 DocumentFragment 将所有操作离线完成,最后一次性插入文档,减少重排和重绘的次数

例4:使用 transform 替代布局属性

  • 问题代码:
const ele = document.querySelector('div');
ele.style.left = '100px'; // 触发重排
ele.style.top = '100px'; // 再次触发重排
  • 问题描述:修改 left 和 top 属性会触发重排,因为它们改变了元素的几何位置。
  • 运行注意事项:运行代码不会看到变化,需要条件定位属性position: relative;,这样就可以看到移动现象了
  • 优化代码:
const ele = document.querySelector('div');
ele.style.transform = 'translate(100px, 100px)'; // 只触发重绘
  • 优化描述:使用 transform 属性代替 left 和 top,因为 transform 不会触发重排,只会触发重绘。

实践过程遇到的新思考

ele.style和ele.style.cssText有什么不同

  • 一句话总结:实践中逐步改变ele.style更优,批量改变ele.style.cssText更优

ele.style

  • ele.style 是一个 CSSStyleDeclaration 对象,它提供了直接操作元素样式的接口。通过 ele.style,你可以访问和修改元素的内联样式。
  • 特点:
    • 直接操作样式属性:可以通过点语法(.)或方括号语法([])直接访问和修改具体的样式属性。例如:ele.style.width = '100px'; ele.style['background-color'] = 'red';
    • 动态修改单个样式:适合在运行时动态修改单个样式属性。
    • 不覆盖原有样式:修改一个属性不会影响其他内联样式。例如:ele.style.width = '100px'; // 不会清除其他内联样式
  • 适用场景:
    • 当需要动态修改单个样式属性时。
    • 当需要根据条件动态设置样式时。
  • 实践体验例子:
<style>
    div {
        border: solid 1px red;
    }
</style>
<button>触发改变</button>
<div>div</div>
<script>
    const btn = document.querySelector('button');
    const div = document.querySelector('div');
    let idx = 0;
    btn.addEventListener('click', function () {
        idx++;
        idx %= 6;
        switch (idx) {
            case 1:
                div.style.width = '100px';
                break;
            case 2:
                div.style.height = '100px';
                break;
            case 3:
                div.style.backgroundColor = 'red';
                break;
            case 4:
                div.style = '';
                break;
            case 5:
                div.style = 'width:200px;height:200px;background:purple;color:#fff;font-size:24px;'
                break;
            default:
                div.style = '';
                break;
        }
    })
</script>

ele.style.cssText

  • ele.style.cssText 是一个字符串属性,用于一次性设置或获取元素的所有内联样式。它允许你通过字符串的形式批量设置样式。
  • 特点:
    • 批量设置样式:通过字符串一次性设置多个样式,避免多次操作 DOM。例如:ele.style.cssText = 'width: 100px; height: 100px; background-color: red;';
    • 覆盖原有样式:设置 cssText 时,会完全覆盖元素的内联样式。如果需要保留原有样式,需要手动拼接字符串
    • 性能优化:在批量设置样式时,比逐个设置 ele.style 更高效,因为浏览器只需要解析一次样式规则
  • 适用场景:
    • 当需要批量设置多个样式属性时。
    • 当需要优化性能,减少 DOM 操作次数时。
  • 实际体验例子:
<style>
    div {
        border: solid 1px red;
    }
</style>
<button>触发改变</button>
<div>div</div>
<script>
    const btn = document.querySelector('button');
    const div = document.querySelector('div');
    let idx = 0;
    btn.addEventListener('click', function () {
        idx++;
        idx %= 4;
        switch (idx) {
            case 1:
                div.style.cssText = 'width:100px';
                break;
            case 2:
                div.style.cssText = 'height:100px';
                break;
            case 3:
                div.style.cssText = 'background-color:red';
                break;

            default:
                div.style.cssText = '';
                break;
        }
    })
</script>

启动硬件加速

transform 属性触发 GPU 加速

  • 当使用 transform 时,浏览器会将元素提升到一个独立的渲染层(composite layer),并利用 GPU 进行渲染。
  • 特别是当使用 3D 变换(如 translateZ(0) 或 translate3d(0, 0, 0))时,浏览器会明确地将元素放入 GPU 可处理的图层中,从而触发硬件加速。
  • 作用:可以显著减少 CPU 的负担,提高动画的流畅性。
div {
  transform: translateZ(0); /* 触发 GPU 加速 */
}

will-change 属性触发 GPU 加速

  • will-change 属性用于提前告知浏览器某个元素即将发生变化,从而让浏览器提前进行优化。
  • 当设置 will-change 属性时,浏览器会为该元素创建一个独立的图层,并可能将其渲染任务交给 GPU。
  • 作用:可以让浏览器提前为动画或变化做好准备,减少渲染开销,提高性能。
div {
  will-change: transform; /* 提前告知浏览器元素将要变化 */
}
  • 注意事项:
    • 适度使用:过度使用 GPU 加速可能会导致内存占用过高,尤其是在移动端设备上。
    • 性能优化:虽然 transform 和 will-change 可以提升性能,但应避免滥用,以确保页面的整体性能。

网页使用GPU的场景

  • 图形渲染:3D 渲染、2D 图形加速
  • 动画效果:CSS 动画加速、JavaScript 动画
  • 视频处理:视频解码与播放、实时视频处理
  • GPU技术:WebGL 和 WebGPU
  • 计算密集型任务:Web Workers 和 GPU 计算、机器学习和 AI
  • 数据可视化:大规模数据可视化、实时数据更新
  • 网页游戏:2D 和 3D 游戏、多人在线游戏

总结

  • 优化的基本目标就是:
    • 减少重排重绘的触发次数,从逐个操作优化成批量操作
    • 避免强制刷新渲染队列,避免在布局操作中读取布局属性,利用好变量
    • 使用硬件加速,使用transform或will-change属性触发GPU加速。

end

恳请诸位同仁、挚友、兄弟姐妹不吝赐教,多多指点!