【非原创】对《CSS and Network Performance》一文的总结

原文地址(作者:Harry):CSS and Network Performance
译文地址(作者:sea_ljf):CSS 与网络性能

前几天读了这篇文章感觉写的很不错,所以简单做些笔记以及摘录方便后期查阅(本文非原创)。



为什么要通过 css 来提高性能?

CSS 是页面渲染的关键因素之一,当页面存在外链 CSS 时,浏览器会等待全部的 CSS 下载及解析完成后再渲染页面。css路径上的任何延迟都会影响首屏时间,因而我们需要尽快地将 CSS 传输到用户的设备,否则在页面渲染之前,用户只能看到一个空白的屏幕。

css 是渲染性能的关键这是因为:

  1. 浏览器直到渲染树构建完成后才会渲染页面;
  2. 渲染树由 DOM 与 CSSOM 组合而成;
  3. DOM 是 HTML 加上(同步)阻塞的 JavaScript 操作(DOM 后的)结果;
  4. CSSOM 是 CSS 规则应用于 DOM 后的结果;
  5. 使 JavaScript 非阻塞非常简单,添加 async 或 defer 属性即可;
  6. 相对而言,要让 CSS 变为异步加载是比较困难的;
  7. 所以记住这条经验法则:理想情况下,最慢样式表的下载时间决定了页面渲染的时间。

基于上述考虑,我们需要尽快构建 DOM 与 CSSOM。一般情况下,DOM 的构建是相对较快,(当请求某个页面时,)服务器响应的首个请求是 HTML 文档。但一般 CSS 是作为 HTML 的子资源而存在,因此 CSSOM 的构建通常需要更长的时间。

方法一:使用关键 CSS(最有效的方法)

关键css如何实现?

找出首次渲染所需的样式(通常是首屏相关的样式),将它们内联到 <head> 标签中,其他样式则通过异步的方式进行加载。

虽然这十分有效,但实施起来却并不容易,比如:高度动态化的网站通常难以提取首屏相关的样式、提取的过程需要自动化、需要对首屏不同元素显示或隐藏的状态作出假设、某些边界情况难以处理以及相关工具仍未成熟等问题。如果你的项目相当庞大或是有历史包袱,这将变得更为复杂。如果项目难以执行关键 CSS 策略,可以尝试根据媒体查询拆分 CSS 文件,这也是一种可靠的策略。

方法二:根据媒体查询拆分 CSS 文件

  1. 以非常高的优先级下载符合当前上下文(设备、屏幕尺寸、分辨率、方向等)的 CSS 文件,阻塞关键路径;
  2. 以非常低的优先级下载不符合当前上下文的 CSS 文件,不会阻塞关键路径。
  3. 浏览器基本上能将未命中媒体查询的 CSS 文件延迟下载。
  4. 浏览器仍然会下载全部的 CSS 文件,但只有符合当前上下文的 CSS 文件会阻塞渲染。
<link rel="stylesheet" href="./all.css" media="all" />
<link rel="stylesheet" href="./a.css" media="(max-width: 300px)"/>
<link rel="stylesheet" href="./b.css" media="(max-width: 500px)"/>
<link rel="stylesheet" href="./c.css" media="(max-width: 800px)"/>
<link rel="stylesheet" href="./d.css"/>

我们把css文件拆封成上面 all、a、b、c、d 五个文件通过改变浏览器宽度发现请求规律是下面这样的:

all.css > 被媒体查新命中的css文件 > d.css > 未被媒体查询命中的css文件

总结:all.css 之所以被首先加载是因为 all.css 的media="all" 所以 all.css 始终是被命中的,而且 all.css 也是最先被引入的,证明了浏览器会优先加载被媒体查询命中的css文件,多个文件同时被命中时请求顺序以引入的先后顺序决定。

为什么上文反复提到了阻塞路径,我们用 link 引入了多个css有被媒体查询击中的也有未被击中的,那么只有被击中的才会阻塞路径这这样有什么好处呢,我们想如果路径不被阻塞那么在css加载过程 html,js 也是同步加载的,这时候会有页面裸体的情况出现,而且如果这个时候DOM没有得到css的渲染那么我们用js去获取DOM的属性比如宽高是不是会有问题,所以要等css加载完浏览器才去渲染DOM,link 可以异步加载(多个link同时加载)但是这个时候DOM并不会渲染,上文的阻塞路径就是说只要被媒体查询击中的css加载完成DOM就开始渲染了,不会等未被击中的css加载。

方法三:避免在 CSS 文件中使用 @import

如果了解 @import 的原理,那应该清楚它的性能并不高,使用它会阻塞渲染更长时间。这是因为我们在关键路径上创造了更多队列式的网络请求,如果使用了 @import 那么请求过程是这样的:

  1. 下载 HTML;
  2. 请求并下载依赖的 CSS;下载及解析完成后,本该是构造渲染树,然而;
  3. CSS 依赖了其他的 CSS,继续请求并下载 CSS 文件;
  4. 构造渲染树。

也就是说使用 @import 加载的css是不会并行加载的,他会阻塞页面,这和我们开始的初衷相违背。

注意:有一个特殊的情况值得讨论。如果你没有包含 @import 的 CSS 文件的修改权限,为了让浏览器并行下载 CSS 文件,可以往 HTML 中补充相应的 <link rel="stylesheet" src="@import的地址" />。浏览器会并行下载相应的 CSS 文件且不会重复下载 @import 引用的文件。

方法四:不要将动态插入 JavaScript 的代码放在 link 之后

所有浏览器都存在一个鲜为人知,但符合逻辑的现象:在浏览器下载完该 CSS 文件之前,不会执行下面的 JS

<link rel="stylesheet" href="a.css" />
<script> console.log('hello word!') </script>

这是合理的。当 CSS 文件尚未下载完成时,HTML 文档中任何同步的 JavaScript 代码,均不会执行。考虑以下场景: script 中的代码会访问当前的页面样式,为确保结果正确,需要等待 script 标签前所有 CSS 文件下载并解析完毕后再获取,否则无法保证正确性。因此,在 css 渲染完成之前 css 后面的 js 不会执行,这也是为啥要把 link 方在 script 之前的原因。

看下面的代码示例:

<div id="box">asas</div>
<script> console.log(box.offsetWidth) // 宽度是屏幕宽度 </script>
<style>#box {width: 100px;height: 100px;background: pink;}</style>

<div id="box">asas</div>
<style>#box {width: 100px;height: 100px;background: pink;}</style>
<script> console.log(box.offsetWidth) // 宽度是100px </script>

总结:

  • 如果 link 在 script 之前那么 script 要等 link 加载完才去执行以保证样式的正确获取。
  • 如果 script 在 link 之前那么 script 不会等待 link 的渲染这样样式获取就会有问题。
  • 综上如果 script 是依赖 link 的那么必须要把 script 放在 link 下面。

看下面的例子:

<link rel="stylesheet" href="app.css"/>
<script>
  var script = document.createElement('script')
  script.src = "a.js"
  document.getElementsByTagName('head')[0].appendChild(script)
</script>

如果我们将一个 link 放在 script 之前,script 中动态创建新 script 的代码只会在 CSS 文件下载完之后才会执行,这意味着 CSS 推迟了资源的下载与执行,完全失去了并行下载的优势。

要了解为什么会并行下载我们要先了解一下浏览器的预加载扫描器:
各大浏览器都实现了一个名为预加载扫描器的辅助解析器。浏览器的核心解析器主要用于构建 DOM、CSSOM、运行 JavaScript 等。HTML 文档中某些标签与状态会阻塞核心解析器,因而核心解析器的运行是断断续续的。而预加载扫描器可以跳到核心解析器尚未解析的部分,用以发现其他待引用的子资源(如 CSS、JS 文件、图片等)。一旦发现此类子资源,预加载扫描器会开始下载它们,以便核心解析器在解析到对应内容时就能使用它们(,而不是直到那一刻才开始下载该资源)。预加载扫描器的出现,使网页的加载性能提高了19%,这是一项了不起的成就,可以极大地优化用户体验。

总结:
在 head 中将无需查询 CSSOM 的 JavaScript 代码放在 CSS 文件之前,需要查询的放在 CSS 文件之后,这样即便有动态生成的 script 资源也可以得到提前的简析和加载。对于第三方的 script 资源不可一概而论如非必要,放在页面末尾或空闲时下载及执行也未尝不可。(尽管执行 JavaScript 代码时会停止解析 DOM, 但预加载扫描器会提前下载之后的 CSS)

注:如果你一部分 JavaScript 需要依赖 CSS 而另一部分却不用,最佳的实践是将 JavaScript 分为两部分,分别置于 CSS 的两侧。根据这种组织方式,我们的页面会按最佳的方式下载与执行相关代码。

注意:head 中的代码组织基本可以按照这种方式,即 JS 在 CSS 之前,因为 head 中的 JS 代码基本不依赖 CSS,唯一的反例是 JS 代码体积非常大或执行时间很长。

方法五:合理构建 css 文件

我们习惯于将全部的 css 打成一个文件如 style.css,然而从三方面而言,渲染性能降低了:

  1. 每个页面只用到 style.css 中的部分样式用户会下载多余的 CSS。
  2. 难以制定缓存策略,例如服务端重新生成 style.css 后,旧的 style.css 缓存将失效。
  3. 整个 style.css 在解析构建完 CSSOM 之前,页面渲染被阻塞,尽管当前页面可能只用到了 17% 的 CSS代码,但(浏览器)仍需等待其他 83% 的代码下载并解析完后,才能开始渲染。

所以合理拆分 css,制定合理的 css 加载策略对性能是有一定好处的。


全文总结:

  • 懒加载非关键 CSS:优先加载关键 CSS,懒加载其他 CSS;或根据媒体类型拆分 CSS 文件。
  • 避免使用 @import:在 HTML 文档中应该避免;在 CSS 文件之中更应避免;以及警惕预加载扫描器的怪异行为。
  • 关注 CSS 与 JavaScript 的顺序:
    • 在 CSS 文件后的 JavaScript 仅在 CSSOM 构建完成后才会执行;
    • 如果你的 JavaScript 不依赖 CSS;将它放置于 CSS 之前;
    • 如果 JavaScript 依赖 CSS:将它放置于 CSS 之后。
  • 仅加载 DOM 依赖的 CSS:这将提高初次渲染的速度使让页面逐步渲染。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335