在 Sass 中处理未使用的 CSS 以提高性能
已发表: 2022-03-10在现代前端开发中,开发人员应该致力于编写可扩展和可维护的 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 运行。 让我们试一试。

coverage
,然后选择“显示覆盖率”选项。 (大预览)结果显示主页仅使用了 568kb 样式表中的30kb CSS,其余 538kb 与网站其余部分所需的样式有关。 这意味着高达94.8%的 CSS 未被使用。

CSS 是网页关键渲染路径的一部分,它涉及浏览器在开始页面渲染之前必须完成的所有不同步骤。 这使得 CSS 成为渲染阻塞资产。
因此,考虑到这一点,当使用良好的 3G 连接加载 Sheffield United 的网站时,需要 1.15 秒才能下载 CSS 并开始页面渲染。 这是个问题。
谷歌也意识到了这一点。 在线或通过浏览器运行 Lighthouse 审核时,通过删除未使用的 CSS 可以节省的任何潜在加载时间和文件大小都会突出显示。

现有解决方案
目标是确定哪些 CSS 类不是必需的,并将它们从样式表中删除。 现有的解决方案是可用的,它们试图自动化这个过程。 它们通常可以通过 Node.js 构建脚本或 Gulp 等任务运行器使用。 这些包括:
- 联合国CSS
- 净化CSS
- 清除CSS
这些通常以类似的方式工作:
- 在 Bulld 上,该网站是通过无头浏览器(例如:puppeteer)或 DOM 仿真(例如:jsdom)访问的。
- 根据页面的 HTML 元素,识别出任何未使用的 CSS。
- 这将从样式表中删除,只留下需要的内容。
虽然这些自动化工具是完全有效的,并且我已经在许多商业项目中成功使用了其中的许多工具,但在此过程中我遇到了一些值得分享的缺点:
- 如果类名包含特殊字符,例如“@”或“/”,则在不编写一些自定义代码的情况下可能无法识别这些字符。 我使用 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
,您应该会看到一个相当标准的组件,单击该组件会展开以显示一些内容。 您也可以在此处查看此内容,其中将应用真实的网络条件,因此您可以运行自己的测试。

部分级别的过滤
在典型的 SCSS 设置中,您可能会有一个清单文件(例如:repo 中的main.scss
),或者每页一个(例如: index.scss
、 products.scss
、 contact.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 应用到很多类。 这是一个使用新版本更容易实现的系统,您无需处理现有代码。
希望你觉得这本书读起来很有趣,就像我觉得把它放在一起很有趣一样。 如果您有任何建议、想法来改进这种方法,或者想指出我完全错过的一些致命缺陷,请务必在下面的评论中发表。