發布日期:2024 年 10 月 2 日
開始使用新的 CSS 功能時,請務必瞭解這項功能對網站成效的影響 (無論正面或負面皆是)。@property
現已納入基準,本文將探討其對成效的影響,以及您可以採取哪些措施來避免負面影響。
使用「PerfTestRunner
」基準化 CSS 成效
為評估 CSS 的效能,我們建構了「CSS 選取器基準測試」測試套件。這個工具採用 Chromium 的 PerfTestRunner
,並且會對 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 選取器基準需要 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
啟動後,您可以前往基準測試的發布網址,手動執行 window.startTest()
。
為了在不介入任何擴充功能或其他因素的情況下,單獨執行這些基準測試,Puppeteer 會從 CLI 觸發,以載入並執行傳遞的基準測試。
針對這些 @property
基準測試,請在 CLI 上叫用下列指令,而非造訪其網址 http://localhost:3000/benchmarks/at-rule/at-property.html
的相關網頁:
npm run benchmark at-rule/at-property
這會透過 Puppeteer 載入網頁、自動呼叫 window.startTest()
,並回報結果。
基準測試 CSS 屬性的效能
如要基準測試 CSS 屬性的效能,請測試其處理樣式無效化的速度,以及瀏覽器需要執行的後續樣式重新計算作業。
樣式無效化是指標示哪些元素需要重新計算樣式,以回應 DOM 的變更。最簡單的方法是,在每次變更時讓所有內容失效。
在這種情況下,您必須區分要繼承的 CSS 屬性,以及不繼承的 CSS 屬性。
- 當繼承目標元素變更的 CSS 屬性時,目標元素底下子樹狀結構中所有元素的樣式也需要變更。
- 當 CSS 屬性未繼承目標元素的變更時,只有該個別元素的樣式會失效。
由於將會比較會繼承的資源與不會繼承的資源,因此我們會執行兩組基準:
- 一組具有可繼承屬性的基準。
- 一組基準測試,其中包含不會繼承的屬性。
請務必謹慎選擇要做為基準的資源。雖然某些屬性 (例如 accent-color
) 只會使樣式失效,但許多屬性 (例如 writing-mode
) 也會使版面配置或繪製等其他項目失效。您需要的屬性只會使樣式失效。
如要判斷這項屬性是否適用於 Blink,請查看 Blink 的 CSS 屬性清單。每個資源都有一個 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
。
成果
在 2021 年 MacBook Pro (Apple M1 Pro) 上執行這些基準測試,並重複 20 次,記憶體為 16 GB,可獲得以下平均值:
- 繼承的一般屬性 (
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;
}
成果
在 2021 年 MacBook Pro (Apple M1 Pro) 上執行這些基準測試,並重複 20 次,記憶體為 16 GB,可獲得以下平均值:
- 不會繼承的一般資源:每秒 290,269 次執行 (即每執行 3.44µs)
- 未繼承的註冊自訂屬性:每秒 214,110 次執行 (即每執行一次 4.67µs)
我們多次重複執行這項測試,並取得以下結果。
這裡最明顯的差異在於,不繼承的資源比繼承的資源速度快得多。雖然一般資源會預期這種情況,但自訂資源也會發生這種情況。
- 對於一般資源,執行次數從每秒 163 次提高到每秒 29 萬次以上,效能提升了 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
。完成後,這些註冊項目就不會再對其他項目造成影響。
結語和重點
總而言之,我們從這一切中學到三件事:
透過
@property
註冊自訂屬性會產生些許效能。這筆費用通常可以忽略不計,因為註冊自訂資源可發揮其最大潛力,而這項作業必須註冊自訂資源才能達成。在註冊自訂屬性時使用
inherits: false
有實質影響。否則資源就不會沿用設定。因此,當屬性值變更時,只會影響相符元素的樣式,而不會影響整個子樹狀結構。@property
註冊項目數量多寡不會影響樣式重新計算。註冊時,前期成本只會極低,但作業完成後,您就可以放心了。