关于浏览器的一些原理

浏览器的内核

一个完整的浏览器内核主要分成两部分:渲染引擎(layout engineer或Rendering Engine)和JS引擎

渲染引擎:负责取得网页的内容(HTML、XML、图像等等)、整理讯息(例如加入CSS等),以及计算网页的显示方式,然后会输出至显示器或打印机。浏览器的内核的不同对于网页的语法解释会有不同,所以渲染的效果也不相同。所有网页浏览器、电子邮件客户端以及其它需要编辑、显示网络内容的应用程序都需要内核

JS引擎则:解析和执行javascript来实现网页的动态效果

最开始渲染引擎和JS引擎并没有区分的很明确,后来JS引擎越来越独立,内核就倾向于只指渲染引擎, 到如今。我们所指的浏览器内核,一般就是指渲染引擎(下文所指的内核,若无特殊提示,则代表渲染引擎)

渲染引擎

目前使用的主流浏览器有五个:Internet Explorer、 Firefox、 Safari、 Chrome 和 Opera,他们使用的渲染引擎分别如下:

  • Trident引擎:Internet Explorer
  • Webkit引擎:Chrome(28版本后基于blink,blink是webkit的一个分支)和Safari
  • Gecko 英[ˈɡekəʊ]引擎:Firefox
  • Presto引擎:早期Opera采用,后用webkit引擎

浏览器内核渲染机制

  • 处理 HTML 并构建 DOM 树。
  • 处理 CSS 构建 CSSOM 树。
  • 将 DOM 与 CSSOM 合并成一个渲染树。
  • Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)
  • Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
  • Display:将像素发送给GPU,展示在页面上。(这一步其实还有很多内容,比如会在GPU将多个合成层合并为同一个层,并展示在页面中。而css3硬件加速的原理则是新建合成层

构建DOM树

  • 当我们打开一个网页时,浏览器都会去请求对应的 HTML 文件。虽然平时我们写代码时都会分为 JS、CSS、HTML 文件,也就是字符串,但是计算机硬件是不理解这些字符串的,所以在网络中传输的内容其实都是 0 和 1 这些字节数据。当浏览器接收到这些字节数据以后,它会将这些字节数据转换为字符串,也就是我们写的代码。
  • 当数据转换为字符串以后,浏览器会先将这些字符串通过词法分析转换为标记(token),这一过程在词法分析中叫做标记化(tokenization)
  • 简单来说,标记还是字符串,是构成代码的最小单位。这一过程会将代码分拆成一块块,并给这些内容打上标记,便于理解这些最小单位的代码是什么意思
  • 当结束标记化后,这些标记会紧接着转换为 Node,最后这些 Node 会根据不同 Node 之前的联系构建为一颗 DOM 树

构建CSSOM树

这一过程大体流程和上面的类似

在这一过程中,浏览器会确定下每一个节点的样式到底是什么,并且这一过程其实是很消耗资源的。因为样式你可以自行设置给某个节点,也可以通过继承获得。在这一过程中,浏览器得递归 CSSOM 树,然后确定具体的元素到底是什么样式

1
2
3
4
5
6
7
8
9
10
11
<div>
<a> <span></span> </a>
</div>
<style>
span {
color: red;
}
div > a > span {
color: red;
}
</style>

对于第一种设置样式的方式来说,浏览器只需要找到页面中所有的 span 标签然后设置颜色,但是对于第二种设置样式的方式来说,浏览器首先需要找到所有的 span 标签,然后找到 span 标签上的 a 标签,最后再去找到 div 标签,然后给符合这种条件的 span 标签设置颜色,这样的递归过程就很复杂。

在构建 CSSOM 树时,会阻塞渲染,直至 CSSOM 树构建完成,所以我们应该尽可能的避免写过于具体的 CSS 选择器,然后对于 HTML 来说也尽量少的添加无意义标签,保证层级扁平

构建渲染树

  • 从DOM树的根节点开始遍历每个可见节点,例如css的display:none就是个不可见节点,不会进入渲染树。
  • 对于每个可见的节点,找到CSSOM树中对应的规则,并应用它们。
  • 根据每个可见节点以及其对应的样式,组合生成渲染树。

第一步中,既然说到了要遍历可见的节点,那么我们得先知道,什么节点是不可见的。不可见的节点包括:

  • 一些不会渲染输出的节点,比如script、meta、link等
  • 一些通过css进行隐藏的节点。比如display:none。注意,利用visibility和opacity隐藏的节点,还是会显示在渲染树上的。只有display:none的节点才不会显示在渲染树上

总结:渲染树只包含可见的节点

回流与重绘

回流

前面我们通过构造渲染树,我们将可见DOM节点以及它对应的样式结合起来,可是我们还需要计算它们在设备视口(viewport)内的确切位置和大小,这个计算的阶段就是回流。

重绘

最终,我们通过构造渲染树和回流阶段,我们知道了哪些节点是可见的,以及可见节点的样式和具体的几何信息(位置、大小),那么我们就可以将渲染树的每个节点都转换为屏幕上的实际像素,这个阶段就叫做重绘节点。

回流与重绘的触发机制

我们前面知道了,回流这一阶段主要是计算节点的位置和几何信息,那么当页面布局和几何信息发生变化的时候,就需要回流。比如以下情况:

  • 添加或删除可见的DOM元素
  • 元素的位置发生变化
  • 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
  • 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。
  • 页面一开始渲染的时候(这肯定避免不了)
  • 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)

注意:回流一定会触发重绘,而重绘不一定会回流

根据改变的范围和程度,渲染树中或大或小的部分需要重新计算,有些改变会触发整个页面的重排,比如,滚动条出现的时候或者修改了根节点。

现代的浏览器都是很聪明的,由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重排过程。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列。但是!当你获取布局信息的操作的时候,会强制队列刷新,比如当你访问以下属性或者使用以下方法:

offsetTop、offsetLeft、offsetWidth、offsetHeight
scrollTop、scrollLeft、scrollWidth、scrollHeight
clientTop、clientLeft、clientWidth、clientHeight
getComputedStyle()
getBoundingClientRect

以上属性和方法都需要返回最新的布局信息,因此浏览器不得不清空队列,触发回流重绘来返回正确的值。因此,我们在修改样式的时候,最好避免使用上面列出的属性,他们都会刷新渲染队列。如果要使用它们,最好将值缓存起来。

减少回流与重绘

使用cssText或者className一次性改变属性

1
2
3
4
const el = document.getElementById('test');
el.style.padding = '5px';
el.style.borderLeft = '1px';
el.style.borderRight = '2px';

替换为

1
2
const el = document.getElementById('test');
el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;';

批量操作时先使元素脱离文档流再修改

当我们进行DOM操作,特别是批量操作时,可以使用如下步骤:

  • 使元素脱离文档流
  • 对其进行多次修改
  • 将元素带回到文档中。

对于批量操作,避免在循环内部一次又一次地进行dom操作,尽量将多次dom操作合并为一次

1
2
3
4
5
6
7
8
9
10
11
function appendDataToElement(appendToElement, data) {
let li;
for (let i = 0; i < data.length; i++) {
li = document.createElement('li');
li.textContent = 'text';
appendToElement.appendChild(li);
}
}

const ul = document.getElementById('list');
appendDataToElement(ul, data);

替换为:

1
2
3
4
5
6
7
8
9
10
11
12
function appendDataToElement(appendToElement, data) {
let li;
for (let i = 0; i < data.length; i++) {
li = document.createElement('li');
li.textContent = 'text';
appendToElement.appendChild(li);
}
}
const ul = document.getElementById('list');
ul.style.display = 'none';
appendDataToElement(ul, data);
ul.style.display = 'block';

然而对于上述那种情况,实验结果不是很理想。

原因:原因其实上面也说过了,浏览器会使用队列来储存多次修改,进行优化,所以对这个优化方案,我们其实不用优先考虑。

对于复杂动画效果,使用绝对定位让其脱离文档流

对于复杂动画效果,由于会经常的引起回流重绘,因此,我们可以使用绝对定位,让它脱离文档流。否则会引起父元素以及后续元素频繁的回流

使用 visibility 替换display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)

css3硬件加速

比起考虑如何减少回流重绘,我们更期望的是,根本不要回流重绘。这个时候,css3硬件加速就闪亮登场啦!!

划重点:使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘 。但是对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。

常见的触发硬件加速的css属性:

  • transform
  • opacity
  • filters
  • Will-change

缺点:占用较大,会有性能问题

尽量避免使用table布局

一个很小的改动都有可能造成table布局的回流

将频繁重绘或者回流的节点设置为图层

图层能够阻止该节点的渲染行为影响别的节点。比如对于 video 标签来说,浏览器会自动将该节点变为图层

一般来说,可以把普通文档流看成一个图层。特定的属性可以生成一个新的图层。不同的图层渲染互不影响,所以对于某些频繁需要渲染的建议单独生成一个新图层,提高性能。但也不能生成过多的图层,会引起反作用

通过以下几个常用属性可以生成新图层

  • 3D变换:translate3d、translateZ
  • will-change
  • video、iframe 标签
  • 通过动画实现的 opacity 动画转换
  • position: fixed

回流重绘与event loop

  • 当 Eventloop 执行完 Microtasks 后,会判断 document 是否需要更新,因为浏览器是 60Hz 的刷新率,每 16.6ms 才会更新一次。
  • 然后判断是否有 resize 或者 scroll 事件,有的话会去触发事件,所以 resize 和 scroll 事件也是至少 16ms 才会触发一次自带节流功能
  • 判断是否触发了 media query
  • 更新动画并且发送事件
  • 判断是否有全屏操作事件
  • 执行 requestAnimationFrame回调
  • 执行 IntersectionObserver 回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好 更新界面
  • 以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行 requestIdleCallback回调

什么情况阻塞渲染

  • 首先渲染的前提是生成渲染树,所以 HTML 和 CSS 肯定会阻塞渲染,换句话说,构建DOM树和CSS树都会阻塞渲染。如果你想渲染的越快,你越应该降低一开始需要渲染的文件大小,并且扁平层级,优化选择器。
  • 然后当浏览器在解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS文件,这也是都建议将 script 标签放在 body 标签底部的原因。
  • 当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性
  • 当 script 标签加上 defer 属性以后,表示该 JS 文件会并行下载,但是会放到 HTML 解析完成后顺序执行,所以对于这种情况你可以把 script标签放在任意位置。
  • 对于没有任何依赖的 JS 文件可以加上 async 属性,表示 JS 文件下载和解析不会阻塞渲染

浏览器从输入url到显示发生了什么

  1. 在浏览器地址栏输入URL
  2. 浏览器查看缓存,如果请求资源在缓存中并且未过期,跳转到转码步骤
  3. 浏览器对于缓存的读取是有优先级的(关于缓存,另一篇博客单独讲述)
  • Service Worker
  • Memory Cache(内存中的缓存)
  • Disk Cache(硬盘中的缓存)
  • 网络请求
  1. 如果资源未缓存(没有Memory Cache),发起新请求
  2. 检测是否有缓存(Disk Cache),如果有,检验是否未过期,足够新鲜直接提供给客户端,否则与服务器进行验证。
  3. 这里的检验缓存是否过期通常有两个HTTP头进行控制Expires和Cache-Control:
  4. HTTP1.0提供Expires,值为一个绝对时间表示缓存到期时间
  5. HTTP1.1增加了Cache-Control: max-age=,值为以秒为单位的有效时间
  6. 浏览器解析URL获取协议,主机,端口,path
  7. 浏览器组装一个HTTP(GET)请求报文
  8. 浏览器获取主机ip地址,过程如下:
  • 浏览器缓存
  • 本机缓存
  • hosts文件
  • 路由器缓存
  • 运营商 DNS缓存
  • DNS递归查询(可能存在负载均衡导致每次IP不一样)
  1. 打开一个socket与目标IP地址,端口建立TCP链接,三次握手如下:
  • 客户端发送一个TCP的SYN=1,Seq=X的包到服务器端口
  • 服务器发回SYN=1, ACK=X+1, Seq=Y的响应包
  • 客户端发送ACK=Y+1, Seq=Z
    (TCP三次握手四次挥手,会在另一篇博客单独讲述)
  1. TCP链接建立后发送HTTP请求
  2. 服务器接受请求并解析,将请求转发到服务程序,如虚拟主机使用HTTP Host头部判断请求的服务程序
    服务器检查HTTP请求头是否包含缓存验证信息如果验证缓存新鲜,返回304等对应状态码
  3. 处理程序读取完整请求并准备HTTP响应,可能需要查询数据库等操作
  4. 服务器将响应报文通过TCP连接发送回浏览器
  5. 浏览器接收HTTP响应,然后根据情况选择关闭TCP连接或者保留重用,关闭TCP连接的四次握手如下:
  • 主动方发送Fin=1, Ack=Z, Seq= X报文
  • 被动方发送ACK=X+1, Seq=Z报文
  • 被动方发送Fin=1, ACK=X, Seq=Y报文
  • 主动方发送ACK=Y, Seq=X报文
  1. 浏览器检查响应状态吗:是否为1XX,3XX, 4XX, 5XX,这些情况处理与2XX不同
  2. 如果资源可缓存,进行缓存
  3. 对响应进行解码(例如gzip压缩)
  4. 根据资源类型决定如何处理(假设资源为HTML文档)
  5. 解析HTML文档,构件DOM树,下载资源,构造CSSOM树,执行js脚本,这些操作没有严格的先后顺序,以下分别解释

构建DOM树:通过词法分析进行标记化,再将标记变成Node节点,由Node节点组成DOM树
construction:根据HTML标记关系将对象组成DOM树 解析过程中遇到图片、样式表、js文件,启动下载

构建CSSOM树: Tokenizing:字符流转换为标记流 Node:根据标记创建节点 CSSOM:节点创建CSSOM树

根据DOM树和CSSOM树构建渲染树:

回流:将可见DOM节点以及它对应的样式结合起来,计算它们在设备视口(viewport)内的确切位置和大小

重绘: 知道了哪些节点是可见的,以及可见节点的样式和具体的几何信息(位置、大小),将渲染树的每个节点都转换为屏幕上的实际像素。

复合图层的合成、GPU绘制、外链资源的处理

  1. JS引擎解析过程(JS的解释阶段,预处理阶段,执行阶段生成执行上下文,作用域链、回收机制等等
  2. 显示页面(HTML解析过程中会逐步显示页面)
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.

扫一扫,分享到微信

微信分享二维码
  • Copyrights © 2015-2021 AURORA_ZXH
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信