CSS对于呈现页面至关重要 - 在找到,下载和解析所有CSS之前,浏览器不会开始呈现 - 因此我们必须尽可能快地将其加载到用户的设备上。 关键路径上的任何延迟都会影响我们的“开始渲染”并让用户看到空白屏幕。
什么是大问题?
从广义上讲,这就是CSS对性能至关重要的原因:
- 浏览器在构建渲染树之前无法渲染页面;
- 渲染树是DOM和CSSOM的组合结果;
- DOM是HTML加上需要对其进行操作的任何阻塞JavaScript;
- CSSOM是针对DOM应用的所有CSS规则;
- 使用async和defer属性很容易使JavaScript无阻塞;
- CSS不容易异步;
- 所以要记住的一个好的经验法则是,您的页面会在你最慢的样式表加载完成之后才展示。
考虑到这一点,我们需要尽快构建DOM和CSSOM。 在大多数情况下,构建DOM相对较快:您的第一个HTML响应是DOM。 但是,由于CSS几乎总是HTML的子资源,因此构建CSSOM通常需要更长的时间。
在这篇文章中,我想看看CSS如何证明是网络上的一个重大瓶颈(本身和其他资源)以及我们如何缓解它,从而缩短关键路径并缩短开始渲染的时间。
使用关键CSS
如果你有能力,减少Start Render时间的最有效方法之一就是使用Critical CSS模式:识别Start Render所需的所有样式(通常是首屏所需的样式), 将它们内联到文档的<head>中的<style>标记中,并从这里异步加载剩余的样式表。
虽然这种策略是有效的,但并不简单:高度动态的网站很难从中提取样式,流程需要自动化,我们必须对折叠率甚至是什么做出假设,很难捕获边缘情况和工具 仍处于相对初期阶段。 如果您正在使用大型或遗留代码库,事情会变得更加困难......
拆分媒体类型
如果实现关键CSS非常棘手 - 它可能只是一种选择,我们将主要的CSS文件拆分为其各自的媒体查询。 这样做的实际结果是浏览器会......
- 以非常高的优先级下载当前上下文所需的任何CSS(中等,屏幕大小,分辨率,方向等),阻止关键路径;
- 以非常低的优先级下载当前上下文不需要的任何CSS,完全脱离关键路径。
基本上,浏览器有效地延迟了不需要渲染当前视图的任何CSS。
<link rel="stylesheet" href="all.css" />
如果我们将所有CSS捆绑到一个文件中,那么它会这样子加载:
如果我们可以将单个全渲染阻塞文件拆分为各自的媒体查询:
<link rel="stylesheet" href="all.css" media="all" />
<link rel="stylesheet" href="small.css" media="(min-width: 20em)" />
<link rel="stylesheet" href="medium.css" media="(min-width: 64em)" />
<link rel="stylesheet" href="large.css" media="(min-width: 90em)" />
<link rel="stylesheet" href="extra-large.css" media="(min-width: 120em)" />
<link rel="stylesheet" href="print.css" media="print" />
然后我们看到网络以不同方式处理文件:
浏览器仍将下载所有CSS文件,但它只会阻止渲染完成当前上下文所需的文件。
避免在CSS文件中使用@import
我们可以做的下一件事就是帮助Start Render更加简单。 避免在CSS文件中使用@import。
@import,根据它的工作原理,很慢。 对于Start Render性能来说真的非常糟糕。 这是因为我们正在关键路径上积极创建更多循环路径:
- 下载HTML;
- HTML请求CSS;(这是我们希望能够构建渲染树的地方,但是;)
- CSS请求更多CSS;
- 构建渲染树。
以下HTML:
<link rel="stylesheet" href="all.css" />
包含在all.css中@import
@import url(imported.css);
我们最终得到这样的瀑布图:
通过简单地将其展平为两个<link rel =“stylesheet”/>和去掉@imports:
<link rel="stylesheet" href="all.css" />
<link rel="stylesheet" href="imported.css" />
我们得到一个更健康的瀑布图:
请注意HTML中的@import
要完全理解本节,我们首先需要了解浏览器的预装载扫描程序:所有主流浏览器都实现了通常称为预装载扫描程序的辅助惰性解析器。 浏览器的主要解析器负责构建DOM,CSSOM,运行JavaScript等,并且随着文档的不同部分阻止它而不断停止和启动。 Preload Scanner可以安全地跳过主解析器并扫描HTML的其余部分,以发现对其他子资源(例如CSS文件,JS,图像)的引用。 一旦发现它们,Preload Scanner就会开始下载它们,以便主要解析器接收它们并在以后执行/应用它们。 Preload Scanner的推出使网页性能提高了大约19%,所有这些都不需要开发人员参与。 这对用户来说是个好消息!
我们作为开发人员需要警惕的一件事是无意中隐藏了Preload Scanner中可能发生的事情。 稍后会详细介绍。
本节介绍WebKit和Blink的Preload Scanner中的错误,以及Firefox和IE / Edge的Preload Scanner中的低效率。
Firefox和IE / Edge:将@import放在HTML中的JS和CSS之前
在Firefox和IE / Edge中,Preload Scanner似乎没有使用<script src =“”>或<link rel =“stylesheet”/>之后定义的任何@import。
这意味着这个HTML:
<script src="app.js"></script>
<style>
@import url(app.css);
</style>
将产生这个瀑布图:
由于无效预装载扫描程序导致Firefox失去并行化(N.B.在IE / Edge中出现相同的瀑布。)
这个问题的直接解决方案是交换<script>或<link rel =“stylesheet”/>和<style>块。 但是,当我们更改依赖顺序时,这可能会破坏事物(想想他们之间的关联)。
这个问题的首选解决方案是完全避免使用@import并使用第二个<link rel =“stylesheet”/>:
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="app.css" />
瀑布图如下:
两个<link rel =“stylesheet”/> s让我们回到并行化。 (N.B. IE / Edge中出现相同的瀑布。)
Blink和WebKit:用HTML格式引用@import URL
仅当您的@import URL缺少引号(“)时,WebKit和Blink的行为与Firefox和IE / Edge完全相同。这意味着WebKit和Blink中的Preload Scanner存在错误。
简单地将@import包装在引号中将解决问题,您无需重新排序任何内容。 不过,和以前一样,我的建议是完全避免使用@import,而是选择第二个<link rel =“stylesheet”/>。
之前
<link rel="stylesheet" href="style.css" />
<style>
@import url(app.css);
</style>
瀑布图:
我们的@ import
网址中缺少引号会破坏Chrome的预装扫描程序(N.B.在Opera和Safari中会出现相同的瀑布。)
修改之后:
<link rel="stylesheet" href="style.css" />
<style>
@import url("app.css");
</style>
在我们的@ import
网址中添加引号可修复Chrome的Preload Scanner(N.B.在Opera和Safari中也会出现相同的瀑布。)
这绝对是WebKit / Blink中的一个错误 - 缺少引号不应该隐藏Preload Scanner中的@imported样式表。
不要在Async 脚本之前放置<link rel =“stylesheet”/>
上一节讨论了如何通过其他资源减慢CSS,本节将讨论CSS如何无意中延迟下载资源的下载,主要是使用异步加载代码段插入的JavaScript,如下所示:
<script>
var script = document.createElement('script');
script.src = "analytics.js";
document.getElementsByTagName('head')[0].appendChild(script);
</script>
在所有浏览器中都存在一种有意和预期的迷人行为,但我从未遇到过一个了解它的开发人员。 当您考虑它可以带来的巨大性能影响时,这是非常令人惊讶的:
如果有任何当前CSS在加载,浏览器将不会执行<script>。
<link rel="stylesheet" href="slow-loading-stylesheet.css" />
<script>
console.log("I will not run until slow-loading-stylesheet.css is downloaded.");
</script>
这是设计的。 这是故意的。 当前正在下载任何CSS时,HTML中的任何同步<script>都不会执行。 这是一个简单的防御策略来解决<script>可能会询问页面样式的边缘情况:如果脚本在CSS到达并被解析之前询问页面的颜色,那么JavaScript给我们的答案 可能是不正确或陈旧的。 为了缓解这种情况,浏览器在构造CSSOM之前不会执行<script>。
这样做的结果是,CSS下载时间的任何延迟都会对你的异步片段产生连锁反应。 用一个例子可以很好地说明这一点。
如果我们在异步片段前放置<link rel =“stylesheet”/>,则在下载和解析该CSS文件之前它不会运行。
<link rel="stylesheet" href="app.css" />
<script>
var script = document.createElement('script');
script.src = "analytics.js";
document.getElementsByTagName('head')[0].appendChild(script);
</script>
根据这个顺序,我们可以清楚地看到JavaScript文件甚至在构建CSSOM之前甚至没有开始下载。 我们完全失去了任何并行化:
在异步代码段之前使用样式表可以撤消我们并行化的机会。
有趣的是,Preload Scanner希望提前获得对analytics.js的引用,但是我们无意中隐藏了它:“analytics.js”是一个字符串,并且在<<之前不会成为可标记的src属性 script>元素存在于DOM中。 这是我早些时候说的,当我稍后再说这个时。
第三方供应商提供这样的异步代码片段以更安全地加载脚本是很常见的。 开发人员对这些第三方持怀疑态度,并在页面后面放置异步片段也是很常见的。 虽然这是出于最好的意图 - 我不想在我自己的资产之前放置第三方<script>! - 通常可能是净损失。 事实上,谷歌分析甚至告诉我们该做什么,他们是对的:
将此代码作为第一项复制并粘贴到您要跟踪的每个网页的<HEAD>中。
所以我的建议是:
如果您的<script> ... </ script>块不依赖于CSS,请将它们放在样式表上方。
以下是我们转向此模式时会发生的代码:
<script>
var script = document.createElement('script');
script.src = "analytics.js";
document.getElementsByTagName('head')[0].appendChild(script);
</script>
<link rel="stylesheet" href="app.css" />
交换样式表和异步代码片段可以重新获得并行化。
现在您可以看到我们已经完全重新获得了并行化,并且页面加载速度提高了近2倍。
在CSS之前放置任何非CSSOM查询JavaScript; 在CSS之后放置任何CSSOM查询JavaScript
更进一步,除了异步加载片段之外,我们应该如何更普适地加载CSS和JavaScript? 为了解决这个问题,我提出了以下问题并从那里开始工作:
如果:
- 在CSSOM构造上阻止CSS后定义的同步JS;
- 同步JS阻止DOM构造
那么 - 假设没有相互依赖 - 哪个更快/更喜欢?
Script -> style;
style -> script?
答案是:
如果文件不相互依赖,那么您应该将阻塞脚本置于阻塞样式之上 - 没有必要将JavaScript执行延迟到JavaScript实际上不依赖的CSS。
(Preload Scanner确保即使在脚本上阻止了DOM构造,CSS仍然会并行下载。)
如果你的一些JavaScript做了但有些不依赖于CSS,那么加载同步JavaScript和CSS的绝对最佳顺序是将JavaScript分成两部分并将其加载到CSS的任何一侧:
<!-- This JavaScript executes as soon as it has arrived. -->
<script src="i-need-to-block-dom-but-DONT-need-to-query-cssom.js"></script>
<link rel="stylesheet" href="app.css" />
<!-- This JavaScript executes as soon as the CSSOM is built. -->
<script src="i-need-to-block-dom-but-DO-need-to-query-cssom.js"></script>
使用这种加载模式,我们可以按最佳顺序进行下载和执行。 我为下面的截图中的微小细节道歉,但希望你能看到代表JavaScript执行的小粉红色标记。
entry(1)是计划在其他文件到达和/或执行时执行某些JavaScript的HTML;
entry(2)执行它到达的那一刻;
entry(3)是CSS,所以不执行任何JavaScript;
在CSS完成之前,entry(4)实际上不会执行。
注: 您必须根据自己的特定用例测试此模式:根据您之前的CSS JavaScript文件与CSS本身之间的文件大小和执行成本是否存在巨大差异,可能会有不同的结果。 测试,测试,测试。
将<link rel =“stylesheet”/>放在<body>中
这个最终策略是一个相对较新的策略,对感知性能和渐进式渲染有很大好处。 它也非常友好。
在HTTP / 1.1中,我们将所有样式连接到一个主要包中是很典型的。 我们称之为app.css:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="app.css" />
</head>
<body>
<header class="site-header">
<nav class="site-nav">...</nav>
</header>
<main class="content">
<section class="content-primary">
<h1>...</h1>
<div class="date-picker">...</div>
</section>
<aside class="content-secondary">
<div class="ads">...</div>
</aside>
</main>
<footer class="site-footer">
</footer>
</body>
这带来三个关键的低效率:
- 任何给定的页面只会使用app.css中的一小部分样式:我们几乎肯定会下载比我们需要的更多的CSS。
- 我们受限于一种效率低下的缓存策略:例如,对仅在一个页面上使用的日期选择器上当前所选日期的背景颜色进行更改将需要我们缓存整个app.css。
- 整个app.css阻止渲染:如果当前页面只需要17%的app.css并不重要,我们仍然需要等待其他83%才能开始渲染。
使用HTTP / 2,我们可以开始解决点(1)和(2):
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="core.css" />
<link rel="stylesheet" href="site-header.css" />
<link rel="stylesheet" href="site-nav.css" />
<link rel="stylesheet" href="content.css" />
<link rel="stylesheet" href="content-primary.css" />
<link rel="stylesheet" href="date-picker.css" />
<link rel="stylesheet" href="content-secondary.css" />
<link rel="stylesheet" href="ads.css" />
<link rel="stylesheet" href="site-footer.css" />
</head>
<body>
<header class="site-header">
<nav class="site-nav">...</nav>
</header>
<main class="content">
<section class="content-primary">
<h1>...</h1>
<div class="date-picker">...</div>
</section>
<aside class="content-secondary">
<div class="ads">...</div>
</aside>
</main>
<footer class="site-footer">
</footer>
</body>
现在我们正在解决冗余问题,因为我们能够加载更适合页面的CSS,而不是不加选择地下载所有内容。 这减少了关键路径上阻塞CSS的大小。
我们还可以采用更有意思的缓存策略,只缓存破坏需要它的文件,并保持其余部分不受影响。
我们还没有解决的问题是它仍然阻止渲染 - 我们仍然只有最慢的样式表。 这意味着如果无论出于何种原因,site-footer.css需要很长时间才能下载,浏览器无法开始渲染.site-header。
但是,由于Chrome最近发生了变化(我相信版本69),以及Firefox和IE / Edge中已经存在的行为,<link rel =“stylesheet”/> 只会阻止后续内容的呈现,而不是 整页。 这意味着我们现在能够像这样构建我们的页面:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="core.css" />
</head>
<body>
<link rel="stylesheet" href="site-header.css" />
<header class="site-header">
<link rel="stylesheet" href="site-nav.css" />
<nav class="site-nav">...</nav>
</header>
<link rel="stylesheet" href="content.css" />
<main class="content">
<link rel="stylesheet" href="content-primary.css" />
<section class="content-primary">
<h1>...</h1>
<link rel="stylesheet" href="date-picker.css" />
<div class="date-picker">...</div>
</section>
<link rel="stylesheet" href="content-secondary.css" />
<aside class="content-secondary">
<link rel="stylesheet" href="ads.css" />
<div class="ads">...</div>
</aside>
</main>
<link rel="stylesheet" href="site-footer.css" />
<footer class="site-footer">
</footer>
</body>
这样做的实际结果是,我们现在能够逐步呈现我们的页面,在页面可用时有效地将页面输送样式添加到页面中。
在目前不支持这种新行为的浏览器中,我们不会遇到性能下降:我们会回到原来的行为,我们只有最慢的CSS文件加载完成才会展示页面。
总结
本文中有很多要消化的内容。 它最终超越了我最初打算写的帖子。 尝试总结加载CSS的最佳网络性能实践:
-
Lazyload Start Start Render不需要的任何CSS:
拆分关键CSS; 或将您的CSS拆分为媒体查询。
-
避免@import:
在你的HTML中; 特别是在CSS中; 并提防Preload Scanner的奇怪之处。
-
警惕同步CSS和JavaScript命令:
在CSSOM完成之前,CSS之后定义的JavaScript将无法运行 所以如果你的JavaScript不依赖于你的CSS,在CSS之前加载它; 如果它取决于你的CSS,在CSS之后加载它。
- 在DOM需要时加载CSS,这将取消阻止“开始渲染”并允许渐进式渲染
我上面概述的所有内容都遵循规范或已知/预期的行为,但是,一如既往,自己测试一切。 虽然这在理论上都是正确的,但在实践中事情总是有所不同。 套用中国的一句老话,实践出真知啊。
创建了一个程序员交流微信群,大家进群交流IT技术
如果已过期,可以添加博主微信号15706211347,拉你进群