Web性能指标与优化手段

今天无意间点开了MDN中关于性能优化的系列文章,学到了很多新的东西,也把很多旧的东西重新梳理了一下。

比如HTML不同的类型资源的加载顺序,是否会互相阻塞等问题,又比如学到了一些新的优化方式,如14K优化,dns-prefetch等

这个是入口地址:https://developer.mozilla.org/zh-CN/docs/Web/Performance

Web渲染的性能指标

在进行性能优化之前,我们需要为应用选择一个正确的度量标准(性能指标)以及设定一个合理的优化目标。

并不是所有指标都同样重要,这取决于你的应用。最后根据度量标准设定一个现实的目标。

下面是一些值得考虑的指标:

  • 首次有效绘制(First Meaningful Paint,简称FMP,当主要内容呈现在页面上)

  • 英雄渲染时间(Hero Rendering Times,度量用户体验的新指标,当用户最关心的内容渲染完成)

  • 可交互时间(Time to Interactive,简称TTI,指页面布局已经稳定,关键的页面字体是可见的,并且主进程可用于处理用户输入,基本上用户可以点击UI并与其交互)

  • 输入响应(Input responsiveness,界面响应用户输入所需的时间)

  • 感知速度指数(Perceptual Speed Index,简称PSI,测量页面在加载过程中视觉上的变化速度,分数越低越好)

  • 自定义指标,由业务需求和用户体验来决定。

FMP与英雄渲染时间非常相似,但它们不一样的地方在于FMP不区分内容是否有用,不区分渲染出的内容是否是用户关心的。

基于以上这些指标以及RAIL性能模型,我们可以设立一些目标,比如

  • 100毫秒的界面响应时间与60FPS

  • 速度指标(Speed Index)小于1250ms

  • 3G网络环境下可交互时间小于5s

  • 重要文件的大小预算小于170kb

浏览器渲染原理以及关键渲染路径

快速响应的网站提供更好的用户体验。用户期待内容快速加载和交互流畅的 Web 体验。

等待资源加载时间和大部分情况下的浏览器单线程执行是影响 Web 性能的两大主要原因。

等待时间是需要去克服来让浏览器快速加载资源的主要威胁。为了实现快速加载,开发者的目标就是尽可能快的发送请求的信息,至少看起来相当快。网络等待时间是在链路上传送二进制到电脑端所消耗的链路传输时间。Web 性能优化需要做的就是尽可能快的使页面加载完成。

大部分情况下,浏览器是单线程执行的。为了有流畅的交互,开发者的目标是确保网站从流畅的页面滚动到点击响应的交互性能。渲染时间是关键要素,确保主线程可以完成所有给它的任务并且仍然一直可以处理用户的交互。通过了解浏览器单线程的本质与最小化主线程的责任可以优化 Web 性能,来确保渲染的流畅和交互响应的及时。

第一步:导航

导航是加载 web 页面的第一步。它发生在以下情形:用户通过在地址栏输入一个 URL、点击一个链接、提交表单或者是其他的行为。

Web 性能优化的目标之一就是缩短导航完成所花费的时间,在理想情况下,它通常不会花费太多的时间,但是等待时间和带宽会导致它的延时。

DNS查询

对于一个 web 页面来说导航的第一步是要去寻找页面资源的位置。如果导航到 https://example.com,HTML 页面被定位到 IP 地址为 93.184.216.34 的服务器。如果以前没有访问过这个网站,就需要进行 DNS 查询。

浏览器向名称服务器发起 DNS 查询请求,最终得到一个 IP 地址。第一次请求之后,这个 IP 地址可能会被缓存一段时间,这样可以通过从缓存里面检索 IP 地址而不是再通过名称服务器进行查询来加速后续的请求。

通过主机名加载一个页面通常仅需要一次 DNS 查询。但是,对于页面指向的不同的主机名,则需要多次 DNS 查询。如果字体(fonts)、图像(images)、脚本(scripts)、广告(ads)和网站统计(metrics)都有不同的主机名,则需要对每一个主机名进行 DNS 查询。

TCP握手

一旦获取到服务器 IP 地址,浏览器就会通过 TCP“三次握手” (en-US)与服务器建立连接。这个机制的是用来让两端尝试进行通信——在浏览器和服务器通过上层协议 HTTPS 发送数据之前,可以协商网络 TCP 套接字连接的一些参数。

TCP 的“三次握手”技术经常被称为“SYN-SYN-ACK”——更确切的说是 SYN、SYN-ACK、ACK——因为通过 TCP 首先发送了三个消息进行协商,然后在两台电脑之间开始一个 TCP 会话。是的,这意味着终端与每台服务器之间还要来回发送三条消息,而请求尚未发出。

TLS协商

为了在 HTTPS 上建立安全连接,另一种握手是必须的。更确切的说是 TLS 协商,它决定了什么密码将会被用来加密通信,验证服务器,在进行真实的数据传输之前建立安全连接。在发送真正的请求内容之前还需要三次往返服务器。

虽然建立安全连接对增加了加载页面的等待时间,对于建立一个安全的连接来说,以增加等待时间为代价是值得的,因为在浏览器和 web 服务器之间传输的数据不可以被第三方解密。

经过 8 次往返,浏览器终于可以发出请求。

第二步:响应

一旦我们建立了到 web 服务器的连接,浏览器就代表用户发送一个初始的 HTTP GET 请求,对于网站来说,这个请求通常是一个 HTML 文件。一旦服务器收到请求,它将使用相关的响应头和 HTML 的内容进行回复。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!doctype HTML>
<html>
<head>
<meta charset="UTF-8"/>
<title>My simple page</title>
<link rel="stylesheet" src="styles.css"/>
<script src="myscript.js"></script>
</head>
<body>
<h1 class="heading">My Page</h1>
<p>A paragraph with a <a href="https://example.com/about">link</a></p>
<div>
<img src="myimage.jpg" alt="image description"/>
</div>
<script src="anotherscript.js"></script>
</body>
</html>

初始请求的响应包含所接收数据的第一个字节。Time to First Byte(TTFB)是用户通过点击链接进行请求与收到第一个 HTML 数据包之间的时间。第一个内容分块通常是 14KB 的数据。

上面的例子中,这个请求肯定是小于 14KB 的,但是直到浏览器在解析阶段遇到链接时才会去请求链接的资源,下面有进行描述。

TCP慢启动/14kB规则

第一个响应数据包是 14KB 大小的。这是慢启动的一部分,慢启动是一种均衡网络连接速度的算法。慢启动逐渐增加发送数据的数量直到达到网络的最大带宽。

在 TCP 慢启动 中,在收到初始包之后,服务器会将下一个数据包的大小加倍到大约 28KB。后续的数据包依次是前一个包大小的二倍直到达到预定的阈值,或者遇到拥塞。

如果您听说过初始页面加载的 14KB 规则,TCP 慢启动就是初始响应为 14KB 的原因,也是为什么 web 性能优化需要将此初始 14KB 响应作为优化重点的原因。TCP 慢启动逐渐建立适合网络能力的传输速度,以避免拥塞。

第三步:解析和渲染

一旦浏览器收到数据的第一块,它就可以开始解析收到的信息。“解析”是浏览器将通过网络接收的数据转换为 DOM 和 CSSOM 的步骤,通过渲染器把 DOM 和 CSSOM 在屏幕上绘制成页面。

DOM 是浏览器标记的内部表示。DOM 也是被暴露的,可以通过 JavaScript 中的各种 API 进行 DOM 操作。

即使请求页面的 HTML 大于初始的 14KB 数据包,浏览器也将开始解析并尝试根据其拥有的数据进行渲染。这就是为什么在前 14KB 中包含浏览器开始渲染页面所需的所有内容,或者至少包含页面模板(第一次渲染所需的 CSS 和 HTML)对于 web 性能优化来说是重要的。但是在渲染到屏幕上面之前,HTML、CSS、JavaScript 必须被解析完成。

关键渲染路径(CRP)

在解析 HTML 时会创建文档对象模型。HTML 可以请求 JavaScript,而 JavaScript 反过来,又可以更改 DOM。HTML 包含或请求样式,依次来构建 CSS 对象模型。浏览器引擎将两者结合起来以创建渲染树。布局确定页面上所有内容的大小和位置。确定布局后,将像素绘制到屏幕上。

优化关键渲染路径可以缩短首次渲染的时间。了解和优化关键渲染路径对于确保重排和重绘可以每秒 60 帧的速度进行,以确保高效的用户交互并避免讨厌是很重要的。

Web 性能包含了服务器请求和响应、加载、执行脚本、渲染、布局和绘制每个像素到屏幕上。

网页请求从 HTML 文件请求开始。服务器返回 HTML – 响应头和数据。然后浏览器开始解析 HTML,转换收到的数据为 DOM 树。浏览器每次发现外部资源就初始化请求,无论是样式、脚本或者嵌入的图片引用。有时请求会阻塞,这意味着解析剩下的 HTML 会被终止直到重要的资源被处理。浏览器接着解析 HTML,发请求和构造 DOM 直到文件结尾,这时开始构造 CSS 对象模型。等到 DOM 和 CSSOM 完成之后,浏览器构造渲染树,计算所有可见内容的样式。一旦渲染树完成布局开始,定义所有渲染树元素的位置和大小。完成之后,页面被渲染完成,或者说是绘制到屏幕上。

DOM(文档对象模型)

DOM 构建是增量的。HTML 响应变成令牌(token),令牌变成节点,而节点又变成 DOM 树。单个 DOM 节点以 startTag 令牌开始,以 endTag 令牌结束。节点包含有关 HTML 元素的所有相关信息。该信息是使用令牌描述的。节点根据令牌层次结构连接到 DOM 树中。如果另一组 startTag 和 endTag 令牌位于一组 startTag 和 endTag 之间,则您在节点内有一个节点,这就是我们定义 DOM 树层次结构的方式。

节点数量越多,关键渲染路径中的后续事件将花费的时间就越长。

CSSOM

DOM 包含页面所有的内容。CSSOM 包含了页面所有的样式,也就是如何展示 DOM 的信息。CSSOM 跟 DOM 很像,但是不同。DOM 构造是增量的,CSSOM 却不是。CSS 是渲染阻塞的:浏览器会阻塞页面渲染直到它接收和执行了所有的 CSS。CSS 是渲染阻塞是因为规则可以被覆盖,所以内容不能被渲染直到 CSSOM 的完成。

CSS 有其自身的规则集合用来定义标识。注意 CSS 中的 C 代表的是“层叠”。CSS 规则是级联的。随着解析器转换标识为节点,节点的后代继承了样式。像处理 HTML 那样的增量处理功能没有被应用到 CSS 上,因为后续规则可能被之前的所覆盖。CSS 对象模型随着 CSS 的解析而被构建,但是直到完成都不能被用来构建渲染树,因为样式将会被之后的解析所覆盖而不应该被渲染到屏幕上。

从选择器性能的角度,更少的特定选择器是比更多的要快。例如,.foo {} 是比 .bar .foo {} 更快的因为当浏览器发现 .foo ,接下来必须沿着 DOM 向上走来检查 .foo 是不是有一个祖先 .bar。越是具体的标签浏览器就需要更多的工作,但这样的弊端未必值得优化。

如果你测量过解析 CSS 的时间,你将会被浏览器实在地快所震惊。更具体的规则更昂贵因为它必须遍历更多的 DOM 树节点,但这所带来的额外的消耗通常很小。先测量一下。然后按需优化。特定化或许不是你的低垂的果实。在 CSS 中选择器的性能优化,提升仅仅是毫秒级的。有其他一些方式来优化 CSS,例如压缩和使用媒体查询来异步处理 CSS 为非阻塞的请求。

渲染树

渲染树包括了内容和样式:DOM 和 CSSOM 树结合为渲染树。为了构造渲染树,浏览器检查每个节点,从 DOM 树的根节点开始,并且决定哪些 CSS 规则被添加。

渲染树只包含了可见内容。头部(通常)不包含任何可见信息,因此不会被包含在渲染树种。如果有元素上有 display: none;,它本身和其后代都不会出现在渲染树中。

布局

一旦渲染树被构建,布局变成了可能。布局取决于屏幕的尺寸。布局这个步骤决定了在哪里和如何在页面上放置元素,决定了每个元素的宽和高,以及他们之间的相关性。

什么是一个元素的宽?块级元素,根据定义,默认有父级宽度的 100%。一个宽度 50% 的元素,将占据父级宽度的一半。除非另外定义,body 有 100% 的宽,意味着它占据视窗的 100%。设备的宽度影响布局。

视窗的元标签定义了布局视窗的宽度,从而影响布局。没有的话,浏览器使用视窗的默认宽度,默认全屏浏览器通常是 960px。在默认情况下像你的手机浏览器的全屏浏览器,通过设置

,宽度将会是设备的宽度而不是默认的视窗宽度。设备宽度当用户在横向和纵向模式旋转他们的手机时将会改变。布局发生在每次设备旋转或浏览器缩放时。

布局性能受 DOM 影响 – 节点数越多,布局就需要更长的时间。布局将会变成瓶颈,如果期间需要滚动或者其他动画将会导致迟滞。20ms 的延迟在加载或者方向改变时或许还可以接受,但在动画或滚动时就会迟滞。任何渲染树改变的时候,像添加节点、改变内容或者在一个节点更新盒模型样式的时候布局就会发生。

为了减小布局事件的频率和时长,批量更新或者避免改动盒模型属性。

绘制

最后一步是将像素绘制在屏幕上。一旦渲染树创建并且布局完成,像素就可以被绘制在屏幕上。加载时,整个屏幕被绘制出来。之后,只有受影响的屏幕区域会被重绘,浏览器被优化为只重绘需要绘制的最小区域。绘制时间取决于何种类型的更新被附加在渲染树上。绘制是一个非常快的过程,所以聚焦在提升性能时这大概不是最有效的部分,重点要记住的是当测量一个动画帧需要的时间需要考虑到布局和重绘时间。添加到节点的样式会增加渲染时间,但是移除样式增加的 0.001ms 或许不能让你的优化物有所值。记住先测量。然后你可决定它的优化优先级。

预加载扫描器

浏览器构建 DOM 树时,这个过程占用了主线程。当这种情况发生时,预加载扫描仪将解析可用的内容并请求高优先级资源,如 CSS、JavaScript 和 web 字体。多亏了预加载扫描器,我们不必等到解析器找到对外部资源的引用来请求它。它将在后台检索资源,以便在主 HTML 解析器到达请求的资源时,它们可能已经在运行,或者已经被下载。预加载扫描仪提供的优化减少了阻塞。

1
2
3
4
<link rel="stylesheet" src="styles.css"/>
<script src="myscript.js" async></script>
<img src="myimage.jpg" alt="image description"/>
<script src="anotherscript.js" async></script>

在这个例子中,当主线程在解析 HTML 和 CSS 时,预加载扫描器将找到脚本和图像,并开始下载它们。为了确保脚本不会阻塞进程,当 JavaScript 解析和执行顺序不重要时,可以添加 async 属性或 defer 属性。

等待获取 CSS 不会阻塞 HTML 的解析或者下载,但是它确实会阻塞 JavaScript,因为 JavaScript 经常用于查询元素的 CSS 属性

第四步:交互

一旦主线程绘制页面完成,你会认为我们已经“准备好了”,但事实并非如此。如果加载包含 JavaScript(并且延迟到 onload 事件激发后执行),则主线程可能很忙,无法用于滚动、触摸和其他交互。

Time to Interactive (en-US)(TTI)是测量从第一个请求导致 DNS 查询和 SSL 连接到页面可交互时所用的时间——可交互是 First Contentful Paint (en-US) 之后的时间点,页面在 50ms 内响应用户的交互。如果主线程正在解析、编译和执行 JavaScript,则它不可用,因此无法及时(小于 50ms)响应用户交互。

在我们的示例中,可能图像加载很快,但 anotherscript.js 文件可能是 2MB,而且用户的网络连接很慢。在这种情况下,用户可以非常快地看到页面,但是在下载、解析和执行脚本之前,就无法滚动。这不是一个好的用户体验。避免占用主线程

优化手段总结

导航阶段

  • 通过dns-prefetch来预解析域名对应的IP地址,减少导航时间
  • 尽量将不同的资源放到同一个服务器域名下,减少DNS解析的需求

响应阶段

  • 通过SSR来渲染首屏页面。
  • 首屏页面所需内容尽量小于14kb,让其能够在TCP的首包中就能一次性返回并加载

解析阶段

  • 使用预加载扫描器,资源提示(Resource Hints),如preload,prefetch等
  • 合理规划css和script标签的位置,因为css不会block html解析,但是会block js解析和执行,而js如果不是defer或者async,又会block html解析
  • 使用CDN来加速资源的加载
  • 优先加载关键的CSS,将首屏需要的CSS单独拆分出来

交互阶段

优化首屏时间

提升页面加载速度需要通过被加载资源的优先级、控制它们加载的顺序和减小这些资源的体积。

  • 通过异步重要资源的下载来减小请求数量
  • 优化必须的请求数量和每个请求的文件体积
  • 通过区分关键资源的优先级来优化被加载关键资源的顺序,来缩短关键路径长度。

编码优化

事实上数据访问速度有快慢之分,下面列出几个影响数据访问速度的因素:

  • 字面量与局部变量的访问速度最快,数组元素和对象成员相对较慢

  • 变量从局部作用域到全局作用域的搜索过程越长速度越慢

  • 对象嵌套的越深,读取速度就越慢

  • 对象在原型链中存在的位置越深,找到它的速度就越慢

  • 推荐的做法是缓存对象成员值。将对象成员值缓存到局部变量中会加快访问速度

应用在运行时,性能的瓶颈主要在于DOM操作的代价非常昂贵,下面列出一些关于DOM操作相关提升性能的建议:

  • 在JS中对DOM进行访问的代价非常高。请尽可能减少访问DOM的次数(建议缓存DOM属性和元素、把DOM集合的长度缓存到变量中并在迭代中使用。读变量比读DOM的速度要快很多。)

  • 重排与重绘的代价非常昂贵。如果操作需要进行多次重排与重绘,建议先让元素脱离文档流,处理完毕后再让元素回归文档流,这样浏览器只会进行两次重排与重绘(脱离时和回归时)。

  • 善于使用事件委托

下面列出一些流程控制相关的一些可以略微提升性能的细节,这些细节在大型开源项目中大量运用(例如Vue):

  • 避免使用for…in(它能枚举到原型,所以很慢)

  • 在JS中倒序循环会略微提升性能

  • 减少迭代的次数

  • 基于循环的迭代比基于函数的迭代快8倍

  • 用Map表代替大量的if-else和switch会提升性能

静态资源优化

  • 使用Brotli或Zopfli进行纯文本压缩

  • 尽可能通过srcset,sizes和元素使用响应式图片。还可以通过元素使用WebP格式的图像。

  • 使用Tree-shaking,code-splitting等进行产物的压缩和拆分

网络优化

  • 使用HTTP缓存

  • 使用HTTP2的多路复用来合并请求