在衡量 Web 页面性能的时候有一个重要的指标叫“FP(First Paint)”,是指从页面加载到首次开始绘制的时长。这个指标直接影响了用户的跳出率,更快的页面响应意味着更多的 PV、更高的参与度,以及更高的转化率。
浏览器将HTML,CSS,JavaScript到首次渲染到屏幕上(首屏),这期间所经历的一系列步骤,叫做关键渲染路径(Critical Rendering Path)。
可见,通过优化关键渲染路径,我们可以显着缩短首次渲染页面的时间,从而得到一个更好的用户体验。
简单了解一下页面渲染流程(webkit): 渲染大概可以划分成以下几个步骤:
解析html建立DOM树(深度遍历)将CSS解析成 CSSOM树根据DOM树和CSSOM树来构造 Render树布局render树,负责各元素尺寸、位置的计算使用UI后端层绘制render树,绘制页面像素信息浏览器会将各层的信息发送给GPU,GPU会将各层合成,显示在屏幕上。关键资源指的是那些可以阻塞页面首次渲染的资源。例如JavaScript、CSS都是可以阻塞关键渲染路径的资源,这些资源就属于“关键资源”。图片不属于关键资源, 因为图片不会导致阻塞游览器渲染。
默认情况下,CSS 被视为阻塞渲染的资源,这意味着直至 CSSOM 构建完毕前,浏览器将不会渲染任何内容。
在渲染树构建中,我们看到关键渲染路径要求我们同时具有 DOM 和 CSSOM 才能构建渲染树。这会给性能造成严重影响:HTML 和 CSS 都是阻塞渲染的资源。 HTML 显然是必需的,因为如果没有 DOM,我们就没有可渲染的内容,但CSS 的也是很有必要的,一个页面没有样式,效果可想而知,不过哪怕没设置样式,浏览器有内部样式表给相应标签特定的样式。所以,在DOM 或 CSSOM 没构建完毕之前,是不会构建渲染树的,也就是阻塞渲染了。 注意:chrome已经摈弃cssom而是直接解析stylesheet,故也没有所谓渲染树,而是布局树,所以使用谷歌开发者工具的性能面板分析时候并不完全如文中所述,chrome做了很多的优化。当然你也可以了解一下:谷歌浏览器渲染流程
默认情况下,CSS 被视为阻塞渲染的资源。
我们可以通过媒体类型和媒体查询将一些 CSS 资源标记为不阻塞渲染。
浏览器会下载所有 CSS 资源,无论阻塞还是不阻塞。
CSS 是阻塞渲染的资源。需要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间。虽说css会阻塞渲染关键路径,而实际上并不是所有的css资源都那么得关键。 举个🌰:一些响应式CSS只在屏幕宽度符合条件时才会生效,还有一些CSS只在打印页面时才生效。这些CSS在不符合条件时,是不会生效的,那么我们为什么要让浏览器等待我们并不需要的CSS资源呢?针对这种情况,我们应该让这些非关键的CSS资源不阻塞渲染。
我们可以通过 CSS“媒体类型”和“媒体查询”来解决这类用例:
<link href="style.css" rel="stylesheet"> <link href="style.css" rel="stylesheet" media="all"> <link href="print.css" rel="stylesheet" media="print"> <link href="portrait.css" rel="stylesheet" media="orientation:portrait"> <link href="other.css" rel="stylesheet" media="(min-width: 40em)">媒体查询由媒体类型以及零个或多个检查特定媒体特征状况的表达式组成。
第一个声明阻塞渲染,适用于所有情况第二个声明同样阻塞渲染:“all”是默认类型,如果您不指定任何类型,则隐式设置为“all”。因此,第一个声明和第二个声明实际上是等效的。第三个声明只在打印网页时起作用,因此网页首次在浏览器中加载时,它不会阻塞渲染。第四个声明具有动态媒体查询,将在网页加载时计算。根据网页加载时设备的方向,portrait.css 可能阻塞渲染,也可能不阻塞渲染。最后一个窗口声明宽度大于40em时候起作用,符合条件时,浏览器将阻塞渲染,直至样式表下载并处理完毕。上面提供的方法是针对那些不需要生效的CSS资源,如果CSS资源需要在当前页面生效,只是不需要在首屏渲染时生效,那么为了更快的首屏渲染速度,我们可以将这些CSS也设置成非关键资源。只是我们需要一些比较hack的方式来实现这个需求:
<link href="style.css" rel="stylesheet" media="print" onload="this.media='all'">上面代码先把媒体查询属性设置成print,将这个资源设置成非阻塞的资源。然后等这个资源加载完毕后再将媒体查询属性设置成all让它立即对当前页面生效。
类似的方案还有:
<link rel="preload" href="style.css" as="style" onload="this.rel='stylesheet'"> <link rel="alternate stylesheet" href="style.css" onload="this.rel='stylesheet'">最后,请注意“阻塞渲染”仅是指浏览器是否需要暂停网页的首次渲染,直至该资源准备就绪。无论哪一种情况,浏览器仍会下载 CSS 资产,只不过不阻塞渲染的资源优先级较低罢了。
谷歌开发工具的Performance面板:你可看到css设置了media="print"的网页的首绘时间(FP)会比没有设置媒体类型的网页的提早了不少。首绘时间和网络速度以及电脑硬件性能也是有关系的,并非固定值。设置了媒体类型的CSS文件你可以看到其优先级是lowest,而没设置的是Highest 链接:如何评估关键渲染路径?
关于CSS的加载有这么多门道,到底怎样才是最佳实践?答案是:Critical CSS。
Critical CSS的意思是:把首屏渲染需要使用的CSS通过style标签内嵌到head标签中,其余CSS资源使用异步的方式非阻塞加载。
所以Critical CSS从两个方面解决了性能问题:
减少关键资源的数量(将所有与首屏渲染无关的CSS使用异步非阻塞加载)减少关键路径的长度(将首屏渲染需要的CSS直接内嵌到head标签中,移除了网络请求的时间)。避免使用@import
<!doctype html> <html> <head> <meta charset="UTF-8"> <title>Demos</title> <link rel="stylesheet" href="http://127.0.0.1:8887/style.css"> <link rel="stylesheet" href="https://lib.baomitu.com/CSS-Mint/2.0.6/css-mint.min.css"> </head> <body> <div class="cm-alert">Default alert</div> </body> </html>上面这段代码使用link标签加载了两个CSS资源。这两个CSS资源是并行下载的。谷歌开发工具性能面板图:
<!doctype html> <html> <head> <meta charset="UTF-8"> <title>Demos</title> <link rel="stylesheet" href="http://127.0.0.1:8887/style.css"> <style type="text/css"> /* style.css */ @import url('https://lib.baomitu.com/CSS-Mint/2.0.6/css-mint.min.css'); body{background:red;} </style> </head> <body> <div class="cm-alert">Default alert</div> </body> </html>可以看到两个CSS变成了串行加载,前一个CSS加载完后再去下载使用@import导入的CSS资源。这无疑会导致加载资源的总时间变长。(别人测试估计在2019年初) 但很遗憾,在我的测试(2020年中),我发现目前谷歌浏览器应该再度升级优化了,呈现的效果是: 依然是并行的,可见谷歌浏览器更新迭代之快。
与CSS资源相似,JavaScript资源也是关键资源,JavaScript资源会阻塞DOM的构建。并且JavaScript会被CSS文件所阻塞。为了避免阻塞,可以为script标签添加async或defer属性。
defer与async这两个属性会使 script 异步加载,只针对外联脚本,对于inline-script是无效的。 defer: defer 属性表示延迟执行引入 JavaScript,即 JavaScript 加载时 HTML 并未停止解析,这两个过程是并行的。整个 document 解析完毕且 defer-script 也加载完成之后,会执行所有由 defer-script 加载的 JavaScript 代码,再次触发 DOMContentLoaded事件。
async: async 属性表示异步执行引入的 JavaScript,与 defer 的区别在于,如果已经加载好,就会开始执行,无论此刻是 HTML 解析阶段还是 DOMContentLoaded 触发(HTML解析完成事件)之后,但一定在 load 触发之前执行。
浏览器对于带有type="module"的<script>,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。但若使用了async属性,就会转为async的行为特征。
值得注意的是,通过js向 document 动态添加 script 标签时,async 属性默认是 true。
举个🌰:
<!doctype html> <html> <head> <meta charset="UTF-8"> <title>Demos</title> <link rel="stylesheet" href="http://127.0.0.1:8887/style.css"> <script type="text/javascript" src="https://www.google-analytics.com/analytics.js" ></script> </head> <body> <div class="cm-alert">Default alert</div> </body> </html>从捕获出的结果可以看到,JS资源加载完毕后,需要等待CSS资源加载完之后才会执行JS,并且JS会将DOM阻塞,所以最终domcontentloaded事件在2050ms与2100ms之间触发。
<!doctype html> <html> <head> <meta charset="UTF-8"> <title>Demos</title> <link rel="stylesheet" href="http://127.0.0.1:8887/style.css"> <script async type="text/javascript" src="https://www.google-analytics.com/analytics.js" ></script> </head> <body> <div class="cm-alert">Default alert</div> </body> </html>从图中可以看到,JS加载完后不再需要等待CSS资源,并且也不再阻塞DOM的构建,最终domcontentloaded事件在50ms与100ms之间触发。
关键渲染路径是浏览器将HTML,CSS,JavaScript转换为屏幕上所呈现的实际像素的具体步骤。而优化关键渲染路径可以提高网页的呈现速度,也就是首屏渲染优化。
通常在关键渲染路径中,DOM,CSSOM以及JavaScript这些步骤的性能最差。这些步骤是导致首屏渲染速度慢的主要原因。
关键资源的数量越少,浏览器处理渲染的工作量就越少,对 CPU 以及其他资源的占用也就越少。
关键资源的长度指的是获取所有关键资源时,在浏览器与服务器之间的往返次数。 例如:页面一个JS资源、那么理想的情况下关键路径长度是2(先请求一次HTML,检测到HTML含有外部JS文件然后再请求JS)、如果页面有两个资源CSS与JS,那么关键路径长度也是2,因为JS和CSS是可以同时加载的,因为浏览器有一个叫做“预加载扫描仪”的东西会并行加载关键资源。<link rel="preload">
何谓理想情况下?资源间没有依赖关系且资源大小较小(一个数据包搞定)。
那岂不是说无论多少css和js资源,因为预加载并行的原因,那关键路径的长度都只是2呢? 非也!资源之间是会存在依赖关系的,某些资源只能在上一资源处理完毕之后才能开始下载,那就必须要再一次往返B/S之间;还有的情况就是就是资源中又包含其他的关键资源;最后一点是,资源越大,下载所需的往返次数也越多,网络中传输的数据包的大小是有限制的。
关键字节的数量越少,处理内容并让其出现在屏幕上的速度就越快。要减少字节数,我们可以减少资源数(将它们删除或设为非关键资源),此外还要压缩和优化各项资源,确保最大限度减小传送大小。
对于提升应用的加载速度常用的手段有Http Cache、异步加载、304缓存、文件压缩、CDN、CSS Sprite、开启GZIP等等。这些手段无非是在做一件事情,就是让资源更快速的下载到浏览器端。但是除了这些方法,其实还有更加强大的Service Worker线程。
内联样式和嵌入样式可以减少页面请求,为什么还要使用link外联样式呢?
在构建 DOM Tree 的过程中,如果遇到 link 标记,浏览器就会立即发送请求获取样式文件。当然我们也可以直接使用内联样式或嵌入样式,来减少请求;但是会失去模块化和可维护性,并且像缓存和其他一些优化措施也无效了,利大于弊,性价比实在太低了;除非是为了极致优化首页加载等操作,否则不推荐这样做。
参考文档: optimizing-critical-rendering-path analyzing-crp measure-crp 优化关键渲染路径 以通俗的方式理解关键渲染路径