在 Sass 中處理未使用的 CSS 以提高性能

已發表: 2022-03-10
快速總結↬你知道未使用的 CSS 對性能的影響嗎? 劇透:很多! 在本文中,我們將探索一種面向 Sass 的解決方案來處理未使用的 CSS,從而避免需要涉及無頭瀏覽器和 DOM 模擬的複雜 Node.js 依賴項。

在現代前端開發中,開發人員應該致力於編寫可擴展和可維護的 CSS。 否則,隨著代碼庫的增長和更多開發人員的貢獻,他們可能會失去對級聯和選擇器特異性等細節的控制。

實現這一目標的一種方法是使用諸如面向對象的 CSS (OOCSS) 之類的方法,它不是圍繞頁面上下文組織 CSS,而是鼓勵將結構(網格系統、間距、寬度等)與裝飾(字體、品牌、顏色等)。

所以 CSS 類名如:

  • .blog-right-column
  • .latest_topics_list
  • .job-vacancy-ad

被更可重用的替代品取代,這些替代品應用相同的 CSS 樣式,但不依賴於任何特定的上下文:

  • .col-md-4
  • .list-group
  • .card

這種方法通常在 Sass 框架(如 Bootstrap、Foundation)的幫助下實現,或者越來越常見的定制框架可以被塑造成更好地適應項目。

跳躍後更多! 繼續往下看↓

所以現在我們使用從模式、UI 組件和實用程序類的框架中挑選出來的 CSS 類。 下面的例子說明了一個使用 Bootstrap 構建的常見網格系統,它垂直堆疊,然後一旦到達 md 斷點,就會切換到 3 列佈局。

 <div class="container"> <div class="row"> <div class="col-12 col-md-4">Column 1</div> <div class="col-12 col-md-4">Column 2</div> <div class="col-12 col-md-4">Column 3</div> </div> </div>

此處使用以編程方式生成的類(例如.col-12.col-md-4 )來創建此模式。 但是.col-1.col-11.col-lg-4.col-md-6.col-sm-12呢? 這些都是類的示例,它們將包含在編譯的 CSS 樣式表中,由瀏覽器下載和解析,儘管它們沒有被使用。

在本文中,我們將首先探討未使用的 CSS 對頁面加載速度的影響。 然後,我們將討論一些現有的解決方案,將其從樣式表中刪除,然後是我自己的面向 Sass 的解決方案。

衡量未使用的 CSS 類的影響

雖然我喜歡謝菲爾德聯隊,強大的刀片服務器,但他們網站的 CSS 被捆綁到一個 568kb 的壓縮文件中,即使 gzip 壓縮也達到 105kb。 這似乎很多。

這是謝菲爾德聯隊的網站,我當地的足球隊(那是殖民地的足球)。 (大預覽)

我們要看看他們的主頁上實際使用了多少這個 CSS 嗎? 快速的 Google 搜索顯示了很多在線工具可以勝任這項工作,但我更喜歡使用 Chrome 中的覆蓋工具,它可以直接從 Chrome 的 DevTools 運行。 讓我們試一試。

在開發人員工具中訪問覆蓋工具的最快方法是使用鍵盤快捷鍵 Control+Shift+P 或 Command+Shift+P (Mac) 打開命令菜單。 在其中輸入coverage ,然後選擇“顯示覆蓋率”選項。 (大預覽)

結果顯示主頁僅使用了 568kb 樣式表中的30kb CSS,其餘 538kb 與網站其餘部分所需的樣式有關。 這意味著高達94.8%的 CSS 未被使用。

您可以通過網絡在開發者工具中的 Chrome 中查看任何資產的類似時間 -> 單擊您的資產 -> 時間選項卡。 (大預覽)

CSS 是網頁關鍵渲染路徑的一部分,它涉及瀏覽器在開始頁面渲染之前必須完成的所有不同步驟。 這使得 CSS 成為渲染阻塞資產。

因此,考慮到這一點,當使用良好的 3G 連接加載 Sheffield United 的網站時,需要 1.15 秒才能下載 CSS 並開始頁面渲染。 這是個問題。

谷歌也意識到了這一點。 在線或通過瀏覽器運行 Lighthouse 審核時,通過刪除未使用的 CSS 可以節省的任何潛在加載時間和文件大小都會突出顯示。

在 Chrome(和 Chromium Edge)中,您可以通過單擊開發人員工具中的“審核”選項卡來糾正 Google Lighthouse 審核。 (大預覽)

現有解決方案

目標是確定哪些 CSS 類不是必需的,並將它們從樣式表中刪除。 現有的解決方案是可用的,它們試圖自動化這個過程。 它們通常可以通過 Node.js 構建腳本或 Gulp 等任務運行器使用。 這些包括:

  • 聯合國CSS
  • 淨化CSS
  • 清除CSS

這些通常以類似的方式工作:

  1. 在 Bulld 上,該網站是通過無頭瀏覽器(例如​​:puppeteer)或 DOM 仿真(例如:jsdom)訪問的。
  2. 根據頁面的 HTML 元素,識別出任何未使用的 CSS。
  3. 這將從樣式表中刪除,只留下需要的內容。

雖然這些自動化工具是完全有效的,並且我已經在許多商業項目中成功使用了其中的許多工具,但在此過程中我遇到了一些值得分享的缺點:

  • 如果類名包含特殊字符,例如“@”或“/”,則在不編寫一些自定義代碼的情況下可能無法識別這些字符。 我使用 Harry Roberts 的 BEM-IT,它涉及使用響應式後綴構建類名,例如: u-width-6/12@lg ,所以我之前遇到過這個問題。
  • 如果網站使用自動部署,它會減慢構建過程,尤其是在您有大量頁面和大量 CSS 的情況下。
  • 關於這些工具的知識需要在整個團隊中共享,否則當 CSS 神秘地出現在生產樣式表中時,可能會造成混亂和沮喪。
  • 如果您的網站運行了許多第 3 方腳本,有時在無頭瀏覽器中打開時,這些腳本無法正常運行,並可能導致過濾過程出錯。 因此,通常您必須編寫自定義代碼以在檢測到無頭瀏覽器時排除任何第 3 方腳本,這取決於您的設置,這可能會很棘手。
  • 通常,這類工具很複雜,並且會在構建過程中引入許多額外的依賴項。 與所有第 3 方依賴項一樣,這意味著依賴於其他人的代碼。

考慮到這幾點,我向自己提出了一個問題:

僅使用 Sass,是否可以更好地處理我們編譯的 Sass,以便排除任何未使用的 CSS,而無需直接粗暴地刪除 Sass 中的源類?

劇透警報:答案是肯定的。 這就是我想出的。

面向 Sass 的解決方案

該解決方案需要提供一種快速簡便的方法來挑選應該編譯的 Sass,同時又足夠簡單,不會增加開發過程的複雜性或阻止開發人員利用諸如以編程方式生成的 CSS 之類的東西類。

首先,有一個包含構建腳本和一些示例樣式的存儲庫,您可以從這里克隆。

提示:如果您遇到困難,您可以隨時與 master 分支上的已完成版本進行交叉引用。

cd進入 repo,運行npm install然後npm run build以根據需要將任何 Sass 編譯成 CSS。 這應該在 dist 目錄中創建一個 55kb 的 css 文件。

如果您隨後在 Web 瀏覽器中打開/dist/index.html ,您應該會看到一個相當標準的組件,單擊該組件會展開以顯示一些內容。 您也可以在此處查看此內容,其中將應用真實的網絡條件,因此您可以運行自己的測試。

在開發麵向 Sass 的解決方案以處理未使用的 CSS 時,我們將使用這個擴展器 UI 組件作為我們的測試對象。 (大預覽)

部分級別的過濾

在典型的 SCSS 設置中,您可能會有一個清單文件(例如:repo 中的main.scss ),或者每頁一個(例如: index.scssproducts.scsscontact.scss )其中框架部分是進口的。 遵循 OOCSS 原則,這些導入可能如下所示:

示例 1

 /* Undecorated design patterns */ @import 'objects/box'; @import 'objects/container'; @import 'objects/layout'; /* UI components */ @import 'components/button'; @import 'components/expander'; @import 'components/typography'; /* Highly specific helper classes */ @import 'utilities/alignments'; @import 'utilities/widths';

如果這些部分中的任何一個沒有被使用,那麼過濾這個未使用的 CSS 的自然方法就是禁用導入,這將阻止它被編譯。

例如,如果只使用擴展器組件,清單通常如下所示:

示例 2

 /* Undecorated design patterns */ // @import 'objects/box'; // @import 'objects/container'; // @import 'objects/layout'; /* UI components */ // @import 'components/button'; @import 'components/expander'; // @import 'components/typography'; /* Highly specific helper classes */ // @import 'utilities/alignments'; // @import 'utilities/widths';

但是,根據 OOCSS,我們將裝飾與結構分離以實現最大的可重用性,因此擴展器可能需要來自其他對象、組件或實用程序類的 CSS 才能正確呈現。 除非開發人員通過檢查 HTML 了解這些關係,否則他們可能不知道導入這些部分,因此不會編譯所有必需的類。

在 repo 中,如果您查看dist/index.html中擴展器的 HTML,情況似乎就是這樣。 它使用來自盒子和佈局對象、排版組件以及寬度和對齊實用程序的樣式。

dist/index.html

 <div class="c-expander"> <div class="o-box o-box--spacing-small c-expander__trigger c-expander__header" tabindex="0"> <div class="o-layout o-layout--fit u-flex-middle"> <div class="o-layout__item u-width-grow"> <h2 class="c-type-echo">Toggle Expander</h2> </div> <div class="o-layout__item u-width-shrink"> <div class="c-expander__header-icon"></div> </div> </div> </div> <div class="c-expander__content"> <div class="o-box o-box--spacing-small"> Lorum ipsum <p class="u-align-center"> <button class="c-expander__trigger c-button">Close</button> </p> </div> </div> </div>

讓我們通過在 Sass 本身中使這些關係正式化來解決這個等待發生的問題,因此一旦導入了組件,任何依賴項也將自動導入。 這樣,開發人員不再需要審核 HTML 以了解他們需要導入的其他內容的額外開銷。

程序化進口地圖

為了使這個依賴系統正常工作,而不是簡單地在清單文件中的@import語句中進行註釋,Sass 邏輯將需要指示是否編譯部分內容。

src/scss/settings ,創建一個名為_imports.scss的新部分,在settings/_core.scss@import ,然後創建以下 SCSS 映射:

src/scss/settings/_core.scss

 @import 'breakpoints'; @import 'spacing'; @import 'imports';

src/scss/settings/_imports.scss

 $imports: ( object: ( 'box', 'container', 'layout' ), component: ( 'button', 'expander', 'typography' ), utility: ( 'alignments', 'widths' ) );

此映射將與示例 1 中的導入清單具有相同的作用。

示例 4

 $imports: ( object: ( //'box', //'container', //'layout' ), component: ( //'button', 'expander', //'typography' ), utility: ( //'alignments', //'widths' ) );

它的行為應該像一組標準的@imports那樣,如果某些部分被註釋掉(如上所示),則不應在構建時編譯該代碼。

但由於我們希望自動導入依賴項,我們也應該能夠在適當的情況下忽略此映射。

渲染混合

讓我們開始添加一些 Sass 邏輯。 在src/scss/tools中創建_render.scss ,然後將其@import添加到tools/_core.scss

在文件中,創建一個名為render()的空 mixin。

src/scss/tools/_render.scss

 @mixin render() { }

在 mixin 中,我們需要編寫 Sass,它執行以下操作:

  • 使成為()
    “嘿, $imports ,天氣很好,不是嗎? 說,你的地圖中有容器對象嗎?”
  • $進口
    false
  • 使成為()
    “可惜了,看來以後編不下去了。 按鈕組件呢?”
  • $進口
    true
  • 使成為()
    “好的! 那就是正在編譯的按鈕。 替我跟老婆打聲招呼。”

在 Sass 中,這轉化為以下內容:

src/scss/tools/_render.scss

 @mixin render($name, $layer) { @if(index(map-get($imports, $layer), $name)) { @content; } }

基本上,檢查部分是否包含在$imports變量中,如果是,則使用 Sass 的@content指令渲染它,這允許我們將內容塊傳遞給 mixin。

我們會這樣使用它:

示例 5

 @include render('button', 'component') { .c-button { // styles et al } // any other class declarations }

在使用這個 mixin 之前,我們可以對其進行一些小的改進。 層名稱(對象、組件、實用程序等)是我們可以安全預測的,因此我們有機會稍微簡化一下。

在渲染 mixin 聲明之前,創建一個名為$layer的變量,並從 mixins 參數中刪除同名變量。 像這樣:

src/scss/tools/_render.scss

 $layer: null !default; @mixin render($name) { @if(index(map-get($imports, $layer), $name)) { @content; } }

現在,在對象、組件和實用程序@imports所在的_core.scss部分中,將這些變量重新聲明為以下值; 表示要導入的 CSS 類的類型。

src/scss/objects/_core.scss

 $layer: 'object'; @import 'box'; @import 'container'; @import 'layout';

src/scss/components/_core.scss

 $layer: 'component'; @import 'button'; @import 'expander'; @import 'typography';

src/scss/utilities/_core.scss

 $layer: 'utility'; @import 'alignments'; @import 'widths';

這樣,當我們使用render() mixin 時,我們所要做的就是聲明部分名稱。

render() mixin 包裹在每個對象、組件和實用程序類聲明周圍,如下所示。 這將為您提供每個部分的渲染混合使用。

例如:

src/scss/objects/_layout.scss

 @include render('button') { .c-button { // styles et al } // any other class declarations }

src/scss/components/_button.scss

 @include render('button') { .c-button { // styles et al } // any other class declarations }

注意:對於utilities/_widths.scss ,將render()函數包裹在整個部分中會在編譯時出錯,因為在 Sass 中,您不能在 mixin 調用中嵌套 mixin 聲明。 相反,只需將render() mixin 包裹在create-widths()調用周圍,如下所示:

 @include render('widths') { // GENERATE STANDARD WIDTHS //--------------------------------------------------------------------- // Example: .u-width-1/3 @include create-widths($utility-widths-sets); // GENERATE RESPONSIVE WIDTHS //--------------------------------------------------------------------- // Create responsive variants using settings.breakpoints // Changes width when breakpoint is hit // Example: .u-width-1/3@md @each $bp-name, $bp-value in $mq-breakpoints { @include mq(#{$bp-name}) { @include create-widths($utility-widths-sets, \@, #{$bp-name}); } } // End render }

有了這個,在構建時,只有$imports中引用的部分將被編譯。

混合併匹配$imports中註釋掉的組件,然後在終端中運行npm run build來嘗試一下。

依賴關係圖

現在我們正在以編程方式導入部分,我們可以開始實現依賴邏輯。

src/scss/settings ,創建一個名為_dependencies.scss的新部分,在settings/_core.scss@import它,但要確保它在_imports.scss之後。 然後在其中創建以下 SCSS 映射:

src/scss/settings/_dependencies.scss

 $dependencies: ( expander: ( object: ( 'box', 'layout' ), component: ( 'button', 'typography' ), utility: ( 'alignments', 'widths' ) ) );

在這裡,我們為擴展器組件聲明依賴項,因為它需要來自其他部分的樣式才能正確呈現,如 dist/index.html 中所示。

使用這個列表,我們可以編寫邏輯,這意味著無論$imports變量的狀態如何,這些依賴項總是與它們的依賴組件一起編譯。

$dependencies下,創建一個名為dependency-setup()的 mixin。 在這裡,我們將執行以下操作:

1. 遍歷依賴關係圖。

 @mixin dependency-setup() { @each $componentKey, $componentValue in $dependencies { } }

2. 如果可以在$imports中找到該組件,則遍歷其依賴項列表。

 @mixin dependency-setup() { $components: map-get($imports, component); @each $componentKey, $componentValue in $dependencies { @if(index($components, $componentKey)) { @each $layerKey, $layerValue in $componentValue { } } } }

3. 如果依賴項不在$imports中,請添加它。

 @mixin dependency-setup() { $components: map-get($imports, component); @each $componentKey, $componentValue in $dependencies { @if(index($components, $componentKey)) { @each $layerKey, $layerValue in $componentValue { @each $partKey, $partValue in $layerValue { @if not index(map-get($imports, $layerKey), $partKey) { $imports: map-merge($imports, ( $layerKey: append(map-get($imports, $layerKey), '#{$partKey}') )) !global; } } } } } }

包含!global標誌告訴 Sass 在全局範圍內查找$imports變量,而不是在 mixin 的本地範圍內。

4.然後就是調用mixin的事情了。

 @mixin dependency-setup() { ... } @include dependency-setup();

所以我們現在擁有的是一個增強的部分導入系統,如果一個組件被導入,開發人員也不必手動導入它的各種依賴部分。

配置$imports變量,以便只導入擴展器組件,然後運行npm run build 。 您應該在編譯後的 CSS 中看到擴展器類及其所有依賴項。

然而,在過濾掉未使用的 CSS 方面,這並沒有真正帶來任何新的東西,因為仍然在導入相同數量的 Sass,無論是否以編程方式。 讓我們對此進行改進。

改進的依賴項導入

一個組件可能只需要依賴項中的一個類,因此繼續導入該依賴項的所有類只會導致我們試圖避免的同樣不必要的膨脹。

我們可以改進系統以允許在逐個類的基礎上進行更精細的過濾,以確保僅使用它們需要的依賴類來編譯組件。

對於大多數設計模式,無論是否經過修飾,樣式表中都存在最少數量的類,以使模式正確顯示。

對於使用已建立的命名約定的類名稱(例如 BEM),通常至少需要“塊”和“元素”命名的類,而“修飾符”通常是可選的。

注意:實用程序類通常不會遵循 BEM 路線,因為它們由於關注點狹窄而在本質上是孤立的。

例如,看看這個媒體對象,它可能是面向對象 CSS 中最著名的例子:

 <div class="o-media o-media--spacing-small"> <div class="o-media__image"> <img src="url" alt="Image"> </div> <div class="o-media__text"> Oh! </div> </div>

如果組件將此設置為依賴項,則始終編譯.o-media.o-media__image.o-media__text是有意義的,因為這是使模式工作所需的最少 CSS 量。 然而.o-media--spacing-small是一個可選修飾符,它應該只在我們明確說明的情況下編譯,因為它的使用可能在所有媒體對象實例中都不一致。

我們將修改$dependencies映射的結構以允許我們導入這些可選類,同時包括一種導入塊和元素的方法,以防不需要修飾符。

首先,檢查 dist/index.html 中的擴展器 HTML 並記下正在使用的所有依賴類。 將這些記錄在$dependencies映射中,如下所示:

src/scss/settings/_dependencies.scss

 $dependencies: ( expander: ( object: ( box: ( 'o-box--spacing-small' ), layout: ( 'o-layout--fit' ) ), component: ( button: true, typography: ( 'c-type-echo', ) ), utility: ( alignments: ( 'u-flex-middle', 'u-align-center' ), widths: ( 'u-width-grow', 'u-width-shrink' ) ) ) );

如果將值設置為 true,我們會將其翻譯為“僅編譯塊和元素級別的類,沒有修飾符!”。

下一步涉及創建一個白名單變量來存儲這些類以及我們希望手動導入的任何其他(非依賴)類。 在/src/scss/settings/imports.scss中,在$imports之後,創建一個名為$global-filter的新 Sass 列表。

src/scss/settings/_imports.scss

 $global-filter: ();

$global-filter背後的基本前提是,存儲在這裡的任何類都將在構建時編譯,只要它們所屬的部分是通過$imports的。

如果它們是組件依賴項,則可以通過編程方式添加這些類名,也可以在聲明變量時手動添加,如下例所示:

全局過濾器示例

$global-filter: ( 'o-box--spacing-regular@md', 'u-align-center', 'u-width-6/12@lg' );

接下來,我們需要向@dependency-setup mixin 添加更多邏輯,因此$dependencies中引用的任何類都會自動添加到我們的$global-filter白名單中。

在此塊下方:

src/scss/settings/_dependencies.scss

 @if not index(map-get($imports, $layerKey), $partKey) { }

...添加以下代碼段。

src/scss/settings/_dependencies.scss

 @each $class in $partValue { $global-filter: append($global-filter, '#{$class}', 'comma') !global; }

這將遍歷所有依賴類並將它們添加到$global-filter白名單。

此時,如果你在dependency-setup() mixin下面添加@debug語句,在終端打印出$global-filter的內容:

 @debug $global-filter;

...您應該在構建時看到類似這樣的內容:

 DEBUG: "o-box--spacing-small", "o-layout--fit", "c-box--rounded", "true", "true", "u-flex-middle", "u-align-center", "u-width-grow", "u-width-shrink"

現在我們有了一個類白名單,我們需要在所有不同的對象、組件和實用程序部分執行此操作。

src/scss/tools中創建一個名為_filter.scss的新部分,並將@import添加到工具層的_core.scss文件中。

在這個新的部分中,我們將創建一個名為filter()的 mixin。 我們將使用它來應用邏輯,這意味著只有包含在$global-filter變量中的類才會被編譯。

從簡單開始,創建一個接受單個參數的 mixin——過濾器控制的$class 。 接下來,如果$class包含在$global-filter白名單中,則允許對其進行編譯。

src/scss/tools/_filter.scss

 @mixin filter($class) { @if(index($global-filter, $class)) { @content; } }

在部分情況下,我們將 mixin 包裝在一個可選類周圍,如下所示:

 @include filter('o-myobject--modifier') { .o-myobject--modifier { color: yellow; } }

這意味著.o-myobject--modifier類只有在它包含在$global-filter中時才會被編譯,它可以直接設置,也可以通過$dependencies中設置的內容間接設置。

瀏覽 repo 並將filter() mixin 應用於對象和組件層的所有可選修飾符類。 在處理排版組件或實用程序層時,由於每個類都獨立於下一個類,因此將它們全部設為可選是有意義的,因此我們可以根據需要啟用類。

這裡有幾個例子:

src/scss/objects/_layout.scss

 @include filter('o-layout__item--fit-height') { .o-layout__item--fit-height { align-self: stretch; } }

src/scss/utilities/_alignments.scss

 // Changes alignment when breakpoint is hit // Example: .u-align-left@md @each $bp-name, $bp-value in $mq-breakpoints { @include mq(#{$bp-name}) { @include filter('u-align-left@#{$bp-name}') { .u-align-left\@#{$bp-name} { text-align: left !important; } } @include filter('u-align-center@#{$bp-name}') { .u-align-center\@#{$bp-name} { text-align: center !important; } } @include filter('u-align-right@#{$bp-name}') { .u-align-right\@#{$bp-name} { text-align: right !important; } } } }

注意:將響應式後綴類名添加到filter() mixin 時,您不必使用“\”轉義“@”符號。

在此過程中,在將filter() mixin 應用於局部時,您可能(或可能沒有)注意到一些事情。

分組類

代碼庫中的一些類被組合在一起並共享相同的樣式,例如:

src/scss/objects/_box.scss

 .o-box--spacing-disable-left, .o-box--spacing-horizontal { padding-left: 0; }

由於過濾器只接受一個類,它沒有考慮一個樣式聲明塊可能用於多個類的可能性。

為了解決這個問題,我們將擴展filter() mixin,這樣除了單個類之外,它還能夠接受包含許多類的 Sass 參數列表。 像這樣:

src/scss/objects/_box.scss

 @include filter('o-box--spacing-disable-left', 'o-box--spacing-horizontal') { .o-box--spacing-disable-left, .o-box--spacing-horizontal { padding-left: 0; } }

所以我們需要告訴filter() mixin,如果這些類中的任何一個在$global-filter中,你就可以編譯這些類。

這將涉及額外的邏輯來檢查 mixin 的$class參數,如果傳遞 arglist 以檢查每個項目是否在$global-filter變量中,則以循環響應。

src/scss/tools/_filter.scss

 @mixin filter($class...) { @if(type-of($class) == 'arglist') { @each $item in $class { @if(index($global-filter, $item)) { @content; } } } @else if(index($global-filter, $class)) { @content; } }

然後只需回到以下部分以正確應用filter()混合:

  • objects/_box.scss
  • objects/_layout.scss
  • utilities/_alignments.scss

此時,返回$imports並僅啟用擴展器組件。 在編譯的樣式表中,除了來自通用層和元素層的樣式外,您應該只看到以下內容:

  • 屬於擴展器組件的塊和元素類,但不屬於它的修飾符。
  • 屬於擴展器依賴項的塊和元素類。
  • 屬於在$dependencies變量中顯式聲明的擴展器依賴項的任何修飾符類。

從理論上講,如果您決定要在編譯的樣式表中包含更多類,例如擴展器組件修飾符,只需在聲明點將其添加到$global-filter變量中,或在其他點附加它即可在代碼庫中(只要它在聲明修飾符本身的點之前)。

啟用一切

所以我們現在有一個非常完整的系統,它允許您將對象、組件和實用程序導入到這些部分中的各個類中。

在開發過程中,無論出於何種原因,您可能只想一次性啟用所有功能。 為此,我們將創建一個名為$enable-all-classes的新變量,然後添加一些額外的邏輯,因此如果將其設置為 true,則無論$imports$global-filter的狀態如何,都會編譯所有內容$global-filter變量。

首先,在我們的主清單文件中聲明變量:

src/scss/main.scss

 $enable-all-classes: false; @import 'settings/core'; @import 'tools/core'; @import 'generic/core'; @import 'elements/core'; @import 'objects/core'; @import 'components/core'; @import 'utilities/core';

然後我們只需要對我們的filter()render()混合器進行一些小的編輯,以便在$enable-all-classes變量設置為 true 時添加一些覆蓋邏輯。

首先, filter()混合。 在任何現有檢查之前,我們將添加一個@if語句以查看$enable-all-classes是否設置為 true,如果是,則呈現@content ,不詢問任何問題。

src/scss/tools/_filter.scss

 @mixin filter($class...) { @if($enable-all-classes) { @content; } @else if(type-of($class) == 'arglist') { @each $item in $class { @if(index($global-filter, $item)) { @content; } } } @else if(index($global-filter, $class)) { @content; } }

接下來在render() mixin 中,我們只需要檢查$enable-all-classes變量是否為真,如果是,則跳過任何進一步的檢查。

src/scss/tools/_render.scss

 $layer: null !default; @mixin render($name) { @if($enable-all-classes or index(map-get($imports, $layer), $name)) { @content; } }

所以現在,如果您將$enable-all-classes變量設置為 true 並重新構建,每個可選類都將被編譯,從而在此過程中為您節省大量時間。

比較

要了解這種技術給我們帶來了哪些類型的收益,讓我們進行一些比較,看看文件大小的差異是什麼。

為了確保比較公平,我們應該在$imports中添加盒子和容器對象,然後將盒子的o-box--spacing-regular修飾符添加到$global-filter ,如下所示:

src/scss/settings/_imports.scss

 $imports: ( object: ( 'box', 'container' // 'layout' ), component: ( // 'button', 'expander' // 'typography' ), utility: ( // 'alignments', // 'widths' ) ); $global-filter: ( 'o-box--spacing-regular' );

這確保了擴展器父元素的樣式正在編譯,就像沒有進行過濾時一樣。

原始樣式表與過濾樣式表

讓我們將原始樣式表與所有編譯的類進行比較,與僅編譯擴展器組件所需的 CSS 的過濾樣式表進行比較。

標準
樣式表大小 (kb) 大小 (gzip)
原版的54.6kb 6.98kb
過濾15.34kb(小 72%) 4.91kb(小 29%)
  • 原文: https://webdevluke.github.io/handlingunusedcss/dist/index2.html
  • 過濾: https://webdevluke.github.io/handlingunusedcss/dist/index.html

您可能認為 gzip 節省的百分比意味著這不值得付出努力,因為原始樣式表和過濾後的樣式表之間沒有太大區別。

值得強調的是,gzip 壓縮更適用於更大和更重複的文件。 因為過濾後的樣式表是唯一的概念驗證,並且只包含擴展器組件的 CSS,所以沒有實際項目中的那麼多需要壓縮。

如果我們將每個樣式表按 10 倍放大到更典型的網站 CSS 包大小,gzip 文件大小的差異會更加令人印象深刻。

10 倍大小
樣式表大小 (kb) 大小 (gzip)
原始 (10x) 892.07kb 75.70kb
過濾 (10x) 209.45kb(小 77%) 19.47kb(小 74%)

過濾樣式表與 UNCSS

這是過濾樣式表和通過 UNCSS 工具運行的樣式表之間的比較。

過濾與 UNCSS
樣式表大小 (kb) 大小 (gzip)
過濾15.34kb 4.91kb
聯合國CSS 12.89kb(小 16%) 4.25kb(小 13%)

UNCSS 工具在這里略勝一籌,因為它過濾掉了通用目錄和元素目錄中的 CSS。

在一個使用大量 HTML 元素的真實網站上,這兩種方法之間的差異可能可以忽略不計。

包起來

所以我們已經看到——僅使用 Sass——你可以更好地控制在構建時編譯的 CSS 類。 這減少了最終樣式表中未使用的 CSS 數量並加快了關鍵渲染路徑。

在文章的開頭,我列出了現有解決方案(例如 UNCSS)的一些缺點。 以同樣的方式批評這個面向 Sass 的解決方案是公平的,所以在你決定哪種方法更適合你之前,所有的事實都擺在桌面上:

優點

  • 不需要額外的依賴項,因此您不必依賴其他人的代碼。
  • 與基於 Node.js 的替代方案相比,所需的構建時間更少,因為您不必運行無頭瀏覽器來審核您的代碼。 這對於持續集成特別有用,因為您可能不太可能看到構建隊列。
  • 與自動化工具相比,文件大小相似。
  • 開箱即用,您可以完全控制要過濾的代碼,無論這些 CSS 類如何在您的代碼中使用。 使用基於 Node.js 的替代方案,您通常必須維護一個單獨的白名單,以便不會過濾掉屬於動態注入 HTML 的 CSS 類。

缺點

  • 面向 Sass 的解決方案肯定更實用,因為您必須掌握$imports$global-filter變量。 除了初始設置之外,我們看到的 Node.js 替代方案在很大程度上是自動化的。
  • 如果您將 CSS 類添加到$global-filter然後從 HTML 中刪除它們,您需要記住更新變量,否則您將編譯不需要的 CSS。 由於多個開發人員在任何時候都在處理大型項目,除非您進行適當的計劃,否則這可能不容易管理。
  • 我不建議將此系統固定到任何現有的 CSS 代碼庫上,因為您必須花費大量時間拼湊依賴項並將render() mixin 應用到很多類。 這是一個使用新版本更容易實現的系統,您無需處理現有代碼。

希望你覺得這本書讀起來很有趣,就像我覺得把它放在一起很有趣一樣。 如果您有任何建議、想法來改進這種方法,或者想指出我完全錯過的一些致命缺陷,請務必在下面的評論中發表。