呈现一个页面时,在浏览器中会打开众多进程,包括浏览器、渲染、插件、GPU、网络等进程。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
浏览器进程负责存储、界面、下载等管理。在渲染进程中,运行着熟知的主线程、合成线程、JavaScript 解释器、排版引擎等。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
而呈现一个页面大致可分为 4 个步骤:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
- 浏览器进程处理用户在地址栏的输入,然后将 URL 发送给网络进程。
- 网络进程发送 URL 请求,在接收到响应数据后进行解析,接着转发给浏览器进程。
- 浏览器进程收到响应后,发送“提交导航”消息到渲染进程。
- 渲染进程开始接收网络进程发送的数据,并进行文档渲染。
基于上述步骤可以联想到,呈现的优化分为两部分:资源和渲染。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
像上一节的图像其实也属于资源部分,只是内容比较多就单独创建了章节。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
本文所用的示例代码已上传至 Github。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
一、资源
HTTP Archive 关于 2022 年页面大小的报告指出,按大小升序后,排在中间位置的移动页面大概有 70 个请求。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
包括 22 个图像、21 个脚本、7 个 CSS以及 2 个 HTML,脚本和 CSS 占了 40% 的请求。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
除了对这些资源进行尺寸优化之外,还可以对它们的加载进行优化。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
1)优先级文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
浏览器会给不同资源给予不同的请求优先级。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
以 Chrome 为例,分为多个等级,包括 Highest 、High、Low 和 Lowest 等,如下图所示。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
HTML 和 head 元素中的 CSS 优先级是最高的,head 元素中的脚本是高优先级,异步请求的脚本是低优先级。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
若优先级不符合预期,可以通过一些配置修改优先级,例如为 script 元素声明 async/defer,它的优先级就会变成低。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
在 img 元素中,新增了一个 fetchPriority 属性(如下所示),当值是 high 时,意味着这是一张重要的图像,浏览器会提升优先级立即开始请求。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
<img src="hero.png" fetchpriority="high" />
2)link 元素文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
link 元素常用来加载 CSS 文件,但它还支持些其他功能,接下来会一一介绍。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
当 link 的 rel 属性值为 preload 时,就能预加载资源,如下所示。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
<link rel="preload" href="demo.js" as="script" />
as 属性是告知浏览器加载的资源类型,包括 style、script、font、image 等。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
预加载可提升资源的优先级,不过当资源在几秒后未使用时,浏览器会发出告警。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
当 link 的 rel 属性值为 preconnect 时,就能预连接站点,如下所示。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
<link rel="preconnect" href="https://www.pwstrick.com" />
另一个与连接相关的类型是 dns-prefetch(如下所示),用来处理 DNS 查询,即 DNS 预解析。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
<link rel="dns-prefetch" href="https://www.pwstrick.com" />
当 link 的 rel 属性值为 prefetch 时,就能预提取资源,如下所示。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
<link rel="prefetch" href="demo.js" />
预提取会让资源的优先级降为最低,用于让某些非关键资源提前请求,可为用户的下一步交互做准备。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
2023-03-23 当 link 的 rel 属性值为 prerender 时,就能预渲染指定的网站,如下所示。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
<link rel="prerender" href="https://www.pwstrick.com" />
不过,该参数的兼容性有限,Safari 和 Firefox 都不支持,如下图所示。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
有个名为 Tachyon 的开源库,基于 prerender,对页面之间的导航进行了提速。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
在用户将鼠标移动到链接时,会通过创建 link 元素,并赋予 prerender,实现指定地址的预渲染。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
3)script 元素文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
延迟(defer)和异步(async)的出现是为了解决 script 元素阻塞 HTML 解析的问题,下图描绘了 script 元素的 3 种运行机制。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
第一行是默认的运行机制,在解析HTML文档时,一遇到 script 元素就停止解析,改成下载外部脚本,然后执行脚本,执行完后才会继续解析。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
第二行是使用了 defer 属性后的运行机制,HTML 文档的解析和外部脚本的下载是同时进行的,解析完后才会执行脚本。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
第三行是使用了async 属性后的运行机制,HTML 文档的解析和外部脚本的下载也是同时进行,但下载完后就开始执行脚本,执行完后才会继续解析。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
4)数据预请求文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
在客户端的 WebView 中,每次请求后端接口大概要花 100~200ms,如果把这段时间省下来,那么也能减少白屏时间。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
数据预请求是将请求时机由业务发起提前到用户点击时,并行发送数据请求,缩短数据等待时间,如下图所示。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
这种改造需要客户端配合,现在简单介绍下我们公司当时实现的方案,流程图如下所示。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
首屏数据的接口信息,可以通过一些配置关联起来,比如一个单独的配置接口。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
客户端在拿到数据后,就会缓存到一个全局变量中,等待脚本读取。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
注意,到底是客户端先拿到数据,还是网页先拿到,这个无法确定,并且预请求只能以 get 方法通信。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
具体的实现方案如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
- 客户端分析出当前 URL 中的路径和参数,其中 refresh 参数(有的话)是一个时间戳(秒),这个参数用来控制客户端是否需要重新请求配置接口。
- 当分析的 URL 参数中无 refresh 字段时,访问 https://xxx.com/settings 接口,并将URL路径、客户端默认带的参数(包含用户ID等)和 URL 本身的参数全部传递过来(如下所示),然后本地缓存。
https://xxx.com/settings?path=game%2Fstrick&uid=xxxxx&refresh=1618451992
- 客户端会将 settings 接口的响应数据缓存到本地,而 key 就是当前 URL,也就是说 URL 不变的话,默认就不会去请求 settings 接口。若要穿透缓存,那么加上 refresh 参数,赋一个与之前不同的值即可。
- settings 接口返回的 JSON 格式,包含 urls 字段(如下所示),是个数组,由接口集合组成,已经拼接好参数。
{
"urls": [
"http://xxx.com/xx/xx?id=2",
"http://xxx.com/yy/yy?uid=1"
]
}
- 客户端将读取到的数据注入到 WebView 的全局对象中,可以用全局变量同步读取,名字可自行约定,例如叫 TheLClientResponse,读取方式:window.TheLClientResponse,JSON 格式如下,其中 key 是 api 的路径,如果无数据可以返回 null。
{
"xx/xx": {
code: 0,
msg: "test",
data: {
list: []
}
},
"yy/yy": {
code: 0,
msg: "test",
data: {
list: []
}
}
}
5)字体文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
CSS3 提供了 @font-face 规则允许为网页指定自定义字体,其声明和使用如下所示。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
@font-face {
font-family: "iconfont";
src: url("../font/iconfont.woff2") format("woff2"),
url("../font/iconfont.woff") format("woff"),
url("../font/iconfont.ttf") format("truetype");
}
.iconfont {
font-family: "iconfont";
}
上述字体来源于 iconfont,为了兼容性考虑,往往会提供多个格式的字体。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
其中 ttf 是一种未压缩的格式,另外两种内部都做过压缩。在 2022 年大概有 75%~78% 的网页在使用 woff2 格式的字体。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
使用字体除了改变文字外形之外,还有一种普遍用法是用来显示 icon 小图标。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
CSS3 提供了 font-display 属性用于指定字体的渲染方式,在 @font-face 中声明,2022 年用的最多的值是 swap。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
swap 会让文字先按浏览器默认的字体展示,当字体加载完成后,再将其替换掉。在慢网中,会看到字体的前后变化。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
所以应该尽快加载字体,才能让用户享受到最优的体验。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
浏览器在解析 CSS 文件时,并不会马上下载 @font-face 中的字体文件。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
只有当发现 HTML 中有非空节点使用该字体时,才会开始下载。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
如果要提早下载,那么可以使用预加载,如下所示。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
<link rel="preload" href="../../assets/font/dakai.woff2" as="font" crossorigin="anonymous"/>
crossorigin 属性是必填的,表示允许跨域,若省略,就会有告警。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
还有一种优化方法是提取字体的子集(即有选择性的将需要的字符组合在一起),减小字体文件的尺寸,像图标就比较适合这样自定义。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
二、渲染过程
浏览器的渲染过程大致可分为 8 个阶段,如下图所示。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
下面的 1~5 步涉及主线程(main thread),6~8 步涉及合成线程(compositor thread)。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
- 将 HTML 解析成 DOM 树,并将其存储在内存中,同时下载解析到的资源。
- 将 CSS 解析成样式表(style sheets),即生成 CSSOM,在此阶段会计算节点样式,并把相对的值和单位都转换成像素。
- 通过 DOM 和样式表生成布局树(layout tree),在此阶段会计算元素的尺寸和坐标,并且在树中不包含隐藏元素,但会包含 CSS 中创建的内容。
- 对布局树进行分层,生成分层树(layer tree),可控制绘画顺序,裁剪元素内容,CSS 中的 transform、z-index、will-change 等属性都与层相关。
- 通过布局树和分层树生成绘制列表,并将其提交给合成线程。
- 通过绘制列表和图层生成图块(tile),因为渲染所有图块会比较昂贵,所以会划分优先级,例如视口中的可见图块优先级会高。
- 图块在提交到光栅化(raster)线程池后,会被转移到 GPU 中,加速光栅化处理,即转换成位图(bitmap),最终结果会存储在 GPU 内存中。
- GPU 将位图传送回合成线程后,就会生成合成帧,处理完所有位图后,合成器线程向浏览器发送 Draw Quad 命令,开始在屏幕上显示页面。
虽然这 8 个阶段的执行过程比较复杂,但是在现代浏览器中,它们会在 1/60 秒(即 16.67 毫秒)内完成,下图描述了整个渲染过程。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
优化渲染过程的核心就是缩短某个阶段的执行时间,或者直接跳过某些阶段。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
1)流式渲染文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
HTTP/1.1 协议支持分块传输编码(chunked transfer encoding),允许服务器将网页数据分成多块后再进行传输。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
在响应头中设置 Transfer-Encoding: chunked 就会启用分块传输编码的响应格式。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
浏览器在知道 HTML 会被流式返回后,就不用等到 HTML 下载完成后再开始解析了。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
不过,目前流行的客户端渲染(Client Side Render)其实并不需要专门的流式渲染,因为 HTML 的内容本来就少。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
若改成服务端渲染(Server Side Render),那就可根据实际情况进行流式渲染的优化了。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
具体的实现过程,本文不再赘述,可参考网上相关的方案,例如 Vue SSR 指南中的流式渲染。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
2)DOM文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
HTML 在被解析时,一旦遇到 JavaScript,那么就会被阻塞,如下图所示。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
当遇到外部脚本时,还会停止 DOM 树的构建,转由网络进程去请求 JavaScript 脚本地址。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
CSS 本身并不会阻塞 DOM 树的构建,但在与 JavaScript 结合使用时,会出现阻塞。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
在下面的示例中,JavaScript 会修改 demo.css 文件中的样式。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
<link rel="stylesheet" href="demo.css" />
<div id='root'>内容</div>
<script>
const root = document.getElementById('root');
root.style.color = 'red';
</script>
主线程在执行脚本之前,需要先计算节点样式(即解析 CSS 文件),因此 DOM 树就无法被继续构建了。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
若要优化 DOM 树的构建,除了尽量避免上述不科学的写法之外,还可以从两方面入手:减少关键资源请求的数量和大小。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
所谓关键资源(key resource),更确切的说就是网页首屏的核心资源,没有它们,那么首屏将无法正确的呈现。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
减少资源的请求数量可以通过 2 个方法:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
- 将 CSS 或 JavaScript 内联到 HTML 结构中,例如移动端的屏幕适配脚本就比较适合内联。
- 脚本元素可以增加 async 或 defer 的标记,具体可以参考上一节的 script 元素。
关键资源的大小除了进行压缩外,就是只提取首屏需要的代码。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
将其他部分的代码合并到另一个文件,待需要时再加载,或者使用上一节所说的预提取。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
3)重排和重绘文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
重排(reflow)也叫回流,是指修改元素的几何属性后引起的重新渲染,涉及 7 个阶段,如下图所示,修改了元素的高度。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
触发重排的情况有添加或删除可见的元素、修改位置、边距或内容等。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
重绘(repaint)是指修改元素的背景颜色后引起的重新渲染,但与重排不同,重绘将直接进入 Paint 阶段,如下图所示。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
重排和重绘都会降低渲染性能,因为它们都发生在主线程中,并且布局、分层和绘制 3 个阶段的计算过程比较昂贵。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
当在脚本中获取元素的尺寸、位置等排版相关的信息时,就有可能触发强制重排,例如调用 offsetTop、clientWidth、getComputedStyle() 等属性或方法。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
优化它们的方式包括使用 cssText 或 CSS 类修一次性修改多个 CSS 属性,批量修改 DOM,例如使用文档片段 fragment、先隐藏元素再显示等。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
在众多的 CSS 属性中,有两个 CSS 属性(transform 和 opacity)可以避开重排和重绘,直接进入合成阶段。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
例如用 transform 属性实现的元素变化,就不会占用主线程,而是由合成线程处理,如下图所示。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
值得一提的是,早期在脚本中实现动画,都会借助定时器,但定时器无法精确的配置动画帧之间的时间间隔。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
按屏幕刷新率为每秒 60 次计算,那么理论上每帧的间隔约等于是 16.67 毫秒。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
但实际情况比较复杂,间隔不一定是这个值,有可能出现丢帧,从而造成动画不够平滑流畅。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
为了解决动画问题,浏览器提供了 requestAnimationFrame() 方法,在每一帧的开始执行配置的回调。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
注意,只有当浏览器 GPU 生成位图和屏幕显示位图保持同步时,才会触发 requestAnimationFrame() 的回调。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
在下面的示例中,让绝对定位的 span 元素通过 requestAnimationFrame() 向右偏移。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
<span id='container' style="position:absolute">内容</span>
<script>
let left = 0;
const frame = () => {
const container = document.getElementById('container');
container.style.left = `${left++}px`;
if (left > 100) return;
requestAnimationFrame(frame);
};
requestAnimationFrame(frame);
</script>
注意,requestAnimationFrame() 也是运行在主线程中,如果主线程繁忙,那么也有可能延迟回调,造成动画的卡顿。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
并且如果其回调比较耗时(超过一帧),那么就会阻碍后续的任务。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
总结
本文的第一章节详细描述了资源的优化,并在开篇指出资源都存在着优先级,浏览器会按优先级进行请求。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
预加载可提升资源的优先级,预提取可降低资源的优先级,预连接可提前进行 TCP 连接或 DNS 查询。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
script 元素有延迟和异步两种运行机制,可有效地防止 HTML 解析的阻塞。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
数据预请求需要与客户端配合,本文给出了一份解决方案可供参考。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
自定义字体在页面开发中有着广泛的应用,常用的优化手段是预加载和减小尺寸。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
在第二章节中详细分析了浏览器的渲染过程,这个过程大致可分为 8 个阶段。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
围绕这些阶段,引出了流式渲染、DOM 树构建的优化。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
在重排和重绘中,详细说明了它们影响的阶段,并且列举了触发原因,以及优化手段。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html
最后提到了合成动画,并且对比了 JavaScript 动画的两种实现方式。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/39064.html