Сравнительный анализ производительности CSS @property

Опубликовано: 2 октября 2024 г.

Начиная использовать новую функцию CSS, важно понимать ее влияние на производительность ваших веб-сайтов, будь то положительное или отрицательное. Теперь, когда @property включен в базовый план, в этом посте рассматривается его влияние на производительность и то, что вы можете сделать, чтобы предотвратить негативное влияние.

Бенчмаркинг производительности CSS с помощью PerfTestRunner

Для оценки производительности CSS мы создали набор тестов «CSS Selector Benchmark» . Он основан на PerfTestRunner от Chromium и оценивает влияние CSS на производительность. PerfTestRunner — это то, что Blink — базовый движок рендеринга Chromium — использует для своих внутренних тестов производительности.

Бегун включает метод 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 Selector требуют дерева DOM

Поскольку производительность селекторов CSS также зависит от размера DOM, для этих тестов требуется дерево DOM приличного размера. Вместо того, чтобы вручную создавать это дерево DOM, создается это дерево.

Например, следующая функция makeTree является частью тестов @property . Он создает дерево из 1000 элементов, в каждый из которых вложено несколько дочерних элементов.

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

После запуска вы можете посетить тест по опубликованному URL-адресу и выполнить window.startTest() вручную.

Чтобы запустить эти тесты изолированно — без каких-либо расширений или других факторов — Puppeteer запускается из CLI для загрузки и выполнения переданного теста.

Специально для этих тестов @property вместо посещения соответствующей страницы по ее URL-адресу http://localhost:3000/benchmarks/at-rule/at-property.html вызовите следующие команды в CLI:

npm run benchmark at-rule/at-property

Это загружает страницу через Puppeteer, автоматически вызывает window.startTest() и возвращает результаты.

Сравнительный анализ производительности свойств CSS

Чтобы оценить производительность свойства CSS, вы оцениваете, насколько быстро оно может обработать аннулирование стиля и последующую задачу перерасчета стиля, которую должен выполнить браузер.

Инвалидация стиля — это процесс маркировки элементов, стиль которых требует перерасчета в ответ на изменение в DOM. Самый простой возможный подход — сделать все недействительным в ответ на каждое изменение.

При этом следует различать свойства CSS, которые наследуются, и свойства CSS, которые не наследуются.

  • Когда свойство CSS, которое наследует изменения в целевом элементе, стили потенциально всех элементов в поддереве под целевым элементом также должны измениться.
  • Когда свойство CSS, которое не наследует изменения в целевом элементе, становятся недействительными только стили для этого отдельного элемента.

Поскольку было бы несправедливо сравнивать свойства, которые наследуются, со свойствами, которые этого не делают, необходимо выполнить два набора тестов:

  • Набор тестов с наследующими свойствами.
  • Набор тестов со свойствами, которые не наследуются.

Важно тщательно выбирать, какие свойства сравнивать. Хотя некоторые свойства (например, accent-color ) делают недействительными только стили, существует множество свойств (например, writing-mode ), которые также делают недействительными другие вещи, такие как макет или рисование. Вам нужны свойства, которые только делают недействительными стили.

Чтобы определить это, поищите информацию в списке свойств CSS Blink . Каждое свойство имеет поле 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 , поскольку нет свойства для регистрации.

Результаты

Выполнение этих тестов с 20 итерациями на MacBook Pro 2021 года (Apple M1 Pro) с 16 ГБ ОЗУ дает следующие средние значения:

  • Обычное наследуемое свойство ( 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;
}

Результаты

Выполнение этих тестов с 20 итерациями на MacBook Pro 2021 года (Apple M1 Pro) с 16 ГБ ОЗУ дает следующие средние значения:

  • Обычное свойство, которое не наследуется: 290 269 запусков в секунду (= 3,44 мкс на запуск)
  • Зарегистрированное пользовательское свойство, которое не наследуется: 214 110 запусков в секунду (= 4,67 мкс на запуск)

Тест повторялся несколько раз, и это были типичные результаты.

Гистограмма с результатами для свойств, которые не наследуются. Чем выше число, тем быстрее.
Рисунок: Гистограмма с результатами для свойств, которые не наследуются. Чем выше число, тем быстрее.

Здесь выделяется то, что свойства, которые не наследуют, работают намного быстрее, чем свойства, которые наследуют. Этого следовало ожидать для обычных свойств, но это справедливо и для пользовательских свойств.

  • Для обычных объектов количество запусков увеличилось со 163 запусков в секунду до более чем 290 тысяч запусков в секунду, что означает увеличение производительности на 1780%!
  • Для пользовательских свойств количество запусков увеличилось с 252 запусков в секунду до более чем 214 тысяч запусков в секунду, что означает увеличение производительности на 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 тысяч пользовательских свойств. В подсказке указано, что это заняло 32,42 мс.
Рисунок: снимок экрана DevTools, на котором выделена стоимость «Пересчитать стиль» для регистрации 25 000 пользовательских свойств. В подсказке указано, что это заняло 32.42ms

Выводы и выводы

Подводя итог всему этому, можно сделать три вывода:

  • Регистрация пользовательского свойства с помощью @property приводит к небольшому снижению производительности. Эти затраты часто незначительны, поскольку, регистрируя пользовательские свойства, вы раскрываете весь их потенциал , чего без этого невозможно достичь.

  • Использование inherits: false при регистрации пользовательского свойства имеет значимое влияние. С его помощью вы предотвращаете наследование имущества. Таким образом, когда значение свойства изменяется, оно влияет только на стили соответствующего элемента, а не на все поддерево.

  • Наличие небольшого или большого количества регистраций @property не влияет на перерасчет стиля. При регистрации требуется лишь очень небольшая первоначальная стоимость, но как только это будет сделано, все будет в порядке.