Publicado em 2 de outubro de 2024
Ao começar a usar um novo recurso do CSS, é importante entender o impacto dele na performance dos seus sites, seja positivo ou negativo. Com o @property
agora no valor de referência, esta postagem explica o impacto dele no desempenho e o que você pode fazer para evitar um impacto negativo.
Comparação da performance do CSS com PerfTestRunner
Para comparar a performance do CSS, criamos o pacote de testes "CSS Selector Benchmark". Ele usa a tecnologia do PerfTestRunner
do Chromium e compara o impacto no desempenho de CSS. Esse PerfTestRunner
é usado nos testes internos de desempenho do Blink, o mecanismo de renderização subjacente do Chromium.
O executor inclui um método measureRunsPerSecond
usado nos testes. Quanto maior o número de execuções por segundo, melhor. Um comparativo de mercado measureRunsPerSecond
básico com essa biblioteca fica assim:
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
},
});
Cada opção para measureRunsPerSecond
é descrita por comentários no bloco de código, com a função run
sendo a parte principal que é medida.
Os comparativos de mercado do seletor de CSS exigem uma árvore do DOM
Como a performance dos seletores CSS também depende do tamanho do DOM, esses comparativos precisam de uma árvore DOM de tamanho decente. Em vez de criar manualmente essa árvore DOM, ela é gerada.
Por exemplo, a função makeTree
a seguir faz parte dos comparativos de mercado @property
. Ele constrói uma árvore de 1.000 elementos, cada um com alguns filhos aninhados.
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);
Como os comparativos de mercado do seletor de CSS não modificam a árvore DOM, essa geração de árvore é executada apenas uma vez, antes que qualquer uma das comparações seja executada.
Como realizar uma comparação
Para executar um comparativo de mercado que faz parte do conjunto de testes, primeiro é necessário iniciar um servidor da Web:
npm run start
Depois de iniciar, acesse a comparação no URL publicado e execute window.startTest()
manualmente.
Para executar esses comparativos de mercado de forma isolada, sem extensões ou outros fatores, o Puppeteer é acionado pela CLI para carregar e executar o comparativo de mercado transmitido.
Para estes comparativos de mercado do @property
especificamente, em vez de acessar a página relevante no URL http://localhost:3000/benchmarks/at-rule/at-property.html
, invoque os seguintes comandos na CLI:
npm run benchmark at-rule/at-property
Isso carrega a página pelo Puppeteer, chama automaticamente window.startTest()
e informa os resultados.
Comparativo de mercado do desempenho das propriedades do CSS
Para comparar a performance de uma propriedade CSS, você compara a rapidez com que ela pode processar uma invalidação de estilo e a tarefa de recálculo de estilo subsequente que o navegador precisa fazer.
A invalidação de estilo é o processo de marcar quais elementos precisam ter o estilo recalculado em resposta a uma alteração no DOM. A abordagem mais simples possível é invalidar tudo em resposta a cada alteração.
Ao fazer isso, é preciso distinguir entre as propriedades CSS que herdam e as que não herdam.
- Quando uma propriedade CSS que herda mudanças em um elemento de destino, os estilos de todos os elementos no subárvore abaixo do elemento de destino também precisam mudar.
- Quando uma propriedade CSS que não herda mudanças em um elemento de destino, apenas os estilos desse elemento individual são invalidados.
Como não seria justo comparar propriedades que herdam com as que não herdam, há dois conjuntos de comparativos de mercado para executar:
- Um conjunto de comparativos de mercado com propriedades que são herdadas.
- Um conjunto de comparativos de mercado com propriedades que não são herdadas.
É importante escolher com cuidado quais propriedades usar como referência. Embora algumas propriedades (como accent-color
) invalidem apenas estilos, há muitas propriedades (como writing-mode
) que também invalidam outras coisas, como layout ou pintura. Você quer as propriedades que invalidam apenas os estilos.
Para determinar isso, procure as informações na lista de propriedades CSS do Blink. Cada propriedade tem um campo invalidate
que lista o que é invalidado.
Além disso, também é importante escolher uma propriedade que não esteja marcada como independent
nessa lista, já que a comparação dessa propriedade pode distorcer os resultados. As propriedades independentes não têm efeitos colaterais em outras propriedades ou flags. Quando apenas propriedades independentes mudaram, o Blink usa um caminho de código rápido que clona o estilo do descendente e atualiza os novos valores na cópia clonada. Essa abordagem é mais rápida do que fazer um recálculo completo.
Comparativo de mercado da performance de propriedades CSS que são herdadas
O primeiro conjunto de comparativos de mercado se concentra nas propriedades CSS herdadas. Há três tipos de propriedades que herdam para testar e comparar entre si:
- Uma propriedade regular herdada:
accent-color
. - Uma propriedade personalizada não registrada:
--unregistered
. - Uma propriedade personalizada registrada com
inherits: true
:--registered
.
As propriedades personalizadas não registradas são adicionadas a essa lista porque são herdadas por padrão.
Como mencionado anteriormente, a propriedade que herda foi escolhida com cuidado para que invalide apenas estilos e não seja marcada como independent
.
Quanto às propriedades personalizadas registradas, apenas aquelas com o descritor inherits
definido como verdadeiro são testadas nessa execução. O descritor inherits
determina se a propriedade herda ou não os filhos. Não importa se a propriedade está registrada pelo CSS @property
ou JavaScript CSS.registerProperty
, já que o registro em si não faz parte do comparativo de mercado.
Os comparativos de mercado
Como já mencionado, a página que contém os comparativos de mercado começa construindo uma árvore DOM para que a página tenha um conjunto de nós grande o suficiente para mostrar o impacto das mudanças.
Cada comparativo muda o valor de uma propriedade e aciona uma invalidação de estilo. O comparativo basicamente mede quanto tempo leva para o próximo recálculo da página reavaliar todos os estilos invalidados.
Depois que um único comparativo é concluído, todos os estilos injetados são redefinidos para que o próximo comparativo possa começar.
Por exemplo, o comparativo de mercado que mede o desempenho da mudança do estilo de --registered
é assim:
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);
},
});
Os comparativos de mercado que testam os outros tipos de propriedade funcionam da mesma maneira, mas têm um bootstrap
vazio porque não há propriedade a ser registrada.
Os resultados
A execução desses comparativos com 20 iterações em um MacBook Pro 2021 (Apple M1 Pro) com 16 GB de RAM gera as seguintes médias:
- Propriedade regular que herda (
accent-color
): 163 execuções por segundo (= 6,13 ms por execução) - Propriedade personalizada não registrada (
--unregistered
): 256 execuções por segundo (= 3,90 ms por execução) - Propriedade personalizada registrada com
inherits: true
(--registered
): 252 execuções por segundo (= 3,96 ms por execução)
Em várias execuções, os comparativos de mercado produzem resultados semelhantes.
Os resultados mostram que o custo de registrar uma propriedade personalizada é muito pequeno em comparação com o de não registrar. Propriedades personalizadas registradas que herdam a execução em 98% da velocidade das propriedades personalizadas não registradas. Em números absolutos, o registro da propriedade personalizada adiciona uma sobrecarga de 0,06 ms.
Comparação de mercado da performance de propriedades CSS que não são herdadas
As próximas propriedades a serem comparadas são aquelas que não são herdadas. Aqui, há apenas dois tipos de propriedades que podem ser comparadas:
- Uma propriedade comum que não herda:
z-index
. - Uma propriedade personalizada registrada com
inherits: false
:--registered-no-inherit
.
As propriedades personalizadas não registradas não podem fazer parte do comparativo de mercado porque elas são sempre herdadas.
Os comparativos de mercado
As comparações são muito semelhantes aos cenários anteriores. Para o teste com --registered-no-inherit
, o seguinte registro de propriedade é injetado na fase bootstrap
da comparação:
@property --registered-no-inherit {
syntax: "<number>";
initial-value: 0;
inherits: false;
}
Os resultados
A execução desses comparativos com 20 iterações em um MacBook Pro 2021 (Apple M1 Pro) com 16 GB de RAM gera as seguintes médias:
- Propriedade regular que não é herdada: 290.269 execuções por segundo (= 3,44 µs por execução)
- Propriedade personalizada registrada que não herda: 214.110 execuções por segundo (= 4,67μs por execução)
O teste foi repetido em várias execuções, e estes foram os resultados típicos.
O que se destaca aqui é que as propriedades que não herdam têm um desempenho muito mais rápido do que as que herdam. Isso é esperado para propriedades comuns, mas também é válido para propriedades personalizadas.
- Para propriedades regulares, o número de execuções aumentou de 163 para mais de 290 mil por segundo, um aumento de 1780% na performance.
- Para propriedades personalizadas, o número de execuções aumentou de 252 para mais de 214 mil execuções por segundo, um aumento de 848% na performance.
O principal aprendizado é que o uso de inherits: false
ao registrar uma propriedade personalizada tem um impacto significativo. Se você puder registrar sua propriedade personalizada na inherits: false
, com certeza vai precisar fazer isso.
Exemplo de comparação de mercado: vários registros de propriedade personalizada
Outra coisa interessante para comparar é o impacto de ter muitos registros de propriedades personalizadas. Para fazer isso, execute novamente o teste com --registered-no-inherit
e faça outros 25.000 registros de propriedades personalizadas antecipadamente. Essas propriedades personalizadas são usadas em :root
.
Esses registros são feitos na etapa setup
do comparativo de mercado:
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")}
}`);
},
As execuções por segundo desse comparativo são muito semelhantes ao resultado de "Propriedade personalizada registrada que não é herdada" (214.110 execuções por segundo em comparação com 213.158 execuções por segundo), mas essa não é a parte interessante. Afinal, é esperado que a alteração de uma propriedade personalizada não seja afetada pelos registros de outras propriedades.
O interessante desse teste é medir o impacto dos próprios registros. No DevTools, você pode ver que 25.000 registros de propriedades personalizadas têm um custo inicial de recalculo de estilo um pouco acima de 30ms
. Depois disso, a presença desses registros não afeta mais as coisas.
Conclusão e pontos principais
Em resumo, há três pontos principais:
O registro de uma propriedade personalizada com
@property
tem um pequeno custo de desempenho. Esse custo geralmente é insignificante porque, ao registrar propriedades personalizadas, você desbloqueia todo o potencial delas, o que não é possível sem fazer isso.O uso de
inherits: false
ao registrar uma propriedade personalizada tem um impacto significativo. Com ele, você impede a herança da propriedade. Quando o valor da propriedade muda, ele afeta apenas os estilos do elemento correspondente, e não o subárvore inteira.Ter poucos ou muitos registros de
@property
não afeta o recálculo do estilo. Há apenas um custo inicial muito pequeno ao fazer os registros, mas, depois disso, você está pronto.