对 CSS @property 的性能进行基准测试

发布时间:2024 年 10 月 2 日

开始使用新的 CSS 功能时,请务必了解其对网站性能的影响(无论是正面还是负面)。@property 现已纳入基准组,本文将探讨其对效果的影响,以及您可以采取哪些措施来帮助防止出现负面影响。

为了对 CSS 的性能进行基准测试,我们构建了 “CSS 选择器基准测试”套件。它由 Chromium 的 PerfTestRunner 提供支持,并对 CSS 的性能影响进行基准化分析。Blink(Chromium 的底层渲染引擎)会使用此 PerfTestRunner 进行内部性能测试。

该运行程序包含一个用于测试的 measureRunsPerSecond 方法。每秒运行次数越高越好。使用此库的基本 measureRunsPerSecond 基准测试如下所示:

const testResults = PerfTestRunner.measureRunsPerSecond({
  "Test Description",
  iterationCount: 5,
  bootstrap: function() {
    // Code to execute before all iterations run
    // For example, you can inject a style sheet here
  },
  setup: function() {
    // Code to execute before a single iteration
  },
  run: function() {
    // The actual test that gets run and measured.
    // A typical test adjusts something on the page causing a style or layout invalidation
  },
  tearDown: function() {
    // Code to execute after a single iteration has finished
    // For example, undo DOM adjustments made within run()
  },
  done: function() {
    // Code to be run after all iterations have finished.
    // For example, remove the style sheets that were injected in the bootstrap phase
  },
});

代码块中的注释介绍了 measureRunsPerSecond 的每个选项,其中 run 函数是被测核心部分。

CSS 选择器基准测试需要 DOM 树

由于 CSS 选择器的性能也取决于 DOM 的大小,因此这些基准测试需要大小适中的 DOM 树。系统会生成此 DOM 树,而不是手动创建此树。

例如,以下 makeTree 函数是 @property 基准测试的一部分。它会构建一个包含 1,000 个元素的树,每个元素中都嵌套了一些子元素。

const $container = document.querySelector('#container');

function makeTree(parentEl, numSiblings) {
  for (var i = 0; i <= numSiblings; i++) {
    $container.appendChild(
      createElement('div', {
        className: `tagDiv wrap${i}`,
        innerHTML: `<div class="tagDiv layer1" data-div="layer1">
          <div class="tagDiv layer2">
            <ul class="tagUl">
              <li class="tagLi"><b class="tagB"><a href="/" class="tagA link" data-select="link">Select</a></b></li>
            </ul>
          </div>
        </div>`,
      })
    );
  }
}

makeTree($container, 1000);

由于 CSS 选择器基准测试不会修改 DOM 树,因此此树生成操作只会在运行任何基准测试之前执行一次。

运行基准测试

若要运行测试套件中的基准测试,您必须先启动一个网络服务器:

npm run start

启动后,您可以访问基准测试的已发布网址,并手动执行 window.startTest()

为了单独运行这些基准测试(不受任何扩展程序或其他因素的干扰),系统会从 CLI 触发 Puppeteer 来加载和执行传入的基准测试。

具体而言,对于这些 @property 基准测试,请在 CLI 上调用以下命令,而不是访问网址 http://localhost:3000/benchmarks/at-rule/at-property.html相关页面

npm run benchmark at-rule/at-property

这会通过 Puppeteer 加载页面,自动调用 window.startTest(),并报告结果。

对 CSS 属性的性能进行基准测试

要对 CSS 属性的性能进行基准化分析,您可以对它处理样式失效以及浏览器需要执行的后续重新计算样式任务的速度进行基准化分析。

样式失效是指标记哪些元素需要根据 DOM 中的更改重新计算样式的过程。最简单的方法就是针对每项更改使所有内容失效。

在执行此操作时,需要区分会继承的 CSS 属性和不会继承的 CSS 属性。

  • 当继承的 CSS 属性在目标元素上发生更改时,目标元素下方的子树中可能所有元素的样式也需要更改。
  • 当不继承的 CSS 属性发生更改时,只有该单个元素的样式会失效。

由于比较继承的媒体资源与没有继承的媒体资源不公平,因此需要运行两组基准:

  • 一组具有可继承属性的基准。
  • 一组基准测试,其中包含不继承的属性。

请务必仔细选择要对哪些房源进行基准化分析。虽然某些属性(例如 accent-color)只会使样式失效,但许多属性(例如 writing-mode)也会使布局或绘制等其他内容失效。您需要仅使样式失效的属性。

如需确定这一点,请在 Blink 的 CSS 属性列表中查找相关信息。每个媒体资源都有一个 invalidate 字段,用于列出失效的内容。

此外,从该列表中选择未标记为 independent 的媒体资源也很重要,因为对此类媒体资源进行基准测试会导致结果偏差。独立属性对其他属性或标志没有任何副作用。如果只有独立属性发生了更改,Blink 会使用快速代码路径来克隆子孙的样式,并更新该克隆副本中的新值。这种方法比完全重新计算速度更快。

对继承的 CSS 属性的性能进行基准测试

第一组基准测试重点关注可继承的 CSS 属性。有三种类型的属性会继承以进行测试和相互比较:

  • 继承以下常规媒体资源:accent-color
  • 未注册的自定义媒体资源:--unregistered
  • 已向 inherits: true 注册的自定义属性:--registered

系统会将未注册的自定义媒体资源添加到此列表中,因为这些媒体资源默认会继承父级媒体资源。

如前所述,我们精心选择了要继承的属性,使其仅会使样式失效,并且未被标记为 independent

对于已注册的自定义属性,此运行作业仅会测试将 inherits 描述符设为 true 的属性。inherits 描述符决定该属性是否向下继承到子项。此属性是通过 CSS @property 还是 JavaScript CSS.registerProperty 注册的无关紧要,因为注册本身不属于基准测试的范围。

基准

如前所述,包含基准测试的网页首先会构建一个 DOM 树,以便网页包含足够多的节点,以便查看更改的任何影响。

每个基准测试都会更改某个属性的值,然后触发样式失效。该基准测试基本上衡量的是网页下次重新计算所需的时间,即重新评估所有失效样式的所需时间。

单次基准测试完成后,所有注入的样式都会重置,以便开始下一次基准测试。

例如,衡量更改 --registered 样式的性能的基准如下所示:

let i = 0;
PerfTestRunner.measureRunsPerSecond({
  description,
  iterationCount: 5,
  bootstrap: () => {
    setCSS(`@property --registered {
      syntax: "<number>";
      initial-value: 0;
      inherits: true;
    }`);
  },
  setup: function() {
    // NO-OP
  },
  run: function() {
    document.documentElement.style.setProperty('--registered', i);
    window.getComputedStyle(document.documentElement).getPropertyValue('--registered'); // Force style recalculation
    i = (i == 0) ? 1 : 0;
  },
  teardown: () => {
    document.documentElement.style.removeProperty('--registered');
  },
  done: (results) => {
    resetCSS();
    resolve(results);
  },
});

测试其他类型的属性的基准测试的工作原理相同,但因为没有要注册的属性,所以 bootstrap 为空。

成果

在 RAM 为 16GB 的 2021 年 MacBook Pro (Apple M1 Pro) 上运行 20 次迭代,得到以下平均值:

  • 继承的常规属性 (accent-color):每秒运行 163 次(= 每次运行 6.13 毫秒)
  • 未注册的自定义属性 (--unregistered):每秒 256 次运行(即每次运行 3.90 毫秒)
  • 使用 inherits: true (--registered) 注册的自定义属性:每秒 252 次运行(即每次运行 3.96 毫秒)

多次运行后,基准测试会得出类似的结果。

显示继承属性结果的条形图。数字越高,执行速度越快。
图:包含继承属性相关结果的条形图。数字越高,执行速度越快。

结果显示,与不注册自定义属性相比,注册自定义属性所需的费用非常小。继承的已注册自定义媒体资源的运行速度为未注册自定义媒体资源的 98%。以绝对数字表示,注册自定义属性会增加 0.06 毫秒的开销。

对不继承的 CSS 属性的性能进行基准测试

接下来要对比的属性是不会继承的属性。这里只有两种类型的媒体资源可以进行基准测试:

  • 不继承的常规媒体资源:z-index
  • inherits: false--registered-no-inherit 的已注册自定义媒体资源。

未注册的自定义属性不能纳入此基准,因为这些属性始终会继承。

基准

基准测试与之前的场景非常相似。对于使用 --registered-no-inherit 的测试,系统会在基准的 bootstrap 阶段注入以下属性注册:

@property --registered-no-inherit {
  syntax: "<number>";
  initial-value: 0;
  inherits: false;
}

成果

在 RAM 为 16GB 的 2021 款 MacBook Pro (Apple M1 Pro) 上以 20 次迭代运行这些基准测试,得出以下平均值:

  • 不继承的常规属性:每秒 290,269 次运行(即每次运行 3.44 微秒)
  • 未继承的已注册自定义属性:每秒 214,110 次运行(即每次运行 4.67 微秒)

我们重复了多次测试,获得了以下典型结果。

显示不继承的媒体资源的结果的条形图。数值越高,效果就越快。
图:显示不继承的媒体资源的结果的条形图。数值越高,执行速度越快。

值得注意的是,不继承的属性的执行速度比继承的属性快得多。虽然对于常规媒体资源来说,这是意料之中的,但对于自定义媒体资源也是如此。

  • 对于常规媒体资源,运行次数从每秒 163 次增加到每秒 29 万次以上,性能提升了 1780%!
  • 对于自定义属性,运行次数从每秒 252 次运行增加到每秒超过 214, 000 次,性能提高了 848%!

这背后的要点是,在注册自定义媒体资源时使用 inherits: false 会产生显著影响。如果您可以使用 inherits: false 注册自定义媒体资源,则一定要这样做。

奖励基准:多次注册自定义媒体资源

另一个值得基准测试的有趣事项是,进行大量自定义媒体资源注册的影响。为此,请使用 --registered-no-inherit 重新运行测试,并让其预先执行 25,000 次其他自定义媒体资源注册。这些自定义属性用于 :root

这些注册是在基准测试的 setup 步骤中完成的:

setup: () => {
  const propertyRegistrations = [];
  const declarations = [];

  for (let i = 0; i < 25000; i++) {
    propertyRegistrations.push(`@property --custom-${i} { syntax: "<number>"; initial-value: 0; inherits: true; }`);
    declarations.push(`--custom-${i}: ${Math.random()}`);
  }

  setCSS(`${propertyRegistrations.join("\n")}
  :root {
    ${declarations.join("\n")}
  }`);
},

此基准测试每秒运行次数与“未继承的已注册自定义属性”的结果非常相似(每秒 214,110 次运行与每秒 213,158 次运行),但这并不是我们要关注的重点。毕竟,更改一个自定义媒体资源不会受到其他媒体资源的注册影响,这是意料之中的。

此项测试的有趣之处在于衡量注册本身所产生的影响。在 DevTools 中,您可以看到,25,000 项自定义属性注册的初始样式重新计算开销略高于 30ms。完成后,这些注册的存在对系统没有进一步影响。

此 DevTools 屏幕截图突出显示了执行 25,000 次自定义属性注册的“重新计算样式”费用。提示显示花费了 32.42 毫秒
图:DevTools 屏幕截图,其中突出显示了执行 25,000 次自定义媒体资源注册的“重新计算样式”费用。提示中显示该操作耗时 32.42ms

结论和要点总结

总而言之,我们从这三个要点中总结出了以下三个要点:

  • @property 注册自定义属性会略微降低性能。这笔费用通常可以忽略不计,因为注册自定义媒体资源可充分发挥其潜力,而如果不注册,就无法实现这一点。

  • 在注册自定义媒体资源时使用 inherits: false 会产生显著影响。通过设置键值对,您可以防止继承属性。因此,当属性的值发生变化时,只会影响匹配元素的样式,而不是整个子树。

  • @property 注册数量较少或较多不会影响样式重新计算。注册时只需支付很少的预付费用,完成注册后,您就可以放心了。