学习笔记之浏览器 系列 3 -- 填充页面:浏览器的工作方式
Populating the page: how browsers work
用户想要的是加载速度快、交互流畅的网页体验。因此,开发人员应该努力实现这两个目标。
为了理解如何提高性能和感知性能,理解浏览器的工作原理是很有帮助的。
Overview
快速站点提供更好的用户体验。用户想要的是快速加载和流畅交互的网页体验。
web性能的两个主要问题是理解与延迟有关的问题,以及大多数情况下浏览器是单线程的这一事实。
延迟是我们要克服的主要威胁,以确保快速加载。为了快速加载,开发人员的目标包括尽可能快地发送所请求的信息,或者至少看起来超级快。网络延迟是指通过空中传输字节到计算机所花费的时间。Web性能(优化)是我们必须做的事情,以使页面加载尽可能快。
为什么是空中传输?估计是形象说法吧。
大多数情况下,浏览器被认为是单线程的。对于平滑的交互,开发者的目标是确保高性能的站点交互,从平滑的滚动到对触摸的响应。渲染时间是关键,确保主线程能够完成我们抛给它的所有工作,并且始终能够处理用户交互。在可能和适当的情况下,通过理解浏览器的单线程特性并尽量减少主线线程的职责,可以提高Web性能,以确保渲染是平滑的,对交互的响应是即时的。
Navigation
导航是加载网页的第一步。当用户通过在地址栏中输入URL、单击链接、提交表单以及其他操作来请求页面时,它就会发生。
网页性能的目标之一是尽量减少导航所需的时间。在理想情况下,这通常不会花太长时间,但latency
和带宽是敌人,可能会导致delay
。
Latency: The time between data entering a device and leaving it. For example, a switch may add 5ms of latency.
Delay: The one-way time it takes for traffic to leave the sender and arrive at the destination
learningnetwork.cisco.com/s/questi...
DNS Lookup
导航到网页的第一步是找到该页面的资产所在的位置。如果您导航到https://example.com
,则 HTML 页面位于 IP 地址为 93.184.216.34
的服务器上。如果您从未访问过此站点,则必须进行 DNS 查找。
您的浏览器请求DNS查找,最终由域名服务器处理,以IP地址响应。在这个初始请求之后,IP可能会被缓存一段时间,通过从缓存中检索IP地址而不是再次联系域名服务器,从而加快后续请求的速度。
对于页面加载,每个主机名通常只需要进行一次DNS查找。但是,必须对请求页面引用的每个唯一主机名进行DNS查找。如果字体、图像、脚本、广告和metrics
都有不同的主机名,就必须为每个主机名进行DNS查找。
metrics: (网站)指标(工具,比如Google Analytics)。
这可能会对性能造成问题,特别是在移动网络上。当用户在移动网络上时,每次DNS查找都必须从手机到手机发射塔,才能到达权威的DNS服务器。手机、手机发射塔和域名服务器之间的距离会增加显著的延迟。
TCP Handshake
一旦知道 IP 地址,浏览器就会通过TCP 三向握手建立与服务器的连接。这种机制旨在使两个尝试通信的实体(在这种情况下为浏览器和 Web 服务器)先协商网络 TCP 套接字连接的参数,再传输数据(通常是通过HTTPS完成的)。
TCP 的三向握手技术通常被称为“SYN-SYN-ACK”——或者更准确地说是 SYN、SYN-ACK、ACK——因为 TCP 传输了三个消息来协商和启动两台计算机之间的 TCP 会话。是的,这意味着在每个服务器之间又来回发送了三条消息,而且还没有发出请求。
TLS Negotiation
对于通过HTTPS建立的安全连接,需要进行另一次“握手”。这种握手,或者更确切地说,TLS协商,确定将使用哪个密码来加密通信,验证服务器,并在开始实际的数据传输之前建立安全连接。在实际发送内容请求之前,还需要三次到服务器的往返。
这里的三次应该就是ClientKey发送往返3次。TLS握手是5次。
虽然安全连接会增加页面加载的时间,但安全连接的延迟开销是值得的,因为在浏览器和web服务器之间传输的数据不能被第三方解密。
在8次往返之后,浏览器终于能够发出请求了。
3+5=8
Response
一旦我们建立了与web服务器的连接,浏览器就会代表用户发送一个初始的HTTP GET请求,对于网站来说,这通常是一个HTML文件。一旦服务器接收到请求,它将用相关的响应头和HTML的内容进行应答。
<!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>
这个初始请求的响应包含了接收到的数据的第一个字节。到第一个字节的时间(TTFB)是用户发出请求(例如通过单击链接)到收到第一个HTML数据包之间的时间。第一块内容通常是14kb的数据。
在上面的示例中,请求肯定小于14Kb,但直到浏览器在解析过程中遇到链接(如下所述)才会请求链接资源。
TCP Slow Start / 14kb rule
第一个响应包将是14Kb。这是TCP 慢启动的一部分,慢启动是一种平衡网络连接速度的算法。慢启动逐渐增加传输的数据量,直到可以确定网络的最大带宽。
在TCP慢启动中,在接收到初始数据包后,服务器会将下一个数据包的大小增加一倍,大约为28Kb。随后的数据包会增大,直到达到预定的阈值,或者出现拥塞。
如果你听说过14Kb的初始页面加载规则,那么TCP启动慢就是为什么初始响应是14Kb的原因,为什么web性能优化需要集中优化这个初始的14Kb响应。TCP慢启动逐渐建立起适合网络能力的传输速度,以避免拥塞。
Congestion control
当服务器以TCP包的形式发送数据时,用户的客户端通过返回回执(ACKs)来确认发送。由于硬件和网络条件的限制,连接的容量有限。如果服务器发送的数据包太多太快,它们将被丢弃。也就是说,不会有任何回执。服务器将其注册为丢失的ACK。拥塞控制算法使用已发送报文和ACKs的flow
来确定发送速率。
flow: 流还是流量?
Parsing
一旦浏览器收到第一块数据,就能开始解析收到的信息。解析是浏览器采用的将从网络收到的数据转换为DOM和CSSOM的步骤,它被渲染器用来绘制页面到屏幕上。
DOM是浏览器markup
的内部表示。DOM也被暴露并能被 JavaScript 中的各种 API 操作。
markup: a set of tags assigned to elements of a text to indicate their relation to the rest of the text or dictate how they should be displayed.
即使请求页面的HTML大于初始14KB包,浏览器也会开始解析并尝试根据已有的数据进行渲染体验。这就是为什么对web性能优化来说,包括浏览器渲染页面所需的所有内容,或者至少是页面模板(第一次渲染所需CSS和HTML)在最初的14KB中是很重要的。但在将任何内容渲染到屏幕之前,必须解析HTML、CSS和JavaScript。
Building the DOM tree
我们描述了关键渲染路径中的五个步骤
第一步是处理HTML标记并构建DOM树。HTML解析涉及tokenization和树的构造。HTMLtokens
包括开始和结束tags
,以及attribute
名称和值。如果文档是格式良好的,那么解析它就非常简单和快速。解析器将tokenized
的输入解析到文档中,并构建文档树。
DOM树描述文档的内容。<html>
元素是文档树的第一个tag
和根节点。树反映了不同tags
之间的关系和层次结构。嵌套在其他tags
中的tags
是子节点。DOM节点越多,构建DOM树所需的时间就越长
当解析器发现非阻塞资源(如图像)时,浏览器将请求这些资源并继续解析。当遇到CSS文件时,可以继续解析,但 <script>
标签——特别是那些没有async
或defer
属性的——会阻塞渲染,并暂停HTML的解析。尽管浏览器的预加载扫描器加快了这个过程,但是过多的脚本仍然是一个重要的瓶颈。
Preload scanner
当浏览器构建DOM树时,这个过程占用主线程。当这种情况发生时,预加载扫描器将解析可用的内容,并请求高优先级的资源,如CSS、JavaScript和web字体。多亏了预加载扫描程序,我们不必等到解析器找到对一个外部资源的引用才请求它。它将在后台检索资源,这样当主HTML解析器到达请求的资源时,这些资源可能已经在运行中,或者已经被下载了。预加载扫描器提供的优化减少了阻塞。
<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时,预加载扫描器将找到脚本和图像,并开始下载它们。要确保脚本不会阻塞进程,请添加async
属性,如果JavaScript解析和执行顺序很重要,则添加defer
属性。
defer就会在解析完HTML和CSS后再来解析JavaScript。
等待CSS获得(这个过程)不会阻塞HTML解析或下载,但它会阻塞JavaScript,因为JavaScript经常被用来查询CSS属性对元素的影响。
Building the CSSOM
关键渲染路径的第二步是处理CSS并构建CSSOM树。CSS对象模型类似于DOM。DOM和CSSOM都是树。它们是独立的数据结构。浏览器将CSS规则转换成它可以理解和使用的样式图。浏览器遍历CSS中的每个规则集,根据CSS选择器创建具有父、子和兄弟关系的节点树。
与HTML一样,浏览器需要将接收到的CSS规则转换成可以使用的规则。因此,它重复HTML-to-object
的过程,但针对的是CSS。
CSSOM树包括来自用户代理样式表的样式。浏览器从适用于节点的最普遍的规则开始,通过应用更具体的规则递归地细化计算的样式。换句话说,它将property values
级联。
构建CSSOM非常非常快,而且在当前的开发工具中并没有以独特的颜色显示。相反,开发人员工具中的“Recalculate Style”显示了解析CSS、构造CSSOM树和递归计算样式所需的总时间。在web性能优化方面,有更容易实现的目标,因为创建CSSOM的总时间通常少于一次DNS查询所需的时间。
意思就收效甚微,不要在这上面浪费时间了。
Other Processes
JavaScript Compilation
在解析CSS和创建CSSOM的同时,还下载了包括JavaScript文件在内的其他资产(这要归功于预加载扫描器)。JavaScript被解释、编译、解析和执行。脚本被解析成抽象语法树。一些浏览器引擎将抽象语法树传递给解释器,输出在主线程上执行的字节码。这就是所谓的JavaScript编译。
Building the Accessibility Tree
浏览器还构建了一个辅助设备用来解析和解释内容的可访问性树。可访问性对象模型(AOM)类似于DOM的语义版本。当DOM更新时,浏览器会更新可访问性树。辅助技术本身不能修改可访问性树。
在构建AOM之前,屏幕阅读器不能访问内容。
Render
渲染步骤包括样式、布局、绘制,在某些情况下,还包括合成。解析步骤中创建的CSSOM和DOM树被组合成一个渲染树,然后用于计算每个可见元素的布局,然后将其绘制到屏幕上。在某些情况下,内容可以提升到它们自己的层并组合,通过在GPU而不是CPU上绘制部分的屏幕来提高性能(释放主线程)。
Style
关键渲染路径的第三步是将DOM和CSSOM组合成一个渲染树。计算样式树或渲染树的构造从DOM树的根开始,遍历每个可见节点。
不被显示的标签,如<head>
及其子节点和任何带有display: none
的节点(如script {display: none;}
)不包含在渲染树中,因为它们不会出现在渲染的输出中。应用了visibility: hidden
的节点包含在渲染树中,因为它们确实占用空间。因为我们没有给出任何指令来覆盖用户代理默认值,所以上面代码示例中的script
节点将不会包含在渲染树中。
每个可见节点都有自己的CSSOM规则。渲染树包含所有具有内容和计算样式的可见节点——将所有相关样式匹配到DOM树中的每个可见节点,并根据CSS级联确定每个节点的计算样式。
Layout
关键渲染路径的第四步是在渲染树上运行布局来计算每个节点的几何形状。布局是确定渲染树中所有节点的宽度、高度和位置,以及确定页面中每个对象的大小和位置的过程。Reflow是页面任何部分或整个文档的任何后续大小和位置的确定。
一旦渲染树建立,布局就开始了。渲染树标识了显示哪些节点(即使是不可见的)以及它们的计算样式,但没有标识每个节点的尺寸或位置。为了确定每个对象的确切大小和位置,浏览器从渲染树的根开始并遍历它。
在网页上,几乎所有的东西都是一个盒子。不同的设备和不同的桌面首选项意味着无限数量的不同视口大小。在这个阶段,将视口大小考虑在内,浏览器决定屏幕上所有不同框的尺寸。以视口大小为基础,布局通常从body
开始,通过每个元素的盒模型属性,为它不知道尺寸的替换元素提供占位符空间,比如我们的图像。
第一次确定节点的大小和位置称为布局。随后对节点大小和位置的重新计算称为回流。在我们的示例中,假设初始布局发生在返回图像之前。因为我们没有声明图像的大小,所以一旦知道图像的大小,就会有一个回流。
Paint
关键呈现路径中的最后一步是将各个节点绘制到屏幕上,第一次出现的节点称为first meaningful paint。在绘制或光栅化阶段,浏览器将在布局阶段计算出的每个框转换为屏幕上的实际像素。绘图包括将元素的每个可视部分绘制到屏幕上,包括文本、颜色、边框、阴影和替换元素,如按钮和图像。浏览器需要非常快地完成这项工作。
为了确保滚动和动画流畅,所有占用主线程的工作,包括计算样式,以及reflow 和 paint,浏览器都必须在16.67毫秒内完成。在 2048 X 1536 的分辨率, iPad 有超过 3,145,000 像素需要绘制到屏幕上。这需要非常快地绘制大量的像素。为了确保重绘比最初的绘制速度更快,绘制到屏幕上的图形通常分为几层。如果出现这种情况,那么就需要合成。
绘制(过程)可以将布局树中的元素分解成图层。将内容提升到GPU上的层(而不是CPU上的主线程)可以提高绘制和重绘性能。有一些特定的属性和元素可以实例化一个层,包括<video>
和 <canvas>
,有opacity
,3D transform
,will-change
等CSS属性的任何元素。这些节点还有它们的后代将被绘制到它们自己的层上,除非后代层因为上面的一个(或多个)原因而需要它自己的层。
层确实可以提高性能,但在内存管理方面代价昂贵,所以不应该过度使用来作为web性能优化策略的一部分。
Compositing
当文档的各个部分被绘制在不同的层中,相互重叠时,必须进行合成,以确保它们以正确的顺序绘制到屏幕上,并正确渲染内容。
随着页面继续加载资源,可能会发生reflow
(回想一下我们稍后到达的示例图像)。reflow
激发repaint
和re-composite
。如果我们定义了图像的大小,就不需要reflow
了,只需要重新绘制需要重新绘制的图层,并在必要时进行合成。但我们没有包含图像大小!当从服务器获得图像时,渲染过程返回到布局步骤并从那里重新启动。
Interactivity
一旦主线程完成了对页面的绘制,您可能会认为我们就“一切就绪”了。但事实未必如此。如果加载包括JavaScript(它被正确地延迟了),并且只在onload
事件触发后执行,主线线程可能会很忙,不能用于滚动、触摸和其他交互。
Time to Interactive (TTI) 从导致DNS查找和SSL连接的第一个请求到页面可以互动的时间测量(可以互动是指从First Contentful Paint的那个时间点开始,页面能在50ms内响应用户交互)。如果主线程被用来解析、编译和执行JavaScript,那么它就是不可用的,因此无法及时(少于50ms)响应用户交互。
在我们的示例中,可能图像加载得很快,但可能anotherscript.js
文件是2MB,用户的网络连接很慢。在这种情况下,用户会非常快地看到页面,但无法在不卡顿的情况下滚动,直到脚本被下载、解析和执行。这不是一个好的用户体验。避免占用主线程,如这个 WebPageTest 示例所示:
在这个例子中,DOM内容加载过程花费了1.5秒,主线程在这段时间内完全被占用,对点击事件或屏幕点击没有响应。
See also
本作品采用《CC 协议》,转载必须注明作者和本文链接