人人都会AI编程

18.4 浏览器渲染性能优化方向

更新时间:2026-07-11

渲染性能的优化并非零散技巧的堆砌,而是围绕一个核心目标展开:尽可能减少主线程在样式计算、布局、绘制和合成上的耗时,保证每一帧都能在 16.7 毫秒内完成。以下优化方向均来源于真实场景,可直接落地。


18.4.1 避免布局抖动:强制同步布局是性能杀手

布局抖动(Layout Thrashing)指的是在代码中交替读写布局属性,导致浏览器反复执行强制同步布局。其本质是:当你读取一个需要计算才能返回的布局属性(如 offsetWidthgetBoundingClientRect())时,如果之前有未应用的样式变更,浏览器必须立即计算布局才能返回准确值,这会打断正常的渲染流程。

经典错误模式:

// 严重布局抖动:每次循环都交叉读取和写入
const items = document.querySelectorAll('.item');
for (let i = 0; i < items.length; i++) {
  const width = items[i].offsetWidth;        // 读
  items[i].style.width = width * 2 + 'px';   // 写
}

正确做法:批量读取,再批量写入

// 先读取所有尺寸
const widths = [];
for (let i = 0; i < items.length; i++) {
  widths.push(items[i].offsetWidth);
}
// 再统一写入
for (let i = 0; i < items.length; i++) {
  items[i].style.width = widths[i] * 2 + 'px';
}

或者使用 requestAnimationFrame 将写入操作推迟到下一帧,也能彻底分离读写阶段。


18.4.2 优先使用 transform 和 opacity 制作动画

现代浏览器将页面分层的核心意义在于:某些属性的变化可以跳过重排(Layout)和重绘(Paint),仅触发合成(Composite)阶段。最常见的合成属性就是 transformopacity

| 属性变化 | 触发阶段 | 性能代价 |
|------------------|---------------------|----------|
| left/top/width | 布局 → 绘制 → 合成 | 高 |
| color/background | 绘制 → 合成 | 中 |
| transform/opacity | 仅合成 | 低 |

实例对比:

/* 不推荐:移动元素会触发重排和重绘 */
.move-bad {
  transition: left 0.3s;
  left: 100px;
}

/* 推荐:仅合成层变换,不触发布局和绘制 */
.move-good {
  transition: transform 0.3s;
  transform: translateX(100px);
}

应用场景包括:滚动视差、拖拽排序、页面转场、骨架屏闪烁效果等,都应优先选用这两个属性。


18.4.3 降低重排的影响范围

无法完全避免重排时,应该想办法让它的成本最小化。

  1. 将样式修改集中到一次操作中

避免逐条设置 style,使用 classList 切换类名。

   // 差:触发多次样式计算
   el.style.width = '100px';
   el.style.height = '50px';
   el.style.margin = '10px';

   // 好:一次修改
   el.classList.add('active-state');
   
  1. 使用文档片段(DocumentFragment)批量插入 DOM

先在内存中构建好所有节点,最后一次插入文档,避免多次触发布局。

   const fragment = document.createDocumentFragment();
   for (let i = 0; i < 1000; i++) {
     const div = document.createElement('div');
     fragment.appendChild(div);
   }
   container.appendChild(fragment);
   
  1. 对复杂计算的布局层进行离线处理

例如对一个元素做大量样式计算时,可以暂时让它 display: none(脱离渲染树),计算完成后再显示。或者使用 cloneNode 克隆一个节点在内存中操作,再将结果回写。


18.4.4 合理利用 will-change 通知浏览器

will-change 属性可以提前告诉浏览器某个元素即将发生变化,浏览器会为该元素预先创建独立的合成层,从而在变化发生时更快地处理。

.will-animate {
  will-change: transform;
}

使用注意:

  • 不要滥用:每个独立合成层都消耗额外的 GPU 内存,海量分层反而可能导致整体性能下降。
  • 用后即删:动画结束后应及时移除 will-change,释放资源。
  • 只用于会频繁变化的属性,如渲染长列表滚动、持续动画等。

18.4.5 对高频事件进行防抖和节流

scrollresizemousemovetouchmove 等事件的触发频率极高(可达每秒几十次),如果直接在事件回调中修改样式,将导致大量布局计算堆积,极易造成丢帧。

  • 节流(Throttle):固定时间间隔执行一次。适用于需要持续更新但不必每次响应的场景(如滚动时的吸顶导航)。
  • 防抖(Debounce):在事件停止触发一段时间后才执行。适用于只需要最终结果的场景(如窗口大小改变后的重新计算、用户输入结束后的搜索)。
function throttle(fn, delay = 16) {
  let lastTime = 0;
  return function (...args) {
    const now = Date.now();
    if (now - lastTime >= delay) {
      lastTime = now;
      fn.apply(this, args);
    }
  };
}

window.addEventListener('scroll', throttle(() => {
  // 滚动处理逻辑
}));

18.4.6 使用 requestAnimationFrame 编排视觉变更

requestAnimationFrame(rAF)是浏览器专门为动画和视觉更新提供的 API。它会在每一帧渲染前执行回调,保证与浏览器刷新率同步,避免了 setTimeout/setInterval 定时不准和不必要的绘制。

function animate() {
  // 更新动画状态
  element.style.transform = `translateX(${x}px)`;
  x++;
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

任何视觉上的变更(位置、大小、颜色、透明度),如果不是通过 CSS 动画/过渡自动完成,都应该放在 rAF 中执行。


18.4.7 对耗时计算进行切片,避免长任务阻塞帧

JavaScript 执行和渲染共用主线程。如果一段同步任务执行时间超过约 50 毫秒,就会明显造成掉帧和页面卡顿。当必须处理大量数据(如排序、DOM 遍历)时,可以将任务拆分为多个小块,分别放进不同的执行周期。

  • 简单切片:使用 setTimeoutrequestIdleCallback 将长任务分段。
  • 使用 requestAnimationFrame 配合协程:将计算分到每一帧的空闲时间。
function processLargeArray(arr, batchSize, callback) {
  let index = 0;
  function doBatch() {
    const end = Math.min(index + batchSize, arr.length);
    for (; index < end; index++) {
      // 处理单个元素
    }
    if (index < arr.length) {
      requestAnimationFrame(doBatch); // 下一帧继续
    } else {
      callback();
    }
  }
  requestAnimationFrame(doBatch);
}

18.4.8 善用工具定位瓶颈

所有优化都应建立在实际测量之上。浏览器开发者工具提供了强大的性能分析能力:

  • Performance 面板:录制一段操作,定位是哪个函数导致了重排、重绘和长任务。
  • Rendering 工具:勾选“Paint Flashing”可以实时看到哪些区域正在被绘制。避免大面积、无意义的绘制。
  • Layers 面板:查看合成层的分布,确保没有意外产生大量冗余分层。
  • Lighthouse:给出页面性能审计报告,包括渲染阻塞资源、布局偏移(CLS)等。

记住:没有度量就没有优化。用数据说话,而不是凭感觉“优化”。


理解渲染性能优化的底层逻辑(重排、重绘、合成)后,下一节我们将聚焦于首屏加载优化与白屏问题——那是用户感知性能的第一道关卡。