Cómo realizar comparativas del rendimiento de la propiedad @ de CSS

Fecha de publicación: 2 de octubre de 2024

Cuando comienzas a usar una nueva función de CSS, es importante comprender su impacto en el rendimiento de tus sitios web, ya sea positivo o negativo. Ahora que @property está en Baseline, en esta publicación se explora su impacto en el rendimiento y lo que puedes hacer para evitar un impacto negativo.

Cómo comparar el rendimiento del CSS con PerfTestRunner

Para comparar el rendimiento del CSS, compilamos el paquete de pruebas "CSS Selector Benchmark". Cuenta con la tecnología de PerfTestRunner de Chromium y compara el impacto en el rendimiento de CSS. Este PerfTestRunner es lo que usa Blink, el motor de renderización subyacente de Chromium, para sus pruebas de rendimiento internas.

El ejecutor incluye un método measureRunsPerSecond que se usa para las pruebas. Cuanto mayor sea la cantidad de ejecuciones por segundo, mejor. Una comparativa básica de measureRunsPerSecond con esta biblioteca se ve de la siguiente manera:

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 opción para measureRunsPerSecond se describe a través de comentarios en el bloque de código, y la función run es la parte principal que se mide.

Las comparativas del selector CSS requieren un árbol del DOM

Debido a que el rendimiento de los selectores CSS también depende del tamaño del DOM, estas comparativas necesitan un árbol del DOM de tamaño decente. En lugar de crear este árbol de DOM de forma manual, se genera.

Por ejemplo, la siguiente función makeTree forma parte de las comparativas de @property. Construye un árbol de 1,000 elementos, cada uno con algunos elementos secundarios anidados dentro.

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);

Debido a que las comparativas del selector CSS no modifican el árbol del DOM, esta generación de árboles se ejecuta solo una vez, antes de que se ejecute cualquiera de las comparativas.

Cómo ejecutar comparativas

Para ejecutar una comparativa que forma parte del conjunto de pruebas, primero debes iniciar un servidor web:

npm run start

Una vez que comiences, puedes visitar las comparativas en su URL publicada y ejecutar window.startTest() de forma manual.

Para ejecutar estas comparativas de forma independiente, sin que intervengan extensiones ni otros factores, Puppeteer se activa desde la CLI para cargar y ejecutar la comparativa pasada.

Para estas comparativas de @property específicamente, en lugar de visitar la página relevante en su URL http://localhost:3000/benchmarks/at-rule/at-property.html, invoca los siguientes comandos en la CLI:

npm run benchmark at-rule/at-property

Esto carga la página a través de Puppeteer, llama automáticamente a window.startTest() y muestra los resultados.

Cómo realizar comparativas del rendimiento de las propiedades CSS

Para comparar el rendimiento de una propiedad CSS, comparas la rapidez con la que puede controlar una invalidación de estilo y la tarea de recálculo de estilo posterior que debe realizar el navegador.

La invalidación de estilo es el proceso de marcar qué elementos necesitan que se vuelva a calcular su estilo en respuesta a un cambio en el DOM. El enfoque más simple posible es invalidar todo en respuesta a cada cambio.

Cuando lo hagas, debes distinguir entre las propiedades de CSS que heredan y las que no.

  • Cuando una propiedad de CSS que hereda cambios en un elemento de destino, también deben cambiar los estilos de posiblemente todos los elementos del subárbol debajo del elemento de destino.
  • Cuando una propiedad CSS que no hereda cambios en un elemento objetivo, solo se invalidan los estilos de ese elemento individual.

Dado que no sería justo comparar propiedades que heredan con propiedades que no lo hacen, hay dos conjuntos de comparativas para ejecutar:

  • Un conjunto de comparativas con propiedades que heredan.
  • Un conjunto de comparativas con propiedades que no se heredan.

Es importante elegir cuidadosamente qué propiedades comparar. Si bien algunas propiedades (como accent-color) solo invalidan los estilos, hay muchas propiedades (como writing-mode) que también invalidan otros elementos, como el diseño o la pintura. Quieres las propiedades que solo invalidan los estilos.

Para determinar esto, busca en la lista de propiedades CSS de Blink. Cada propiedad tiene un campo invalidate que enumera lo que se invalida.

Además, también es importante elegir una propiedad que no esté marcada como independent de esa lista, ya que las comparativas de esa propiedad sesgarian los resultados. Las propiedades independientes no tienen efectos secundarios en otras propiedades ni marcas. Cuando solo cambiaron las propiedades independientes, Blink usa una ruta de código rápida que clona el estilo del descendiente y actualiza los valores nuevos en esa copia clonada. Este enfoque es más rápido que realizar un recálculo completo.

Cómo realizar comparativas del rendimiento de las propiedades de CSS que se heredan

El primer conjunto de comparativas se enfoca en las propiedades CSS que heredan. Existen tres tipos de propiedades que se heredan para probar y comparar entre sí:

  • Una propiedad normal que hereda: accent-color.
  • Una propiedad personalizada no registrada: --unregistered.
  • Una propiedad personalizada registrada con inherits: true: --registered.

Las propiedades personalizadas no registradas se agregan a esta lista porque se heredan de forma predeterminada.

Como se mencionó antes, la propiedad que hereda se eligió cuidadosamente para que sea una que solo invalide los estilos y que no esté marcada como independent.

En cuanto a las propiedades personalizadas registradas, solo se prueban en esta ejecución las que tienen el descriptor inherits configurado como verdadero. El descriptor inherits determina si la propiedad hereda a los elementos secundarios o no. No importa si esta propiedad está registrada a través de CSS @property o JavaScript CSS.registerProperty, ya que el registro en sí no forma parte de la comparativa.

Las comparativas

Como ya se mencionó, la página que contiene las comparativas comienza construyendo un árbol del DOM de modo que la página tenga un conjunto de nodos lo suficientemente grande como para ver el impacto de los cambios.

Cada comparativa cambia el valor de una propiedad y, luego, activa una invalidación de estilo. Básicamente, la comparativa mide cuánto tiempo tarda el próximo recálculo de la página en volver a evaluar todos esos estilos invalidados.

Una vez que se realiza una sola comparativa, los estilos insertados se restablecen para que pueda comenzar la siguiente.

Por ejemplo, la comparativa que mide el rendimiento de cambiar el estilo de --registered se ve de la siguiente manera:

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);
  },
});

Las comparativas que prueban los otros tipos de propiedades funcionan de la misma manera, pero tienen un bootstrap vacío porque no hay ninguna propiedad para registrar.

Los resultados

Ejecutar estas comparativas con 20 iteraciones en una MacBook Pro 2021 (Apple M1 Pro) con 16 GB de RAM arroja los siguientes promedios:

  • Propiedad normal que hereda (accent-color): 163 ejecuciones por segundo (= 6.13 ms por ejecución)
  • Propiedad personalizada no registrada (--unregistered): 256 ejecuciones por segundo (= 3.90 ms por ejecución)
  • Propiedad personalizada registrada con inherits: true (--registered): 252 ejecuciones por segundo (= 3.96 ms por ejecución)

En varias ejecuciones, las comparativas producen resultados similares.

Gráfico de barras con los resultados de las propiedades que se heredan. Los números más altos funcionan más rápido.
Figura: Gráfico de barras con los resultados de las propiedades que heredan. Los números más altos tienen un rendimiento más rápido.

Los resultados muestran que registrar una propiedad personalizada tiene un costo muy bajo en comparación con no registrarla. Las propiedades personalizadas registradas que se heredan se ejecutan al 98% de la velocidad de las propiedades personalizadas no registradas. En números absolutos, el registro de la propiedad personalizada agrega una sobrecarga de 0.06 ms.

Cómo obtener comparativas del rendimiento de las propiedades CSS que no heredan

Las siguientes propiedades para comparar son aquellas que no se heredan. Aquí solo hay dos tipos de propiedades de las que se pueden hacer comparativas:

  • Una propiedad normal que no hereda: z-index.
  • Una propiedad personalizada registrada con inherits: false: --registered-no-inherit.

Las propiedades personalizadas que no están registradas no pueden formar parte de esta comparativa porque esas propiedades siempre heredan.

Las comparativas

Las comparativas son muy similares a las situaciones anteriores. Para la prueba con --registered-no-inherit, se inserta el siguiente registro de propiedad en la fase bootstrap de la comparativa:

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

Los resultados

La ejecución de estas comparativas con 20 iteraciones en una MacBook Pro (Apple M1 Pro) 2021 con 16 GB de RAM arroja los siguientes promedios:

  • Propiedad normal que no hereda: 290,269 ejecuciones por segundo (= 3.44 µs por ejecución)
  • Propiedad personalizada registrada que no hereda: 214,110 ejecuciones por segundo (= 4.67 µs por ejecución)

La prueba se repitió en varias ejecuciones, y estos fueron los resultados típicos.

Gráfico de barras con los resultados de las propiedades que no heredan. Los números más altos tienen un rendimiento más rápido.
Figura: Gráfico de barras con los resultados de las propiedades que no se heredan. Los números más altos funcionan más rápido.

Lo que destaca aquí es que las propiedades que no heredan tienen un rendimiento mucho más rápido que las que sí lo hacen. Aunque esto era normal en las propiedades normales, esto también se aplica a las propiedades personalizadas.

  • En el caso de las propiedades normales, la cantidad de ejecuciones aumentó de 163 ejecuciones por segundo a más de 290,000 ejecuciones por segundo, lo que representa un aumento del 1,780% en el rendimiento.
  • En el caso de las propiedades personalizadas, la cantidad de ejecuciones aumentó de 252 ejecuciones por segundo a más de 214,000 ejecuciones por segundo, lo que representa un aumento del 848% en el rendimiento.

La conclusión clave detrás de esto es que el uso de inherits: false cuando se registra una propiedad personalizada tiene un impacto significativo. Si puedes registrar tu propiedad personalizada con inherits: false, te recomendamos que lo hagas.

Comparativa adicional: varios registros de propiedades personalizadas

Otro aspecto interesante para comparar es el impacto de tener muchos registros de propiedades personalizadas. Para ello, vuelve a ejecutar la prueba con --registered-no-inherit y realiza 25,000 otros registros de propiedades personalizadas por adelantado. Estas propiedades personalizadas se usan en :root.

Estos registros se realizan en el paso setup de la comparativa:

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")}
  }`);
},

Las ejecuciones por segundo de esta comparativa son muy similares al resultado de "Propiedad personalizada registrada que no hereda" (214,110 ejecuciones por segundo en comparación con 213,158 ejecuciones por segundo), pero esa no es la parte interesante que debes analizar. Después de todo, se espera que el cambio de una propiedad personalizada no se vea afectado por los registros de otras propiedades.

La parte interesante de esta prueba es medir el impacto de los registros en sí. En DevTools, puedes ver que 25,000 registros de propiedades personalizadas tienen un costo inicial de recálculo de estilo de poco más de 30ms. Una vez que se hace esto, la presencia de estos registros no tiene más efecto.

Captura de pantalla de DevTools con el costo de &quot;Recalculate Style&quot; para realizar 25,000 registros de propiedades personalizadas destacados. La información sobre la herramienta indica que se tardó 32.42 ms
Figura: Captura de pantalla de DevTools con el costo de "Recalculate Style" para realizar 25,000 registros de propiedades personalizadas destacados. La información sobre la herramienta indica que se tardó 32.42ms.

Conclusión y conclusiones

En resumen, se pueden extraer tres conclusiones de todo esto:

  • El registro de una propiedad personalizada con @property tiene un costo de rendimiento leve. Este costo suele ser despreciable, ya que, cuando registras propiedades personalizadas, desbloqueas todo su potencial, lo que no es posible lograr sin hacerlo.

  • El uso de inherits: false cuando se registra una propiedad personalizada tiene un impacto significativo. Con ella, evitas que la propiedad herede. Por lo tanto, cuando cambia el valor de la propiedad, solo afecta a los diseños del elemento coincidente, en lugar de a todo el subárbol completo.

  • Tener pocos o muchos registros de @property no afecta el recálculo del diseño. El pago por adelantado es muy pequeño para realizar los registros, pero no se preocupe si lo hace una vez que lo hace.