Data di pubblicazione: 2 ottobre 2024
Quando inizi a utilizzare una nuova funzionalità CSS, è importante comprenderne l'impatto sul rendimento dei tuoi siti web, positivo o negativo. Ora che @property
è disponibile in Base, questo post illustra il suo impatto sul rendimento e cosa puoi fare per evitare un impatto negativo.
Benchmarking del rendimento del CSS con PerfTestRunner
Per eseguire il benchmark del rendimento del CSS, abbiamo creato la suite di test "CSS Selector Benchmark". È basato su PerfTestRunner
di Chromium e esegue benchmark sull'impatto del CSS sul rendimento. Questo PerfTestRunner
è ciò che Blink, il motore di rendering sottostante di Chromium, utilizza per i suoi test di prestazioni interni.
Il runner include un metodo measureRunsPerSecond
utilizzato per i test. Maggiore è il numero di esecuzioni al secondo, meglio è. Un benchmark measureRunsPerSecond
di base con questa libreria ha il seguente aspetto:
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
},
});
Ogni opzione per measureRunsPerSecond
è descritta tramite commenti nel blocco di codice, con la funzione run
che rappresenta la parte principale misurata.
I benchmark del selettore CSS richiedono un albero DOM
Poiché le prestazioni dei selettori CSS dipendono anche dalle dimensioni del DOM, questi benchmark richiedono un albero DOM di dimensioni adeguate. Anziché creare manualmente l'albero DOM, viene generato questo albero.
Ad esempio, la seguente funzione makeTree
fa parte dei benchmark @property
. Costruisce un albero di 1000 elementi, ciascuno con alcuni elementi secondari nidificati al suo interno.
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);
Poiché i benchmark dei selettori CSS non modificano l'albero DOM, questa generazione dell'albero viene eseguita una sola volta, prima dell'esecuzione di qualsiasi benchmark.
Eseguire un benchmark
Per eseguire un benchmark che fa parte della suite di test, devi innanzitutto avviare un server web:
npm run start
Una volta avviato, puoi visitare il benchmark all'URL pubblicato ed eseguire window.startTest()
manualmente.
Per eseguire questi benchmark in isolamento, senza l'intervento di estensioni o altri fattori, Puppeteer viene attivato dall'interfaccia a riga di comando per caricare ed eseguire il benchmark passato.
Per questi benchmark @property
in particolare, anziché visitare la pagina pertinente al relativo URL http://localhost:3000/benchmarks/at-rule/at-property.html
, richiama i seguenti comandi nell'interfaccia a riga di comando:
npm run benchmark at-rule/at-property
La pagina viene caricata tramite Puppeteer, chiama automaticamente window.startTest()
e restituisce i risultati.
Benchmarking del rendimento delle proprietà CSS
Per eseguire il benchmark del rendimento di una proprietà CSS, devi misurare la velocità con cui può gestire un'invalidazione dello stile e la successiva attività di ricalcolo dello stile che il browser deve eseguire.
L'invalidazione dello stile è il processo di indicazione degli elementi di cui è necessario ricalcolare lo stile in risposta a una modifica nel DOM. L'approccio più semplice possibile è invalidare tutto in risposta a ogni modifica.
A questo scopo, è necessario distinguere tra le proprietà CSS che ereditano e quelle che non lo fanno.
- Quando una proprietà CSS che eredita le modifiche su un elemento target cambia, devono cambiare anche gli stili di potenzialmente tutti gli elementi del sottoalbero sotto l'elemento target.
- Quando una proprietà CSS che non eredita le modifiche su un elemento scelto come target, solo gli stili per quel singolo elemento vengono invalidati.
Poiché non sarebbe giusto confrontare le proprietà che ereditano con quelle che non lo ereditano, puoi eseguire due serie di benchmark:
- Un insieme di benchmark con proprietà che ereditano.
- Un insieme di benchmark con proprietà che non vengono ereditate.
È importante scegliere con cura le proprietà da confrontare. Sebbene alcune proprietà (ad esempio accent-color
) invalidino solo gli stili, esistono molte proprietà (ad esempio writing-mode
) che invalidano anche altri elementi, come il layout o la pittura. Devi selezionare le proprietà che invalidano solo gli stili.
Per determinarlo, consulta l'elenco delle proprietà CSS di Blink. Ogni proprietà ha un campo invalidate
che elenca gli elementi che vengono invalidati.
Inoltre, è importante scegliere una proprietà non contrassegnata come independent
dall'elenco, in quanto il benchmarking di una proprietà di questo tipo potrebbe falsare i risultati. Le proprietà indipendenti non hanno effetti collaterali su altre proprietà o flag. Quando sono state modificate solo le proprietà indipendenti, Blink utilizza un percorso di codice rapido che clona lo stile del discendente e aggiorna i nuovi valori nella copia clonata. Questo approccio è più veloce di un nuovo calcolo completo.
Benchmarking del rendimento delle proprietà CSS che ereditano
Il primo insieme di benchmark si concentra sulle proprietà CSS che ereditano. Esistono tre tipi di proprietà che ereditano per testare e confrontare tra loro:
- Una proprietà normale che eredita:
accent-color
. - Una proprietà personalizzata non registrata:
--unregistered
. - Una proprietà personalizzata registrata con
inherits: true
:--registered
.
Le proprietà personalizzate non registrate vengono aggiunte a questo elenco perché vengono ereditate per impostazione predefinita.
Come accennato in precedenza, la proprietà che eredita è stata scelta con cura in modo da essere una che invalida solo gli stili e una che non è contrassegnata come independent
.
Per quanto riguarda le proprietà personalizzate registrate, in questa esecuzione vengono testate solo quelle con il descrittore inherits
impostato su true. Il descrittore inherits
determina se la proprietà viene ereditata dalle proprietà secondarie o meno. Non importa se questa proprietà è registrata tramite CSS @property
o JavaScript CSS.registerProperty
, poiché la registrazione stessa non fa parte del benchmark.
I benchmark
Come già accennato, la pagina contenente i benchmark inizia con la costruzione di un albero DOM in modo che la pagina abbia un insieme di nodi sufficientemente grande da rilevare l'impatto delle modifiche.
Ogni benchmark modifica il valore di una proprietà dopodiché attiva l'invalidazione di uno stile. Fondamentalmente, il benchmark misura il tempo necessario al successivo ricalcolo della pagina per rivalutare tutti gli stili non più validi.
Al termine di un singolo benchmark, tutti gli stili iniettati vengono reimpostati in modo da poter iniziare il benchmark successivo.
Ad esempio, il benchmark che misura il rendimento della modifica dello stile di --registered
ha il seguente aspetto:
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);
},
});
I benchmark che testano gli altri tipi di proprietà funzionano allo stesso modo, ma hanno un bootstrap
vuoto perché non c'è alcuna proprietà da registrare.
I risultati
L'esecuzione di questi benchmark con 20 iterazioni su un MacBook Pro 2021 (Apple M1 Pro) con 16 GB di RAM restituisce le seguenti medie:
- Proprietà normale che eredita (
accent-color
): 163 esecuzioni al secondo (= 6,13 ms per esecuzione) - Proprietà personalizzata non registrata (
--unregistered
): 256 esecuzioni al secondo (= 3,90 ms per esecuzione) - Proprietà personalizzata registrata con
inherits: true
(--registered
): 252 esecuzioni al secondo (= 3,96 ms per esecuzione)
In più esecuzioni, i benchmark forniscono risultati simili.
I risultati mostrano che la registrazione di una proprietà personalizzata ha un costo molto ridotto rispetto alla mancata registrazione. Le proprietà personalizzate registrate che ereditano vengono eseguite al 98% della velocità delle proprietà personalizzate non registrate. In numeri assoluti, la registrazione della proprietà personalizzata aggiunge un overhead di 0,06 ms.
Benchmarking delle prestazioni delle proprietà CSS che non vengono ereditate
Le proprietà successive da sottoporre a benchmarking sono quelle che non vengono ereditate. Di seguito sono riportati solo due tipi di proprietà che possono essere considerati come benchmark:
- Una proprietà normale che non eredita:
z-index
. - Una proprietà personalizzata registrata con
inherits: false
:--registered-no-inherit
.
Le proprietà personalizzate non registrate non possono far parte di questo benchmark perché vengono sempre ereditate.
I benchmark
I benchmark sono molto simili a quelli degli scenari precedenti. Per il test con --registered-no-inherit
, la seguente registrazione della proprietà viene inserita nella fase bootstrap
del benchmark:
@property --registered-no-inherit {
syntax: "<number>";
initial-value: 0;
inherits: false;
}
I risultati
L'esecuzione di questi benchmark con 20 iterazioni su un MacBook Pro 2021 (Apple M1 Pro) con 16 GB di RAM restituisce le seguenti medie:
- Proprietà regolare che non eredita: 290.269 esecuzioni al secondo (= 3,44 μs per esecuzione)
- Proprietà personalizzata registrata che non eredita: 214.110 esecuzioni al secondo (= 4,67 μs per esecuzione)
Il test è stato ripetuto più volte e questi sono stati i risultati tipici.
La cosa che spicca è che le proprietà che non ereditano hanno un rendimento molto più elevato rispetto a quelle che lo fanno. Questo era normale per le proprietà standard, ma vale anche per le proprietà personalizzate.
- Per le proprietà standard, il numero di esecuzioni è aumentato da 163 esecuzioni al secondo a oltre 290 mila esecuzioni al secondo, con un aumento del rendimento del 1780%.
- Per le proprietà personalizzate, il numero di esecuzioni è aumentato da 252 esecuzioni al secondo a oltre 214 mila esecuzioni al secondo, con un aumento del rendimento dell'848%.
Il concetto chiave è che l'utilizzo di inherits: false
quando si registra una proprietà personalizzata ha un impatto significativo. Se puoi registrare la tua proprietà personalizzata con inherits: false
, ti consigliamo vivamente di farlo.
Benchmark bonus: più registrazioni di proprietà personalizzate
Un altro aspetto interessante da confrontare è l'impatto di un numero elevato di registrazioni di proprietà personalizzate. Per farlo, esegui nuovamente il test con --registered-no-inherit
eseguendo in anticipo 25.000 altre registrazioni di proprietà personalizzate. Queste proprietà personalizzate vengono utilizzate in :root
.
Queste registrazioni vengono effettuate nel passaggio setup
del benchmark:
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")}
}`);
},
Il numero di corse al secondo per questo benchmark è molto simile al risultato per "Proprietà personalizzata registrata che non eredita" (214.110 esecuzioni al secondo rispetto a 213.158 esecuzioni al secondo), ma non è questo l'aspetto interessante da osservare. Dopotutto, è normale che la modifica di una proprietà personalizzata non sia interessata dalle registrazioni di altre proprietà.
La parte interessante di questo test è misurare l'impatto delle registrazioni stesse. Passando a DevTools, puoi vedere che 25.000 registrazioni di proprietà personalizzate hanno un costo di ricalcolo iniziale dello stile di poco più di 30ms
. Una volta completata questa operazione, la presenza di queste registrazioni non ha ulteriori effetti.
Conclusione e punti chiave
In sintesi, ci sono tre concetti chiave da tutto questo:
La registrazione di una proprietà personalizzata con
@property
prevede un leggero costo per le prestazioni. Questo costo è spesso trascurabile perché, registrando le proprietà personalizzate, sfrutti tutto il loro potenziale, il che non è possibile senza questa operazione.L'utilizzo di
inherits: false
durante la registrazione di una proprietà personalizzata ha un impatto significativo. In questo modo impedisci l'ereditarietà della proprietà. Di conseguenza, quando il valore della proprietà cambia, influisce solo sugli stili dell'elemento corrispondente e non sull'intero sottoalbero.Il numero di registrazioni
@property
non influisce sul ricomputo dello stile. Le registrazioni comportano un costo iniziale molto ridotto, ma una volta completate non dovrai più preoccuparti di nulla.