浏览器:从网络请求到页面渲染,async/defer和prefetch/preload有什么作用?

1,025 阅读13分钟

综述

整个过程可以分成网络请求和浏览器渲染两个部分

  1. 输入url
  2. 检查资源缓存
  3. DNS解析
  4. 建立TCP连接
  5. 发送HTTP请求于接收响应
  6. 关闭TCP连接
  7. 渲染阶段

输入url

浏览器会根据输入内容判断是url还是搜索内容,如果是搜索内容,会使用默认搜索引擎去搜索,如果是url,会把不完整的url合成完整的url

完整的URL(统一资源定位符):协议+主机+端口+路径(+参数)(+锚点)

检查资源的缓存

这一步一定是在真正的请求之前,只有先检查缓存再请求,缓存的机制才会有效。

缓存的设置:
缓存由响应头的cache-control/expires/last-modified/e-tag字段设置,又分为强缓存和协商缓存
强缓存是设置在一个条件下资源可以直接使用,协商缓存是向服务器评估缓存缓存的有效性,有效才能使用。
强缓存:(由cache-control和expires设置)

  1. cache-control可能的值:
    (1)max-age:用来设置资源可以被缓存多长时间,单位为秒
    (2)s-maxage:和max-age是一样的,不过它只针对代理服务器缓存而言;
    (3)public:指示响应可浏览器+CDN缓存
    (4)private:只能被浏览器缓存
    (5)no-cache:缓存资源,但是每次都不命中强缓存,一定要向服务器发送请求评估缓存的有效性。
    (6)no-store:响应不被缓存。

    2. expires:过期时间,和cache-control的max-age相比相对不可靠,因为expires是一个绝对时间,可以通过修改本地时间欺骗;而max-age是接收到响应后多少时间内是有效的,不受本地时间影响

    协商缓存:(由last-modified/e-tag设置)
    当强缓存没有生效时,浏览器会检查资源的E-tag/last-modified,如果有E-tag,会带着If-None-Match和E-tag的值的请求头发起请求,没有则带着If-modified-since和last-modified的值作为请求头发起请求。让服务器决定本地缓存是否可以使用。
    如果服务器判断资源可以继续使用,返回304,使用缓存
    如果服务器判断资源不可使用,则返回200和新的资源,响应头的etag/last-modified也会更新,浏览器使用http响应,并且用本次响应替换上一次的缓存。
    E-tag和last-modified区别:
    E-tag是对资源和最新修改时间的哈希计算,而last-modified只是一个最后修改时间精度是秒,故E-tag更精确。
    使用last-modified可能出现毫秒时间内更新了资源,但时间没有变化,即可能造成资源不能及时更新。

DNS解析

DNS解析会依次搜索浏览器的DNS缓存-OS的DNS缓存;如果没有命中或者过期,才会向DNS服务器发起请求解析域名

DNS解析是属于应用层的,为了保证响应的及时,使用的是UDP协议

建立TCP连接

三次握手,详见:深入学习TCP协议

发送HTTP请求于接收响应

详见HTTP2.0做了哪些改变

关闭TCP连接

四次挥手,详见:深入学习TCP协议

浏览器渲染

渲染进程里的js现成和gui线程是互斥的,这两个线程使用的是非抢占式的调度,js线程做的事情是执行js代码,gui线程是解析dom和绘制。 解析dom,简单来说就是把dom节点上的信息对象化,将一个dom节点变成一个js对象,便于访问。绘制,即使用GPU将储存好的对象信息绘制在屏幕上。 gui线程有两件事情:1.解析dom,2绘制图像 js线程有一件事:执行js文件。 尝尝说要把js文件放在后面去引入,因为它会阻塞dom的渲染,使我们不能及时看到画面。 但是看下面这个例子:

第一步:构建 DOM 树+CSSOM(样式计算)

  1. 浏览器将 HTML 解析成树形结构的 DOM 树

  2. 把 CSS 转换为浏览器能够理解的结构

  • 通过 link 引用的外部 CSS 文件
  • <style>标签内的样式
  • 元素的 style 属性内嵌的 CSS

将上述三种 CSS 文本转换为浏览器可以理解的结构——styleSheets。

第二步:构建渲染树

CSSOM 树,再和 DOM 树合并成渲染树

第三步:布局阶段

那么接下来就需要计算出 DOM 树中可见元素的几何位置,我们把这个计算过程叫做布局。

布局阶段需要完成两个任务:创建布局树和布局计算。

1、创建布局树

你可能注意到了 DOM 树还含有很多不可见的元素,比如 head 标签,还有使用了 display:none 属性的元素。所以在显示之前,我们还要额外地构建一棵只包含可见元素布局树。 构建布局树,浏览器大体上完成了下面这些工作:

  • 遍历 DOM 树中的所有可见节点,并把这些节点加到布局树中;
  • 而不可见的节点会被布局树忽略掉,如 head 标签下面的全部内容,再比如 body.p.span 这个元素,因为它的属性包含 dispaly:none,所以这个元素也没有被包进布局树。

2、布局计算

现在我们有了一棵完整的布局树。那么接下来,就要计算布局树节点的坐标位置了。并将这些信息保存在布局树中。

  1. 输入:DOM & CSSOM 合并成渲染树;
  2. 处理:布局树(DOM 树中的可见元素);
  3. 输出:布局树。

第四步:分层

现在我们有了布局树,而且每个元素的具体位置信息都计算出来了,那么接下来是不是就要开始着手绘制页面了?答案依然是否定的。

因为页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-index 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。

浏览器的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面。

并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层

通常满足下面两点中任意一点的元素就可以被提升为单独的一个图层。

1、拥有层叠上下文属性的元素会被提升为单独的一层。明确定位属性的元素、定义透明属性的元素、使用 CSS 滤镜的元素等,都拥有层叠上下文属性。

2、需要剪裁(clip)的地方也会被创建为图层。(overflow)

第五步:图层绘制

在完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行绘制 当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程 绘制操作是由渲染引擎中的合成线程来完成的。

  1. 输入:layerTree;
  2. 处理:拆分成绘制指令,生成绘制列表,提交到合成线程;
  3. 输出:绘制列表。

第六步:栅格化(raster)操作

通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做视口(viewport)。

基于这个原因,合成线程会将图层划分为图块(tile),这些图块的大小通常是 256x256 或者 512x512,如下图所示

合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。

  1. 输入:绘制列表;
  2. 处理:合成线程会按照视口附近的图块来优先生成位图;
  3. 输出:位图。

第七步:合成和显示

一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。

浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。

  1. 输入:位图;
  2. 处理:合成线程发出命令——“DrawQuad”到主线程;
  3. 输出:屏幕上的最终展示。

image.png

性能优化

有了上面介绍渲染流水线的基础,我们再来看看三个和渲染流水线相关的概念——“「「重排」」”“「「重绘」」”和“「「合成」」”。理解了这三个概念对于你后续 Web 的性能优化会有很大帮助。

1. 更新了元素的几何属性(重排)

你可先参考下图:

从上图可以看出,如果你通过 JavaScript 或者 CSS 修改元素的几何位置属性,例如改变元素的宽度、高度等,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫**「重排」「。无疑,」「重排需要更新完整的渲染流水线,所以开销也是最大的」**。

2. 更新元素的绘制属性(重绘)

接下来,我们再来看看重绘,比如通过 JavaScript 更改某些元素的背景颜色,渲染流水线会怎样调整呢?你可以参考下图:

从图中可以看出,如果修改了元素的背景颜色,那么布局阶段将不会被执行,因为并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程就叫重绘。相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。

直接合成阶段

那如果你更改一个既不要布局也不要绘制的属性,会发生什么变化呢?渲染引擎将跳过布局和绘制,只执行后续的合成操作,我们把这个过程叫做合成。具体流程参考下图:

\

在上图中,我们使用了 CSS 的 transform 来实现动画效果,这可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作。这样的效率是最高的,因为是在非主线程上合成,并没有占用主线程的资源,另外也避开了布局和绘制两个子阶段,「「所以相对于重绘和重排,合成能大大提升绘制效率」」

  • 根据指定的编码格式(UTF-8)将获取的原始二进制资源转化成字符串,根据content-type判断资源的类型
  • token化:简单来说就是可以使得之后构建dom树时,在读到HTML开始标签处就构建dom,而不用等待读到闭合标签(否则script标签对dom树的构建总时长不会有影响)
  • 构建dom树:将HTML标签映射为节点Node,这是一个深度优先的遍历
  • 构建CSSOM树:在读取到HTML中的style标签时,会开始构建CSSOM树,CSSOM树和DOM树的构建是不互斥的,所以CSS文件通常放在head中,使CSSOM树会尽早构建完毕
  • 构建渲染树:把DOM树中的可见节点遍历出来,将CSSOM树上匹配的属性加上。CSS的匹配时从右到左的,要尽量避免过深的层级
  • 布局:根据渲染树,输出一个盒子模型。会计算出精确的位置和大小,再将相对测量值转化为屏幕的具体像素
  • 绘制:根据布局转化为屏幕上的像素

外链资源获取

在解析HTML的过程中,会遇到需要加载的其他资源,可以划分为JS/CSS/图片三种类型,特点如下:

  • CSS资源异步下载,下载和解析都不会阻塞构建dom树<link href='./style.css' rel='stylesheet'/>
  • JS资源同步下载,下载和执行都会阻塞构建dom树<script src='./index.js'/>
  • 图片资源异步下载,下载完成后替换原来的img标签的src,不会阻塞构建dom树

因为这样的特性,往往推荐将CSS样式表放在head头部,js文件放在body尾部,使得渲染能尽早开始

javascript阻塞渲染

浏览器内核的常驻线程有5个:

  1. GUI渲染线程
  2. JS引擎线程
  3. 定时触发器线程
  4. 事件触发器线程
  5. 浏览器HTTP请求线程

上面的dom树+cssom树+渲染树的构建,就是GUI渲染线程的任务。

GUI线程和JS引擎线程是互斥的,因为javascript可能操纵dom。如果过早引入js文件,会导致渲染树不能构建完成,阻塞渲染。

js文件异步引入:async和defer

  • 没有 defer 或 async,浏览器会立即加载并执行指定的脚本
  • async 属性表示异步执行引入的 JavaScript,经加载好,就会开始执行
  • defer 属性表示延迟执行引入的 JavaScript

在加载多个JS脚本的时候,async是无顺序的加载,而defer是有顺序的加载

资源提示与指令:预加载

preload: 以高优先级为当前页面加载资源。

preload 指令事实上克服了只能在HTML中使用的限制并且允许预加载在 CSS 和JavaScript 中定义的资源,并允许决定何时应用每个资源。

设定优先级: 使用 as=”style” 属性将获得最高的优先级,as =“script”将获得低优先级或中优先级,没有 “as” 属性的将被看作异步请求。 通常有JS文件,图片资源、字体等。

<link rel="preload" href="image.png"> // 预加载图片
<link rel="preload" href="https://example.com/fonts/font.woff" as="font" crossorigin> // 跨域预加载
<link rel='preload' as='style' href='./style.css' onload='this.rel=stylesheet'>

preload不仅可以通过HTML标签设置,还可以通过js设置

var res = document.createElement("link"); 
res.rel = "preload"; 
res.as = "style"; 
res.href = "css/mystyles.css"; 
document.head.appendChild(res); 

prefetch:浏览器在空闲时获取将来可能用得到的资源,并且将他们缓存。

  1. link prefetch:浏览器会寻找 HTML 元素中的 prefetch 或者 HTTP 头中如下的 Link:
HTML: <link rel="prefetch" href="/uploads/images/pic.png">
HTTP Header: Link: </uploads/images/pic.png>; rel=prefetch

被预加载的资源必须要支持被缓存
比如一个HTML文件的头部有mata标签<meta httpEquiv="cache-control" content="no-cache, no-store, must-revalidate" />表明这个html资源不允许被缓存在浏览器中

  1. DNS Prefetching:在后台运行 DNS 的解析。
 <link rel="dns-prefetch" href="//fonts.googleapis.com">
 <link rel="dns-prefetch" href="//www.google-analytics.com"> 
 <link rel="dns-prefetch" href="//opensource.keycdn.com">
 <link rel="dns-prefetch" href="//cdn.domain.com">
  1. Prerendering:获取下个页面所有的资源,在空闲时渲染了整个页面。
<link rel="prerender" href="https://www.keycdn.com">

Preconnect:在一个 HTTP 请求正式发给服务器前预先执行一些操作,这包括 DNS 解析,TLS 协商,TCP 握手

preconnect 可以直接添加到 HTML 中 link 标签的属性中,也可以写在 HTTP 头中或者通过

<link href="https://cdn.domain.com" rel="preconnect" crossorigin>

回流和重绘

回流是指当浏览器需要重新计算、布局和绘制,当元素的尺寸、结构或触发某些属性时会发生;

重绘是重新像素绘制,当元素样式的改变不影响布局时。

回流=计算+布局+绘制;重绘=绘制。故回流对性能的影响更大 触发回流的操作:

  • 添加或删除可见的DOM元素
  • 元素的位置发生变化
  • 元素的尺寸发生变化
  • 浏览器的窗口尺寸变化

浏览器工作原理:从输入URL到页面加载完成