浏览器知识一:浏览器请求和渲染相关知识点

355 阅读18分钟

前言

本文总结了从输入一个URL地址到浏览器完成渲染的整个过程,并将关联的知识点进行详细描述。

全文文字描述,可能会比较繁琐,请耐心观看。

第一次总结知识点,如果有写的不对的地方,请批评改正。

1、从输入一个URL地址到浏览器完成渲染的整个过程

总体流程

1、输入地址
2、查找域名对应的IP地址
3、建立TCP连接
4、发送HTTP请求
5、服务器返回HTTP响应
6、浏览器解析渲染页面
7、断开连接

详细流程

可以分为两个阶段:导航阶段渲染阶段

一、导航阶段

这一阶段又分为4个步骤:
1、用户输入
2、URL请求
3、准备渲染进程
4、提交文档

1. 用户输入

浏览器会判断地址栏输入的关键字类别,是搜索内容还是请求的URL。

  • 如果是搜索内容,地址栏会使用默认的搜索引擎,来合成新的带搜索关键字的URL。
  • 如果输入的内容符合URL规则,那么地址栏会根据规则,把内容加上协议等,合成完整的URL。
  • 同时,标签页上的图标进入加载状态。浏览器进程会将信息传递给网络进程。

2. URL请求:

这一步就比较重点,涉及到许多网络知识。

  • 首先网络进程会根据用户的输入查找本地缓存是否有该资源。如果有缓存直接返回给浏览器进程,进入渲染阶段。如果没有就进入下一步。(涉及知识点:浏览器的缓存策略)
  • 接着就是进行DNS解析,所谓的DNS解析,其目的就是获取请求域名对应的服务器IP地址。(涉及知识点:DNS过程详解)
  • 当获取到目标的IP地址后,就可以和服务器进行TCP连接,就是著名的三次握手。(涉及知识点:TCP连接)
  • 建立连接后,浏览器就可以发送请求,这个时候浏览器端会构建请求行、请求头等消息,并把和该域名相关的Cookie等数据附加到请求头中,然后向服务器发送构建的请求信息。(涉及知识点:HTTP请求)
  • 等服务器接收到请求信息后,会根据信息生成响应数据,并返回。
  • 网络进程接受到服务器响应内容后开始进行解析。(涉及知识点:HTTP响应)

3、准备渲染进程:

准备渲染进程时会分成两种情况

  • 通常情况下,打开新的页面会使用单独的渲染进程。
  • 如果从A页面打开B页面,并且A和B都属于同一站点的话,那么B页面会复用A页面的渲染进程。
  • 值得注意的是,此时数据还在网络进程中,并没有提交给渲染进程,所以下一步就进入了提交文档步骤。

4、提交文档:

  • 提交文档的消息是由浏览器进程发出的,渲染进程收到“提交文档”的消息后,会和网络进程建立传输数据的“管道”。
  • 等文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程。
  • 浏览器进程收到“确认提交”的消息后,会更新浏览器界面状态,包括:安全状态、地址栏的URL、前进后退的历史状态、并更新Web页面。

以上导航阶段的步骤完成后,就进入渲染阶段

渲染阶段分为7个步骤:
1、构建DOM树
2、构建CSSOM树
3、加载或执行JS脚本
4、构建渲染树renderTree
5、分层 + 图层绘制
6、分块 + 栅格化
7、合成和显示

1、构建DOM树

构建DOM的原因是因为浏览器无法直接理解和使用HTML,所以需要将HTML转换成浏览器能够理解的结构--DOM树。

  • 这个步骤输入的内容是HTML文件,由HTML解析器解析后,生成树状结构的DOM节点。

2、构建CSSOM树

主要计算的是每个DOM节点中元素的具体样式。

  • 输入内容是css文件,可以是link引入的外部CSS文件,标记内的CSS属性,以及元素style属性内嵌的CSS属性。
  • 解析的过程也是将CSS文件转化成浏览器能够理解的styleSheets,对样式表中的属性值进行转换,使其标准化,然后再计算出DOM树中的每个节点的具体样式,这时会考虑到CSS的继承规则和层叠规则。
  • 最后生成类似于DOM树的CSSOM树。

3、加载或执行JS脚本

加载JS脚本和CSS文件时都会阻塞渲染。

4、构建渲染树renderTree

  • 根据DOM树和CSSOM树会生成一个渲染树render Tree。
  • 根据渲染树,会对树中的节点进行计算,确定每个节点在页面中的宽高和位置,最终确定布局,这一流程称为回流
    布局结束后,渲染进程会进行分层、图层绘制、分块、栅格化、最后到合成和显示。

5、分层 + 图层绘制

分层的主要目的是渲染引擎需要为特定的节点生成专用的图层,并生成一颗对应的图层树LayerTree。

  • 输入的内容是布局树,处理的主要对象是拥有层叠上下文属性的元素和需要裁剪的地方(超出容器的内容被隐藏或出现滚动条),这些内容都会被提升为单独的一层图层。
  • 图层绘制的目的是渲染引擎对图层树中的每个图层进行绘制。

6、分块 + 栅格化

  • 分块的概念是若一个页面很大的时候,一下子全部渲染会导致渲染时间过长。通过视口,用户只能看到很小的一部分,为了提升性能,合成线程会按照视口附近的图块优先生成位图,实际生成位图的操作是由栅格化来执行的。
  • 栅格化是指将图块转换成位图,图块是栅格化执行的最小单位,生成的位图保存在GPU内存中。

7、合成和显示

最后当所有图块被栅格化,合成线程就会生成一个绘制图块的命令,浏览器接收到该命令后,将页面的内容绘制到内存中,最后将内存显示在屏幕上。

2、重绘和回流是什么?

重绘Repaint
当元素样式的改变不影响布局时,浏览器会将新样式赋予给元素并重新绘制它,对应的就是分层、图层绘制、分块、栅格化以及合成的过程。例如:color、background-color、visibility等。

回流Reflow
当渲染树中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程。对应的就是构建DOM树、CSSOM树以及合成渲染树的过程。

导致回流的操作:

  • 页面首次渲染。
  • 浏览器窗口大小发生改变。
  • 元素尺寸或位置发生改变。
  • 元素内容发生改变。
  • 元素字体发生改变。
  • 添加或删除可见的DOM元素。
  • 激活CSS伪类。
  • 查询某些属性或调用某些方法,比如clientWidth、clientHeight、offsetWidth、offsetHeight、scrollWidth、scrollHeight、scrollTo()等等。

性能影响:
回流比重绘的代价更高,因为回流会导致重绘,但是重绘不一定引起回流。而且有时回流一个单一的元素,会导致其父元素和与之相关的元素也产生回流。

避免回流:

  • CSS方面:

    • 避免使用table布局。
    • 尽可能在DOM树的最末端改变class。
    • 避免设置多层内联样式。
    • 将动画效果应用到position属性为absolute或fixed的元素上。
  • JS方面:

    • 避免频繁操作DOM,最好是统一操作。

    • 避免频繁操作样式,最好一次性重写。

    • 也可以先为元素设置display:none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。

    • 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。

    • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

3、浏览器的缓存机制

概念:

浏览器缓存是指浏览器会将从服务器请求回来的资源进行存储,等下次访问的时候判断存储的状态是否能够使用后,决定该次资源的获取方式:再次请求服务器 or 使用缓存资源。

目的:
对于一个请求来说,资源的流转过程大致可理解为:
浏览器发起网络请求-后端对请求进行处理-服务器响应
浏览器缓存的目的或者说作用在于,可以帮助优化请求和响应的性能
如果可以使用缓存,就减少了请求时间,避免重复请求,优化整个页面的渲染时间。

缓存的位置:
浏览器缓存的位置主要可以分为Memory CacheDisk Cache。 比如:

  • 200 from memory cache,代表不访问服务器,直接从内存中读缓存。
  • 200 from disk cache,代表不访问服务器,直接从磁盘中读缓存。 1. Memory Cache
    主要是包含当前页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等,其特点在于
  • 读取速度速度比disk cache快
  • 缓存持续性短
  • 会随着浏览器进程的释放而释放(一旦关闭Tab栏,内存中的缓存就会被释放) 2. Disk Cache
    主要是大文件和系统内存使用率高的文件,其特点在于
  • 读取速度慢点
  • 比memory cache存储时间长,可存储的东西多 在所有浏览器缓存中,disk cache覆盖面基本是最大的

三级缓存原理(访问缓存优先级):

  1. 先在内存中查找,如果有的话直接加载
  2. 如果内存中不存在,就到硬盘中查找。
  3. 如果硬盘中也没有,就进行网络请求,请求到的资源缓存到硬盘和内存中。

缓存机制:
浏览器和服务器之间的通信方式为:request-response123.png

  • 浏览器发起HTTP请求时,先判断是否有缓存
  • 如果没有,向服务器请求,响应的请求结果以及缓存标识存入缓存中,并加载页面。
  • 如果有,则判断是否命中强缓存
    • 如果命中强缓存,则读取缓存,返回200+缓存信息,加载页面。
    • 如果没有命中强缓存,则发送请求,判断是否命中协商缓存
      • 如果命中协商缓存,且资源可用,则读取缓存,返回缓存,加载页面。
      • 如果没有命中协商缓存,则读取响应,并将资源存储缓存中。

缓存的策略:
缓存可以分为强缓存协商缓存
浏览器向服务器请求资源时,首先判断是否命中强缓存,再根据请求的响应结果判断是否命中协商缓存。

1、强缓存:

浏览器在加载资源时,会先根据本地缓存资源的请求头中的信息判断是否命中强缓存,如果命中,则直接返回缓存中的资源不会再向服务器发送请求。

强缓存中判断请求头的信息是ExpiresCache-Control

  • Expires:http1.0时的规范,它的值是一个绝对时间的GMT格式的时间字符串。这个时间代表着这个资源的失效时间,在此时间之前就可以命中缓存。
    这种方式有一个明显的缺点,由于失效时间是一个绝对时间,所以当服务器与客户端时间偏差度较大时,就会导致缓存混乱。

  • Cache-Control: 是http1.1时出现的,主要是利用该字段的max-age值来进行判断,它是一个相对时间,例如Cache-Control:max-age=3600,代表着资源的有效期是3600秒。

    Cache-Control常用的设置值:
    no-cache:需要进行协商缓存,发送请求到服务器确认是否使用缓存。
    no-store:禁止使用缓存,每一次都要重新请求数据。
    public:可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器。
    private:只能被终端用户的浏览器缓存,不允许CDN等中继缓存服务器对其缓存。

    Cache-Control与Expires可以在服务端配置启用,同时启用的时候,Cache-Control优先级高,在某些不支持HTTP1.1的环境下,Expires会生效。

2、协商缓存:

当强缓存没有命中的时候,浏览器会发送一个请求到服务器,服务器根据请求头中的信息判断是否命中缓存,如果命中,则返回304,告诉服务器资源没有更新,可以使用本地缓存。

协商缓存策略中的请求头信息是Last-Modify / If-Modify-SinceETag / If-None-Match

  • Last-Modify/If-Modify-Since
    浏览器第一次请求一个资源的时候,服务器会在响应头中加上Last-Modify,值是这个资源在服务器上最后的修改时间,浏览器接收后缓存文件和响应头。
    浏览器下一次请求这个资源,当检测到有Last-Modify字段,就会添加If-Modify-Since的请求头字段,值是Last-Modify的值。
    当服务器收到这个资源请求,判断里面有If-Modify-Since的值,会和服务器中这个资源最后的修改时间对比,如果没有变化,响应304和空的响应体,意思是让浏览器直接使用缓存。如果If-Modify-Since时间小于服务器中这个资源的最后修改时间,说明文件有更新,于是响应200和新的资源文件。
    缺点:

    • 短时间内资源发生了改变,Last-Modify并不会发生变化
    • 如果这个资源在一个周期内修改回原来的样子,正常来说应该是可以使用缓存,但是按时间的就没办法辨别,所以就产生了新的策略ETag。
  • ETag/If-None—Match
    与Last-Modify/If-Modify-Since不同的是,ETag/If-None-Match返回的是一个校验码
    ETag是服务器响应请求时返回当前资源文件的一个唯一标识(由服务器生成),只要资源有变化,ETag就会重新生成。
    浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的Etag值放到请求头里的If-None-Match里,服务器只需要比较客户端传来的If-None-Match跟自己服务器上该资源的ETag是否一致,就能很好地判断资源相对客户端而言是否被修改过了。

  • 两者的区别:

    • 精确度上:ETag优于Last-Modify。
    • 性能上:Last-Modify由于ETag,因为Last-Modify只需要记录时间,而ETag需要服务器去计算一个hash值。
    • 优先级上:服务器校验优先考虑ETag。

应用场景

  • 1、频繁变动的资源:Cache-Control: no-cache
    使用协商缓存
  • 2、不常变化的资源:Cache-Control: max-age=31536000
    处理不常变化的资源时,给Cache-Control配置一个很大的时间,这样浏览器之后请求相同的URL会命中强缓存。而为了解决更新的问题,就需要在文件名中添加hash、版本号等动态字符,之后更改动态字符从而达到更改引用URL的目的,让之前的强缓存失效。

用户行为对浏览器缓存的影响

  • 1、打开网址,在地址栏输入地址
    查找disk cache中是否有匹配,如果有则匹配,如果没有则发送网络请求。
  • 2、普通刷新(F5)
    因为Tab栏并没有关闭,因此Memory Cache时可用的,所以会被优先使用,其次会查找Disk Cache,换句话说强缓存无效,而协商缓存有效。
  • 3、强制刷新(Ctrl + F5)
    浏览器不适用缓存,因此发送的请求头均带有Cache-Control:no-cache,强缓存无效,而且协商缓存无效。

4、DNS服务

什么是DNS?
DNS全称Domain Name System,即域名系统。
DNS是一个由分层的DNS服务器实现的分布式数据库;一个使得主机能够查询分布式数据库的应用层协议。
DNS协议运行在UDP之上。

DNS的服务

  1. DNS解析
    DNS解析就是通过域名,最终得到该域名对应的IP地址的过程。
  2. 主机别名(host aliasing)
    有着复杂域名的主机能拥有一个或者多个别名,当主机别名存在时,可能比域名规范名更加容易记忆,所以DNS可以通过获取主机别名来找到对应的规范域名以及对应的IP地址。
  3. 邮件服务器别名(mail server aliasing)
    同样的,电子邮箱地址虽然现在看上去简单,但是其邮件服务器的域名可能会很复杂。电子邮件程序可以调用DNS,对提供的域名进行解析,获取该地址的规范域名以及其IP地址。
  4. 负载分配(load distribution)
    DNS也用于在冗余的服务器之间进行负载分配。繁忙的站点被冗余分布在多台服务器上,每台服务器均运行在不同的端系统上,每个都有着不同的IP地址。由于这些冗余的Web服务器,一个IP地址集合因此与同一个规范域名相联系。DNS数据库中存储着这些IP地址集合,当用户对映射到某地址集合的名字发出一个DNS请求时,该服务器用IP地址的整个集合进行响应,这样就等于DNS会在所有冗余的Web服务器之间循环分配了负载。

DNS缓存

DNS解析是带有缓存的,因为IP地址是唯一的,缓存能减少查找时间。
一般来说DNS缓存会存在于浏览器缓存、操作系统缓存、本地hosts文件、部分路由器缓存、本地DNS服务器缓存、根域名DNS服务器缓存
查询过程:

  1. 先搜索浏览器自身的DNS缓存
  2. 尝试读取操作系统的hosts文件
  3. 部分路由器会存储DNS缓存
  4. 查找本地DNS服务器
  5. 查找根域名DNS服务器

5、CSS、JS是否阻塞渲染?

首先结论是CSS、JS都会阻塞页面的渲染

2531619586-5a8e24dbd33a5.png

CSS

一般来说,浏览器的渲染会经过几个流程:

  • 构建DOM Tree - 构建CSS Rule Tree - 将DOM Tree和CSS Rule Tree合成为Render Tree。

CSS阻塞页面渲染

对于页面来说,渲染的时候需要两种元素,一种是DOM Tree,一种是CSS Rule Tree。换句话说,如果在CSS加载过程中,就无法生成CSS Rule Tree,自然也就会阻塞渲染。

CSS不会阻塞DOM解析
根据上面的流程图,以及浏览器渲染的经过,不难看出:构建DOM树,是和构建CSS Rule Tree并行发生的,所以CSS的加载并不会影响HTML解析和DOM树的构成。

JavaScript
JS脚本和CSS文件不同的是,JS脚本不仅会阻塞页面的渲染,还会阻塞DOM的解析。
当浏览器加载中遇到JS脚本,由于JS脚本会操作DOM,所以会导致暂停构建DOM,去下载并运行JS脚本,直到脚本运行完成,然后继续构建DOM。
每次去执行JS脚本都会严重地阻塞DOM树的构建,如果JS脚本还操作了CSSOM,而正好这个CSSOM还没有下载和构建,浏览器甚至会延迟脚本执行和构建DOM,直至完成其CSSOM的下载和构建。

所以,JS脚本的位置很重要,之前的策略都是将JS脚本放在HTML的最后位置,这样,当程序运行到JS脚本的位置时,DOM构建已经完成,就不会去阻塞DOM的构建过程。

现在有新的script标签属性:deferasync

script标签中的defer和async

2151798436-59da4801c6772_fix732.png

之前的script标签中并没有,在HTML5中,<script>标签新增的两个属性defer和async
defer:加载后续文档元素的过程将和 <script> 的加载并行进行(异步),但是 <script> 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成。
下载是异步的,执行需等待DOM解析完成后
async:加载和渲染后续文档元素的过程将和 <script> 的加载与执行并行进行(异步)。
下载是异步的,下载完就执行,执行会阻塞DOM解析