以下是 Wladston Ferreira Filho 的客座文章。我们之前已经介绍过关键 CSS,但 所涵盖的技术 特定于 SCSS。在这里,Wladston 涵盖了基础知识,并介绍了一种使用 PostCSS 的另一种方法,并展示了结果的示例(带数据)。
关键 CSS 是一种有效但使用并不普遍的方法,用于提高页面渲染性能。关键 CSS 包含以下两部分
- 延迟加载主样式表
- 内联最重要的(“可视区域以上”)样式
实现关键 CSS 需要一些工作。首先,这意味着将 CSS 分为两部分(关键部分和其余部分),这可能导致维护问题。
在本文的后面部分,我们将介绍一种工具,该工具可以通过在主 CSS 文件中基于注释(即 /* 注释 */
)自动分割 CSS 来解决此缺点。
事实
来自 Mozilla、Akamai 和 许多其他来源 的研究证实:页面渲染时间的小幅变化会显著影响性能指标。在网络连接较差的情况下,页面性能变得更加重要,因为下载时间可能会高出好几倍。
Google 甚至提供 一项服务 来为页面提供“速度评分”,这是一种委婉的暗示,性能可能与 SEO 相关。Google 建议适当地使用关键 CSS 来提高您的评分。这项技术一定会对渲染速度产生积极影响。渲染时间减少的程度取决于关键 CSS 的大小以及主样式表的大小。
关键 CSS 的工作原理
CSS 的“常规”方法是在 <head>
中包含您的主样式表作为 <link>
。下载和解析该样式表会阻止渲染。关键 CSS 通过绕过阻止使页面更快地渲染。
第一步是“内联”(在 <head>
中使用简化的 <style>
标签)渲染页面可视区域以上内容所需的必要 CSS。这将使第二步成为可能:非关键 CSS 可以异步加载(非阻塞),同时网页正在渲染。当大型 CSS 文件到达后,它将通过 JavaScript 附加到页面。
在实践中实现此功能的一种常见方法是使用 CSS 预处理器。预处理器可以检测 CSS 中专门编写的注释,因此它可以区分关键 CSS 并自动将其分离。这在 CSS-Tricks 中已有介绍,使用 SCSS。让我们探索在原生 CSS 语法中实现此功能。
注意:要使此功能生效,您将需要一个服务器端工具,将关键 CSS 内联到您提供的所有页面中,以及添加几行内联 JavaScript 来加载主(非关键)样式表。
关键 CSS 的现有技术
关键 CSS 最终需要拥有两个独立的 CSS 片段:关键和非关键。我们如何获得它们?
完全手动:维护两个 CSS 文件
使用此策略,您可以直接编辑两个 CSS 文件而不是一个文件。虽然此策略很简单,不需要任何工具,但它更难使用。更难理解、阅读和更改样式。仅建议用于不太可能更改的静态 CSS。
完全自动化
服务器端工具(例如 Google Page Speed Extension)将自动检测哪些 CSS 是渲染可视区域以上内容所需的,它们会将它们选择的关键 CSS 分离并将其内联到您的页面中,无需您的干预。
这种技术有一些缺点:您自动生成的非关键 CSS 可能在评估的每个页面中都会发生变化,从而降低 CSS 缓存的效率。它也不能完美地检测关键 CSS,特别是对于小屏幕。
此外,您无法自定义或微调该过程。
SCSS 与 Jacket 插件
如果您使用 SCSS,您可以 安装 Jacket 插件 (详细信息请参见此处)。Jacket 会将使用特殊关键类标记的 CSS 分离到另一个文件中,在处理 LCSS 后生成关键 CSS 和非关键 CSS。这种技术的问题在于它将您绑定到 SCSS。如果您决定停止使用它,或者您想要更改预处理风格,您将需要额外的工作来调整您的关键 CSS 解决方案。
我的技术:PostCSS 和 PostCSS-Split
我的技术依赖于使用简单的纯 CSS 注释标记所有关键 CSS 声明。让我们考虑这个超级简单的 HTML 来进行说明
<!DOCTYPE html>
<html lang="en">
<body>
<header>
<h1>I'm a big header</h1>
</header>
<p>I'm below the fold</p></body>
</body>
</html>
header > h1 {
/* !Critical */ margin: 300px;
}
p {
border: 1px dotted black;
}
第一步是通过在其中放置 /* !Critical */
来标记渲染可视区域以上内容所需的 CSS 规则。
为了确定主样式表中的哪些声明应该包含在关键 CSS 中,您可以从免费服务(例如 此服务)中获取建议。
在将“关键”注释放置到基本 CSS 文件中后,使用 npm
安装 PostCSS-Split。如果您尚未安装,则需要 安装 Node.js。在终端中,发出以下命令来安装 PostCSS-Split
sudo npm install -g postcss-split
然后,您可以发出以下命令,将已添加注释的基本 CSS 文件传递给 PostCSS-Split
postcss-split base.css
将根据您的输入文件创建全新的 base-critical.css
和 base-non-critical.css
文件。`base-critical.css` 的内容将插入 <head>
中的 <style>
标签中。
至于加载 base-non-critical.css
,您可以使用异步 CSS 加载器。例如,在 </body>
标签之前添加以下内容(并相应地更改 <your_css_url>
)
<script>
function lCss(u, m) {
var l = document.createElement('link');
l.rel = 'stylesheet';
l.type = 'text/css';
l.href = u;
l.media = m;
document.getElementsByTagName('head')[0].appendChild(l)
}
function dCss() {
lCss('<your_css_url>', 'all')
}
if (window.addEventListener) {
window.addEventListener('DOMContentLoaded', dCss, false)
} else {
window.onload=dCss
}
</script>
任何关键 CSS 技术的潜在缺陷
使用任何关键 CSS 技术时,您可能会遇到一些问题。让我们看看如何解决它们
优先级
如果有多个 CSS 规则具有相同的特异性,则声明在后面的规则将优先于声明在前面的规则。
请记住,您指定为关键的 CSS 会改变其位置:它将内联到您的 <head>
中,这意味着它将首先加载,并且会被随后加载的任何具有相同特异性选择器的 CSS 覆盖。
如果您使用这种方法在使用关键 CSS 时无法获得正确的 CSS 样式,请确保您的 CSS 不依赖于顺序。如果您遇到奇怪的结果,请使用 CSS 检查器来帮助您 解决特异性问题。
FOUC
如果你的关键 CSS 没有包含渲染所有页面可见内容所需的 *所有* 规则,或者用户在你的大部分 CSS 加载之前开始浏览页面不可见内容,你就会遇到 FOUC(未样式内容闪烁)效应。
当你的非关键 CSS 加载时,浏览器会改变你页面的样式,以应用非关键 CSS 中的规则。这种样式变化的“闪烁”可能是不希望看到的。
为了缓解这种尴尬,一种方法是使用 CSS transition 来平滑地从无样式状态过渡到有样式状态。在开发过程中,你可以手动在注入你的大部分 CSS 的 JavaScript 代码中添加延迟。
在 HTML 页面中包含关键 CSS
你需要一个工具将关键 CSS 注入你的 HTML 页面的 <head>
中。如果你使用的是像 PHP 这样的后端语言,你可以使用 include()
语句(或类似方法)轻松实现。
<!DOCTYPE html>
<html lang="en">
<head>
...
<style>
<?php include_once("/path/to/base-critical.css"); ?>
</style>
...
如果你没有直接处理代码(例如,你正在使用 WordPress 等内容管理系统),你可以搜索一个配置设置或插件来完成这项工作。在 WordPress 中,你可以添加一个“钩子”将你的 CSS 文件内容内联到最终的 HTML 中。
Jeremy Keith 在 这里 使用 Grunt/Twig 概述了一种方法。
这真的值得吗?
总结…
以下是实现这种技术所需的步骤。
- 在你的主要样式表中识别并标记你的关键 CSS。
- 在你的部署流程中添加一个任务,将基础 CSS 拆分为两个文件。
- 添加额外的 JavaScript 代码来异步加载你的主要样式表。
- 实现一个服务器端包含功能,将你的关键 CSS 内容添加到每个页面的
<head>
中。
案例研究:使用关键 CSS 的实际网站
我已经对 https://code.energy
网站进行了编程,使其可以提供带有或不带有关键 CSS 的页面。默认情况下,它将使用关键 CSS,除非包含 nocritical
查询字符串(例如,https://code.energy?nocritical
)。另一种禁用关键 CSS 的方法是传递一个包含字符串 nocritical
的用户代理头。
有了这些,我们可以轻松地使用在线工具(如 webpagetest.org)来衡量关键 CSS 对这个网站速度性能的影响。Webpagetest 允许我们轻松地使用自定义用户代理字符串运行测试。以下结果来自每个场景 5 次实验的平均值。
关键 | 加载时间 | 开始渲染 | 完全加载 | 速度指数 |
---|---|---|---|---|
x | 0.949 秒 | 0.988 秒 | 1.003 秒 | 1036 |
✓ | 0.838 秒 | 0.695 秒 | 0.893 秒 | 755 |
最令人印象深刻的差异是“开始渲染”时间。通过异步加载 CSS,我们可以看到浏览器能够并行发出更多请求,因为它更早地开始解析 HTML,正如你在这里看到的。

结论
如果你想要为你的网站获得最佳性能,你需要一个关键 CSS 策略。使用 PostCSS-Split,你可以以很小的维护成本实现它。
只有我一个人把“!Critical”读成“*非*关键”吗?
!Yep => Nope!
是的,但那是 !important…
Alain,这是我第一次这样想,即使我是一个程序员。Pat 理解了我想要表达的意思——那是件 !important 的事 :)
如果你愿意,你也可以在 PostCSS 插件代码中使用自定义正则表达式。
在我的 Magento/Gulp 工作流程中,我使用 Penthouse (https://github.com/pocketjoso/penthouse) 来生成关键 CSS,并使用 Magento 的布局指令将它注入 HTML。然后,使用 filamentgroup 的 loadCSS (https://github.com/filamentgroup/loadCSS) 加载完整的 CSS。
Luis,很有趣。你使用 Penthouse 的体验怎么样?它能很好地选择关键 CSS 吗?loadCSS 似乎是一段精心制作的 JavaScript 代码。我建议的方法更简单,但在我在意的浏览器中运行完美无缺 :)
我不理解:PHP 不应该在页面发送到客户端之前完成吗?即使 PHP 在发送页面之前等待关键 CSS 加载,这种技巧如何加速页面加载?
@Marcello,PHP 实际上并没有等待任何东西加载。你使用某种构建过程(例如 PostCSS)将关键 CSS 提取到一个单独的文件中,然后使用 PHP 将该关键文件的内容直接嵌入到生成的 HTML 中,这样浏览器就不需要发出单独的网络请求。
然后你可以使用 JavaScript 异步加载你的剩余 CSS。(你不需要等到剩余 CSS 加载完毕才能开始,只需立即运行代码,浏览器会自动下载文件,而不会阻塞页面的其他渲染)。
PHP 示例将 CSS 文件插入到 标签中,因此它将渲染该 CSS,你将得到类似以下内容:
这将在你的头部“内联”输出关键 CSS,然后通过
<link>
加载你的 CSS。值得一提的是,Jacket 是 Compass 的一个组件——我们中那些更喜欢 libsass 的人将无法使用它。
是的,Michael。这是我决定使用不依赖任何语法,除了纯 CSS 之外的东西的原因之一。
我知道这有点吹毛求疵,但我认为值得指出;
inline
样式添加到元素本文中提到的实际上是内部样式表
哎呀… 你是对的。不知道 Chris 会不会让我编辑这篇文章!
这篇文章提到的实际上是嵌入式样式表,而不是内部或内联样式表。
为了处理 FOUC,我们可以使用 `overflow: hidden` 在 body 上,并在非关键样式加载后将其删除。我喜欢使用 post-css-split 来分割 CSS 的方法。
谢谢
Kalpesh,谢谢,我很高兴你喜欢它 :)
确实,在所有 CSS 加载完毕之前隐藏 body 是处理 FOUC 的一种广泛采用的策略。我更倾向于一种可以让用户尽快看到内容的策略。我喜欢想象用户可能处于非常糟糕/缓慢的连接状态,并尝试在这种情况下尽可能地使事情正常运作。
@Wladston 确实,因为我默认情况下会阻止所有 JS,我不喜欢必须打开开发工具来设置 `display`(或者更常见的是:`opacity`)才能看到内容。
你可以在一个文件中轻松分割它。只需使用 PHP 作为 CSS 文件。
link rel=”stylesheet” type=”text/css” property=”stylesheet” media=”screen” href=”style.php?id=blog”
其他部分则使用 `id=bottom`。
顺便说一下,感谢您发布“为什么我不使用 CSS 预处理器”。我一直不明白 SASS、LESS 等的必要性。我使用 PHP 进行压缩——不需要额外的工具和工作流程。因此,我所要做的只是改变语法颜色(在 Sublime Text 中的右下角)即可处理该文件。
到目前为止,我所见过的关于该主题的最佳工作流程,谢谢!
抱歉,我有点笨,但这里示例:`h1 { /*!CRITICAL*/ margin: 30px; }` 与插件作者的示例:`h1 /*!CRITICAL*/ { margin:30px; }` 差异很大。
Hey Robin,我很高兴看到你喜欢这篇文章!谢谢!
post-css-split 文档中的示例已经过时了,当我更改默认的正则表达式匹配时,我完全忘记更新文档……我的错 :/
欢迎您向 post-css-split 的代码发送 pull 请求以修复此问题 :)
关键 CSS 与单个页面的渲染时间有关。
但是,如果用户在同一个网站上导航了几个页面呢?
例如:一个总 CSS 大约为 30000 个字符,关键 CSS 为 6700 个字符(22%)的网站。
如果用户导航到多个页面,
– 使用关键 CSS,第一个页面渲染速度更快,但随后,每个页面都会加载 22% 的额外 CSS(内部)。
– 不使用关键 CSS,完整的 CSS 会在第一个页面加载并缓存,并且每个后续页面都应该渲染得更快。
你怎么看?
Hey fr,这是一个你提到的重要权衡。关键是,在主 GET 响应中添加几个字节对加载时间的影响微乎其微,但却对页面渲染速度产生重大影响。因此,如果您关心长期缓存,您可以减少一些关键 CSS,直到达到您满意的平衡点。这就是我提出的方法的意义所在:让用户能够以他们喜欢的方式轻松实现关键 CSS。
Hey Wladston,感谢您的回复。我还发现一个(小)问题:关键 CSS 对于同一个网站的不同页面可能不一样。所以我想知道你将如何维护不同页面不同的关键 CSS?
是的,这确实很难。这种技术假设您只有一个页面布局,或者非常相似的页面布局,它们能够共享完全相同的关键 CSS。所以我看到两种替代方案
对网站所有页面使用相同的关键 CSS
以某种方式增强 post-css-split 插件,使您还可以描述特定 CSS 规则所关键的页面布局。例如,这将适用于所有关键部分
而这将仅适用于特定类型的页面布局
请记住,通过这样做,两个页面的非关键 CSS 将有所不同,因此浏览器缓存的效率会略微降低。
您如何在多个页面上处理这些内容?例如,一个网店通常有不同的着陆页,它们有不同的关键 CSS。由于 CSS 的缓存(很多子页面),您不希望使用自动生成的 CSS。但为每个页面维护关键 CSS 似乎不可行。
Hey Ramon!就像我刚刚回复 @fr 一样,您可以合并关键 CSS 或扩展 post-css-plugin 来处理每个页面的标签。
我会从标记所有页面所需的关键 CSS 集开始。如果最后页面包含了很多不必要的关键 CSS,我将开始为每个页面添加标签。
我很乐意与您合作,扩展 post-css-split 插件来支持此用例。
不错的文章!这种技术对我来说有点太手动(因此我创建了 Penthouse),但如果您有一个遵循单一模板的简单网站,它应该能很好地工作。
只有一个警告:从原始 CSS 中提取关键 CSS(将其分成两个文件)真的只有在您为整个网站使用一个关键 CSS 文件/模板(而不是为每个页面使用独立的关键 CSS 文件)时才有效。否则,您将为每个页面获得不同的“完整 CSS”文件(因为您提取了不同的关键 CSS),并且您将无法再缓存完整的 CSS。
出于这些原因,我总是建议不要从完整的 CSS 中提取关键 CSS,而是将其复制在里面。重复无关紧要,因为完整的 CSS 只应加载一次(然后被缓存),并且唯一重复的部分是关键 CSS,它应该很小。使用完整的“完整 CSS”,您也不必再担心特异性,因为完整 CSS 文件中的规则将胜过关键 CSS 中的规则。
干杯
我完全同意,这也将支持使用 cookie 来确定服务器端是否将关键 CSS 放入头部,以供已将完整的 CSS 缓存到浏览器中的回头用户(虽然我也听说这可能也会造成困难……)。
我同意保留所有样式的完整 CSS,在使用这些半自动方法时更简单。
我们使用 SCSS 来编写关键 CSS,主样式表包含所有样式并通过 JS 加载。然后我们有一个通用的关键 CSS(normalize 等)和每个网站模板的独立关键样式表。在模板中,我们(通过 PHP)包含通用关键 CSS 和模板特定的关键 CSS(如果有需要)。
在主页上,我们能够节省大约 12kB 的来自其他页面的内联 CSS。这很重要,因为内联样式会增加 HTML 的权重,您可能会遇到低于 14kB 的问题(尤其是在使用内联 SVG 时)。
sudo npm install -g postcss-split
显然,插件名称在“post”之后没有连字符,与文章中提到的命令相反。
我认为现在的预解析器相当聪明,它们会优先处理 CSS——但将内部样式放到头部最顶端(紧接在字符集或标题下方)仍然是一个好主意吗?
顺便说一下,很棒的文章!:)
此外,如果您的页面很小,并且所有标记和 CSS 以及 gzip 压缩后的内容都小于 14K(就像 code.energy 的情况一样),您可以考虑将所有 CSS 内联到头部。当然,浏览器必须预先解析所有内容,但如果您的样式表足够小……我没有对速度差异进行基准测试——但即使没有 JS,您也会保持完整样式!:)
您的网站即使在 JS 关闭的情况下也应该保持完整样式,因为在使用关键 CSS 和 JS 加载主 CSS 时,您通常应该在 noscript 元素中链接主 CSS!:)
好点!:)
如果您追求的是毫秒级的速度(这也是非阻塞加载样式表时的目标),您不应该在整个 DOM 中搜索所有 `head` 元素。不要使用 `document.getElementsByTagName('head')[0]`;使用您已经拥有的引用:`document.head`。
相关:`document.body` 用于 `body` 元素(您知道这一点),`document.documentElement` 用于 `html` 根元素。