避免强制同步布局/避免同步抖动

1,148 阅读4分钟

布局是浏览器计算各元素几何信息的过程: 元素的大小以及在页面中的位置。 根据所用的 CSS、元素的内容或父级元素,每个元素都将有显式或隐含的大小信息。此过程在 Chrome、Opera、Safari 和 Internet Explorer 中称为布局 (Layout)。 在 Firefox 中称为自动重排 (Reflow),但实际上其过程是一样的。

与样式计算相似,布局开销的直接考虑因素如下:

  1. 需要布局的元素数量。
  2. 布局的复杂性。
  • 布局的作用范围一般为整个文档。
  • DOM 元素的数量将影响性能;应尽可能避免触发布局。
  • 评估布局模型的性能;新版 Flexbox 一般比旧版 Flexbox 或基于浮动的布局模型更快。
  • 避免强制同步布局和布局抖动;先读取样式值,然后进行样式更改。

参考
示例

尽可能避免布局操作

当您更改样式时,浏览器会检查任何更改是否需要计算布局,以及是否需要更新渲染树。对“几何属性”(如width、height、left或top)的更改都需要布局计算。

.box {
  width: 20px;
  height: 20px;
}

/**
 * Changing width and height
 * triggers layout.
 */
.box--expanded {
  width: 200px;
  height: 350px;
}

布局几乎总是作用到整个文档。  如果有大量元素,将需要很长时间来算出所有元素的位置和尺寸。(占用线程资源)

如果无法避免布局,关键还是要使用 Chrome DevTools 来查看布局要花多长时间,并确定布局是否为造成瓶颈的原因。

查看一个Main主进程任务,Rendering事件占用时间

image.png

查看Layout布局事件的时间

image.png

避免强制同步布局

将一帧送到屏幕会采用如下顺序:

image.png

首先 JavaScript 运行,然后计算样式,然后布局。但是,可以使用 JavaScript 强制浏览器提前执行布局。这被称为强制同步布局

在 JavaScript 运行时,来自上一帧的所有旧布局值是已知的,并且可供您查询。因此,如果您要在帧的开头获取一个元素box的高度,可能编写一些如下代码:

function logBoxHeight() {
  // Gets the height of the box in pixels and logs it out.
  console.log(box.offsetHeight);
}

那如果在获取元素box的高度之前,已更改其样式,就会出现问题(强制同步布局):

function logBoxHeight() {

  box.classList.add('super-big');

  // Gets the height of the box in pixels
  // and logs it out.
  console.log(box.offsetHeight);
}

现在,为了获取元素box的高度,浏览器必须先应用样式更改(因为增加了 super-big 类),然后运行布局。这时,box.offsetHeight 才能返回正确的高度。但是这是不必要的,并且可能是开销很大的工作。

因此,应先批量读取样式并执行(浏览器可以使用上一帧的布局值),然后执行任何写操作:

正确写法应为:

function logBoxHeight() {
  // Gets the height of the box in pixels
  // and logs it out.
  console.log(box.offsetHeight);

  box.classList.add('super-big');
}

大部分情况下,并不需要应用样式然后查询值;使用上一帧的值就足够了。与浏览器同步(或比其提前)运行样式计算和布局可能成为瓶颈,并且您一般不想做这种设计。

避免布局抖动

有一种方式会使强制同步布局甚至更糟: 接二连三地执行大量这种布局。看看这个代码:

function resizeAllParagraphsToMatchBlockWidth() {

  // Puts the browser into a read-write-read-write cycle.
  for (var i = 0; i < paragraphs.length; i++) {
    paragraphs[i].style.width = box.offsetWidth + 'px';
  }
}

此代码循环处理一组段落,并设置每个段落的宽度以匹配同一个称为“box”的元素的宽度。这看起来没有害处,但问题是循环的每次迭代读取一个样式值 (box.offsetWidth),然后立即使用此值来更新段落的宽度 (paragraphs[i].style.width)。在循环的下次迭代时,浏览器必须考虑样式已更改这一事实,因为 offsetWidth 是上次请求的(在上一次迭代中),因此它必须应用样式更改,然后运行布局。每次迭代都将出现此问题!

此示例的修正方法还是先读取值,然后写入值:

// Read. 先读取样式值
var width = box.offsetWidth;

function resizeAllParagraphsToMatchBlockWidth() {
  for (var i = 0; i < paragraphs.length; i++) {
    // Now write. 在更新
    paragraphs[i].style.width = width + 'px';
  }
}

示例代码讲解

截取主要代码

app.update = function (timestamp) {
    for (var i = 0; i < app.count; i++) {
      var m = movers[i];
      if (!app.optimize) { // optimize 作为优化标识
        // 未做优化,发生强制同步布局问题
        var pos = m.classList.contains('down') ?
            m.offsetTop + distance : m.offsetTop - distance;
        if (pos < 0) pos = 0;
        if (pos > maxHeight) pos = maxHeight;
        // 触发同步布局位置
        m.style.top = pos + 'px'; // 更新DOM样式
        if (m.offsetTop === 0) { // 立即读取样式进行比较,这时需要重新计算布局,才能返回正确样式值
          m.classList.remove('up');
          m.classList.add('down');
        }
        if (m.offsetTop === maxHeight) { // 立即读取样式进行比较,这时需要重新计算布局,才能返回正确样式值
          m.classList.remove('down');
          m.classList.add('up');
        }
      } else {
        // 优化,按正常一帧渲染顺序
        var pos = parseInt(m.style.top.slice(0, m.style.top.indexOf('px')));
        m.classList.contains('down') ? pos += distance : pos -= distance;
        if (pos < 0) pos = 0;
        if (pos > maxHeight) pos = maxHeight;
        // 优化差异位置
        m.style.top = pos + 'px'; // 更新DOM样式
        if (pos === 0) { // 未使用新样式值进行比较,而是用上一帧的样式值计算后进行比较,不会发生强制同步布局
          m.classList.remove('up');
          m.classList.add('down');
        }
        if (pos === maxHeight) { // 未使用新样式值进行比较,而是用上一帧的样式值计算后进行比较,不会发生强制同
          m.classList.remove('down');
          m.classList.add('up');
        }
      }
    }
    frame = window.requestAnimationFrame(app.update);
  }
  • 未优化Performance 展示

在Main进程信息下,一个 Task 进程事件包含多个 Layout 布局事件(Paint),同时概览图表可以看到FPS (每秒帧数)出现红色告警(大于60FPS)

image.png

一个Task进程时间统计,rendering占主要部分

image.png

在Event log 下,可以看出触发大量的重复事件,导致渲染性能差

image.png

Button-UP 面板看出 Layout占用时间最多 image.png

  • 优化后Performance 展示

在Main进程信息下,一个 Task 进程事件只有一个 Layout 布局事件(Paint),说明有按照一个帧渲染顺序执行,而且概览图表FPS未出现红色告警

image.png

一个Task进程时间统计,rendering虽然占主要部分,但是明细看到总耗时减少

image.png

在Event log 下,可以看出未触发大量的重复事件,并且 Layout 等布局事件按照顺序执行

image.png

Button-UP 面板看出 Layout占用时间大大减少

image.png