优化关键渲染路径(如何缩短页面首绘时长)

在衡量 Web 页面性能的时候有一个重要的指标叫“FP(First Paint)”,是指从页面加载到首次开始绘制的时长。这个指标直接影响了用户的跳出率,更快的页面响应意味着更多的 PV(Page View)、更高的参与度,以及更高的转化率。

简单了解一下页面渲染流程(webkit):
在这里插入图片描述
渲染大概可以划分成以下几个步骤:

  • 解析html建立DOM树(深度遍历)
  • 将CSS解析成 CSSOM树
  • 根据DOM树和CSSOM树来构造 Render树
  • 布局render树,负责各元素尺寸、位置的计算
  • 使用UI后端层绘制render树,绘制页面像素信息
  • 浏览器会将各层的信息发送给GPU,GPU会将各层合成,显示在屏幕上。

什么是关键渲染路径

浏览器将HTML,CSS,JavaScript到首次渲染到屏幕上(首屏),这期间所经历的一系列步骤,叫做关键渲染路径(Critical Rendering Path)。

在这里插入图片描述
可见,通过优化关键渲染路径,我们可以显著缩短首次渲染页面的时间,从而得到一个更好的用户体验。

什么是关键资源

关键资源指的是那些可以阻塞页面首次渲染的资源。例如JavaScript、CSS都是可以阻塞关键渲染路径的资源,这些资源就属于关键资源。图片不属于关键资源, 因为图片不会导致阻塞游览器渲染。

CSS 渲染阻塞

默认情况下,CSS 被视为阻塞渲染的资源,这意味着直至 CSSOM 构建完毕前,浏览器将不会渲染任何内容。

阻塞渲染怎么理解?

在渲染树构建中,我们看到关键渲染路径要求我们同时具有 DOM 和 CSSOM 才能构建渲染树。

这会给性能造成严重影响:HTML 和 CSS 都是阻塞渲染的资源。 HTML 显然是必需的,因为如果没有 DOM(根据HTML构建DOM),我们就没有可渲染的内容,但CSS 的也是很有必要的,一个页面没有样式,效果可想而知,不过哪怕没设置样式,浏览器有内部样式表给相应标签特定的样式。所以,在DOM 或 CSSOM 没构建完毕之前,是不会构建渲染树的,也就是阻塞渲染了。

  • 默认情况下,CSS 被视为阻塞渲染的资源。
  • 可以通过媒体类型和媒体查询将一些 CSS 资源标记为不阻塞渲染。
  • 浏览器会下载所有 CSS 资源,无论阻塞还是不阻塞。

CSS 是阻塞渲染的资源。需要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间。

将CSS设置为非阻塞渲染资源

虽说css会阻塞渲染关键路径,而实际上并不是所有的css资源都那么的关键。

举个🌰:一些响应式CSS只在屏幕宽度符合条件时才会生效,还有一些CSS只在打印页面时才生效。这些CSS在不符合条件时,是不会生效的,那么我们为什么要让浏览器等待我们并不需要的CSS资源呢?针对这种情况,我们应该让这些非关键的CSS资源不阻塞渲染。

CSS media

我们可以通过 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时候起作用,符合条件时,浏览器将阻塞渲染,直至样式表下载并处理完毕。
link media + onload

上面提供的方法是针对那些不需要生效的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资源加载最佳实践

关于CSS的加载有这么多门道,到底怎样才是最佳实践?
答案是:Critical CSS——把首屏渲染需要使用的CSS通过style标签内嵌到head标签中,其余CSS资源使用异步的方式非阻塞加载。

Critical CSS从两个方面解决了性能问题:

  1. 减少关键资源的数量(将所有与首屏渲染无关的CSS使用异步非阻塞加载)
  2. 减少关键路径的长度(将首屏渲染需要的CSS直接内嵌到head标签中,移除了网络请求的时间)。
避免使用@import

不使用@import引入CSS资源:

<!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资源是并行下载的。谷歌开发工具性能面板图:
在这里插入图片描述
使用@import引入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年中),我发现目前谷歌浏览器应该再度升级优化了,呈现的效果是:
在这里插入图片描述
依然是并行的,可见谷歌浏览器更新迭代之快。

异步JavaScript

与CSS资源相似,JavaScript资源也是关键资源,JavaScript资源会阻塞DOM的构建。并且JavaScript会被CSS文件所阻塞。为了避免阻塞,可以为<script>标签添加asyncdefer属性。

在实际工程中,我们常常将脚本资源放到文档底部。

deferasync这两个属性会使 <script> 异步加载,只针对外联脚本,对于inline-script是无效的。

在这里插入图片描述

defer

defer 属性会开启新的线程下载脚本文件并延迟执行引入 的JavaScript脚本,即 JavaScript 脚本加载时 HTML 并未停止解析,这两个过程是并行的。

如果声明了多个defer的脚本,则会按顺序下载并在整个 document 解析完毕且 defer-script 也加载完成之后按顺序执行,defer脚本会在DOMContentLoaded之后和load事件之前执行。

async

async 属性表示异步执行引入的 JavaScript,defer 的区别在于,如果已经加载好,就会开始执行,无论此刻是 HTML 解析阶段还是 DOMContentLoaded 触发(HTML解析完成事件)之后,但一定在 load 触发之前执行。

声明了多个async的脚本,其下载和执行都是异步的,不能确保彼此的先后顺序。

值得注意的是,通过js向 document 动态添加 script 标签时,async 属性默认是 true。

案例分析

正常加载

正常加载<script>:

<!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之间触发。

异步加载

异步加载<script>:

<!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之间;还有的情况就是就是资源中又包含其他的关键资源;最后一点是,资源越大,下载所需的往返次数也越多,网络中传输的数据包的大小是有限制的。

关键字节的数量越少,处理内容并让其出现在屏幕上的速度就越快。要减少字节数,我们可以减少资源数(将它们删除或设为非关键资源),此外还要压缩和优化各项资源,确保最大限度减小传送大小。

优化关键渲染路径的常规步骤

  1. 对关键路径进行分析和特性描述:资源数、字节数、长度。
  2. 最大限度减少关键资源的数量:删除它们,延迟它们的下载,将它们标记为异步等。
  3. 优化关键字节数以缩短下载时间(往返次数)。
  4. 优化其余关键资源的加载顺序:您需要尽早下载所有关键资产,以缩短关键路径长度。

优化关键渲染路径的常规步骤

  1. 缩小文件的尺寸
    删除不必要注释、空格与换行等无用的文本,特别是废弃代码
  2. 压缩文件
    将文本文件压缩(gzip等)
  3. 不要引入过多的资源文件
    将资源文件打包,或者组装结合到更少数量的资源文件中
  4. 首屏用到的CSS设置为内联样式,其他无关紧要CSS文件设置为非阻塞资源
  5. 使用异步JS
  6. 尽量不要使用 @import 指令

对于提升应用的加载速度常用的手段有Http Cache、异步加载、304缓存、文件压缩、CDN、CSS Sprite、开启GZIP等等。这些手段无非是在做一件事情,就是让资源更快速的下载到浏览器端。但是除了这些方法,其实还有更加强大的Service Worker线程。

内联样式和嵌入样式可以减少页面请求,为什么还要使用link外联样式呢?

在构建 DOM Tree 的过程中,如果遇到 link 标记,浏览器就会立即发送请求获取样式文件。当然我们也可以直接使用内联样式或嵌入样式,来减少请求;但是会失去模块化和可维护性,并且像缓存和其他一些优化措施也无效了,利大于弊,性价比实在太低了;除非是为了极致优化首页加载等操作,否则不推荐这样做。

==================================================================================================

参考文档:
optimizing-critical-rendering-path
analyzing-crp
measure-crp
优化关键渲染路径
以通俗的方式理解关键渲染路径

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值