Publié le 2 octobre 2024
Lorsque vous commencez à utiliser une nouvelle fonctionnalité CSS, il est important de comprendre son impact sur les performances de vos sites Web, qu'il soit positif ou négatif. @property
est désormais disponible dans la version de référence. Cet article explore son impact sur les performances et les mesures que vous pouvez prendre pour éviter tout impact négatif.
Analyse comparative des performances du CSS avec PerfTestRunner
Pour évaluer les performances du CSS, nous avons créé la suite de tests "CSS Selector Benchmark". Basé sur la version PerfTestRunner
de Chromium, il évalue l'impact du CSS sur les performances. C'est ce PerfTestRunner
que Blink, le moteur de rendu sous-jacent de Chromium, utilise pour ses tests de performances internes.
Le programme d'exécution inclut une méthode measureRunsPerSecond
qui est utilisée pour les tests. Plus le nombre d'exécutions par seconde est élevé, mieux c'est. Un benchmark measureRunsPerSecond
de base avec cette bibliothèque se présente comme suit:
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
},
});
Chaque option pour measureRunsPerSecond
est décrite par des commentaires dans le bloc de code, la fonction run
étant la partie principale qui est mesurée.
Les benchmarks des sélecteurs CSS nécessitent un arbre DOM
Étant donné que les performances des sélecteurs CSS dépendent également de la taille du DOM, ces benchmarks nécessitent un arbre DOM de taille raisonnable. Au lieu de créer manuellement cette arborescence DOM, elle est générée.
Par exemple, la fonction makeTree
suivante fait partie des benchmarks @property
. Il construit une arborescence de 1 000 éléments, chacun contenant des enfants imbriqués.
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);
Étant donné que les benchmarks de sélecteur CSS ne modifient pas l'arborescence DOM, cette génération d'arborescence n'est exécutée qu'une seule fois, avant l'exécution des benchmarks.
Exécuter un benchmark
Pour exécuter un benchmark qui fait partie de la suite de tests, vous devez d'abord démarrer un serveur Web :
npm run start
Une fois le benchmark lancé, vous pouvez accéder à son URL publiée et exécuter window.startTest()
manuellement.
Pour exécuter ces benchmarks de manière isolée, sans aucune extension ni autre facteur d'intervention, Puppeteer est déclenché à partir de la CLI pour charger et exécuter le benchmark transmis.
Pour ces benchmarks @property
spécifiques, plutôt que d'accéder à la page correspondante à son URL http://localhost:3000/benchmarks/at-rule/at-property.html
, appelez les commandes suivantes dans la CLI :
npm run benchmark at-rule/at-property
La page est alors chargée via Puppeteer, appelle automatiquement window.startTest()
et renvoie les résultats.
Analyse comparative des performances des propriétés CSS
Pour évaluer les performances d'une propriété CSS, vous devez évaluer la rapidité avec laquelle elle peut gérer une invalidation de style et la tâche de recalcul de style ultérieure que le navigateur doit effectuer.
L'invalidation d'un style consiste à indiquer les éléments dont le style doit être recalculé en réponse à une modification du DOM. L'approche la plus simple consiste à invalider tout en réponse à chaque modification.
Dans ce cas, il faut faire la distinction entre les propriétés CSS qui héritent et celles qui ne le font pas.
- Lorsqu'une propriété CSS héritée est modifiée sur un élément ciblé, les styles de tous les éléments du sous-arbre sous l'élément ciblé doivent également être modifiés.
- Lorsqu'une propriété CSS qui n'hérite pas de modifications sur un élément ciblé, seuls les styles de cet élément individuel sont invalidés.
Il serait injuste de comparer des propriétés qui héritent à des propriétés qui ne le font pas. Vous devez donc exécuter deux ensembles de benchmarks :
- Ensemble de benchmarks avec des propriétés héritées.
- Ensemble d'analyses comparatives dont les propriétés n'héritent pas.
Il est important de choisir soigneusement les établissements à comparer. Alors que certaines propriétés (telles que accent-color
) n'invalident que les styles, de nombreuses propriétés (telles que writing-mode
) invalident également d'autres éléments, tels que la mise en page ou la peinture. Vous souhaitez les propriétés qui n'invalident que les styles.
Pour le déterminer, consultez la liste des propriétés CSS de Blink. Chaque propriété possède un champ invalidate
qui indique ce qui est invalidé.
De plus, il est également important de choisir une propriété qui n'est pas marquée comme independent
dans cette liste, car une analyse comparative de cette propriété fausserait les résultats. Les propriétés indépendantes n'ont aucun effet secondaire sur d'autres propriétés ou indicateurs. Lorsque seules les propriétés indépendantes ont changé, Blink utilise un chemin de code rapide qui clone le style du descendant et met à jour les nouvelles valeurs dans cette copie clonée. Cette approche est plus rapide qu'un recalcul complet.
Analyse comparative des performances des propriétés CSS qui héritent
Le premier ensemble de benchmarks se concentre sur les propriétés CSS qui héritent. Il existe trois types de propriétés qui héritent pour tester et comparer les unes aux autres :
- Propriété standard qui hérite :
accent-color
. - Propriété personnalisée non enregistrée :
--unregistered
. - Une propriété personnalisée enregistrée avec
inherits: true
:--registered
.
Les propriétés personnalisées non enregistrées sont ajoutées à cette liste, car elles héritent par défaut.
Comme indiqué précédemment, la propriété qui hérite d'une propriété qui en hérite a été soigneusement sélectionnée afin qu'elle n'invalide que les styles et qu'elle ne soit pas marquée comme independent
.
Pour les propriétés personnalisées enregistrées, seules celles dont le descripteur inherits
est défini sur "true" sont testées lors de cette exécution. Le descripteur inherits
détermine si la propriété hérite des enfants ou non. Peu importe que cette propriété soit enregistrée via CSS @property
ou JavaScript CSS.registerProperty
, car l'enregistrement lui-même ne fait pas partie de l'analyse comparative.
Les benchmarks
Comme indiqué précédemment, la page contenant les benchmarks commence par construire une arborescence DOM afin que la page dispose d'un ensemble suffisamment important de nœuds pour voir l'impact des modifications.
Chaque benchmark modifie la valeur d'une propriété, puis déclenche une invalidation de style. Le benchmark mesure essentiellement le temps nécessaire au prochain calcul de la page pour réévaluer tous ces styles invalidés.
Une fois qu'un benchmark unique est terminé, tous les styles injectés sont réinitialisés afin que le benchmark suivant puisse commencer.
Par exemple, le benchmark qui mesure les performances de la modification du style de --registered
se présente comme suit:
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);
},
});
Les benchmarks qui testent les autres types de propriétés fonctionnent de la même manière, mais ont un bootstrap
vide, car il n'y a pas de propriété à enregistrer.
Les résultats
L'exécution de ces analyses comparatives avec 20 itérations sur un MacBook Pro (Apple M1 Pro) de 2021 avec 16 Go de RAM donne les moyennes suivantes:
- Propriété standard qui hérite (
accent-color
) : 163 exécutions par seconde (soit 6,13 ms par exécution) - Propriété personnalisée non enregistrée (
--unregistered
) : 256 exécutions par seconde (soit 3,90 ms par exécution) - Propriété personnalisée enregistrée avec
inherits: true
(--registered
): 252 exécutions par seconde (soit 3,96 ms par exécution)
Lors de plusieurs exécutions, les analyses comparatives donnent des résultats similaires.
Les résultats montrent que le coût de l'enregistrement d'une propriété personnalisée est très faible comparé à l'enregistrement de la propriété personnalisée. Les propriétés personnalisées enregistrées qui héritent de l'exécution s'exécutent à 98% de la vitesse des propriétés personnalisées non enregistrées. En termes absolus, l'enregistrement de la propriété personnalisée ajoute un surcoût de 0,06 ms.
Effectuer une analyse comparative des performances des propriétés CSS qui n'héritent pas
Les prochaines propriétés de l'analyse comparative sont celles qui n'héritent pas. Il n'existe que deux types de propriétés pouvant être comparés :
- Propriété standard qui n'hérite pas:
z-index
. - Propriété personnalisée enregistrée avec
inherits: false
:--registered-no-inherit
.
Les propriétés personnalisées non enregistrées ne peuvent pas faire partie de ce benchmark, car elles héritent toujours.
Les benchmarks
Les benchmarks sont très similaires aux scénarios précédents. Pour le test avec --registered-no-inherit
, l'enregistrement de propriété suivant est injecté dans la phase bootstrap
du benchmark :
@property --registered-no-inherit {
syntax: "<number>";
initial-value: 0;
inherits: false;
}
Les résultats
Exécuter ces benchmarks avec 20 itérations sur un MacBook Pro 2021 (Apple M1 Pro) avec 16 Go de RAM donne les moyennes suivantes :
- Propriété régulière qui n'hérite pas : 290 269 exécutions par seconde (soit 3,44 µs par exécution)
- Propriété personnalisée enregistrée qui n'hérite pas : 214 110 exécutions par seconde (soit 4,67 µs par exécution)
Le test a été répété plusieurs fois, et voici les résultats typiques.
Ce qui ressort ici, c'est que les propriétés qui n'héritent pas sont beaucoup plus rapides que celles qui héritent. Bien que cela soit prévisible pour les propriétés standards, cela est également vrai pour les propriétés personnalisées.
- Pour les propriétés standards, le nombre d'exécutions est passé de 163 exécutions par seconde à plus de 290 000 exécutions par seconde, soit une augmentation de 1 780 % des performances.
- Pour les propriétés personnalisées, le nombre d'exécutions est passé de 252 exécutions par seconde à plus de 214 000 exécutions par seconde, soit une augmentation de 848 % des performances.
Ce qu'il faut retenir, c'est que l'utilisation de inherits: false
lors de l'enregistrement d'une propriété personnalisée a un impact significatif. Si vous pouvez enregistrer votre propriété personnalisée avec inherits: false
, nous vous recommandons vivement de le faire.
Étalonnage bonus : plusieurs enregistrements de propriétés personnalisées
Un autre élément intéressant à comparer est l'impact d'un grand nombre d'enregistrements de propriétés personnalisées. Pour ce faire, exécutez à nouveau le test avec --registered-no-inherit
,qui effectue 25 000 autres enregistrements de propriétés personnalisées à l'avance. Ces propriétés personnalisées sont utilisées sur :root
.
Ces enregistrements sont effectués à l'étape setup
de l'analyse comparative :
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")}
}`);
},
Le nombre d'exécutions par seconde pour ce benchmark est très similaire au résultat pour "Propriété personnalisée enregistrée qui n'hérite pas" (214 110 exécutions par seconde contre 213 158 exécutions par seconde), mais ce n'est pas ce qui est intéressant. Après tout, il est normal que la modification d'une propriété personnalisée ne soit pas affectée par les enregistrements d'autres propriétés.
L'aspect intéressant de ce test est de mesurer l'impact des enregistrements eux-mêmes. En passant aux outils de développement, vous constatez que 25 000 enregistrements de propriétés personnalisées ont un coût initial de recalcul du style légèrement supérieur à 30ms
. Une fois cette opération effectuée, la présence de ces enregistrements n'a plus d'effet.
Conclusion et points à retenir
En résumé, il y a trois points à retenir de tout cela:
L'enregistrement d'une propriété personnalisée avec
@property
entraîne un léger coût de performances. Ce coût est souvent négligeable, car en enregistrant des propriétés personnalisées, vous développez tout leur potentiel, ce qui n'est pas possible sans cela.L'utilisation de
inherits: false
lors de l'enregistrement d'une propriété personnalisée a un impact significatif. Vous empêchez ainsi l'héritage de la propriété. Par conséquent, lorsque la valeur de la propriété change, elle n'affecte que les styles de l'élément correspondant, et non l'ensemble du sous-arbre.Le nombre d'enregistrements
@property
n'a aucune incidence sur le recalcul des styles. Les enregistrements ne vous coûtent que très peu de frais à l'avance, mais une fois cette opération effectuée, vous n'avez rien d'autre à faire.