从输入URL到浏览器渲染(自我总结篇)

196 阅读9分钟

从输入URL到浏览器渲染

一、用户输入阶段

用户在地址栏输入内容之后,浏览器首先会判断用户输入的是合法的URL还是搜索内容,如果是搜索内容就会直接利用引擎进行查找,如果是合法的URL就开始进行加载。

二、缓存查询

此时会进行强缓存查询,如果标识命中,且在有效期内,就直接返回资源。如果标识命中了但是过期了,并不会简单的把缓存删除,而是会携带协商缓存标识发起请求,去服务端查看该资源是否能继续使用?

三、发起URL阶段

我们会对合法的URL进行解析,通过DNS查询,我们会找到域名与IP地址之间的一个映射关系。

1. DNS查询
  1. 首先找到浏览器本地的缓存中有没有对应的域名解析的IP地址,如果有的话直接返回

  2. 浏览器缓存中没查到的话,会去电脑的hosts文件中查找,如果有的话直接返回

  3. hosts没找的的话,会去本地区域名服务器(Local DNS)发起查询请求,本地服务器收到请求后,会先查找本地区的缓存,如果有的话就直接返回。

    本地区域名服务器通常性能都会很好,它们一般都会缓存域名解析结果,当然缓存时间是受域名的失效时间控制的,一般缓存空间不是影响域名失效的主要因素。大约90%的域名解析都到这里就已经完成了,所以LDNS主要承担了域名的解析工作。

  4. 如果LDNS缓存没有结果的话,会向根域名服务器发起请求,根域名(Root Server)返回的是一个查询域(根的子域,比如.com)的主域名服务器(gTLD Server)的地址,它是国际顶级域名服务器,比如.com .cn . org这种

  5. LDNS拿到主域名服务器的地址后,向主域名服务器地址发起请求

  6. 接受请求的主域名服务器会查找并返回此域名所对应的Name Server域名服务器地址,它是你注册的域名服务器(也就是为你提供域名服务的提供商的地址,你的域名解析任务由他服务器完成)

  7. LDNS拿到域名服务商的地址之后,向它发起请求,Name Server域名服务器会查询域名与ip的映射关系,然后返回给你

  8. LDNS最终拿到了ip地址与TTL(Time to Live) 根据TTL缓存这个域名与ip之间的关系

  9. 最终把ip返回给浏览器

2.建立TCP连接

通过三次握手与服务器建立连接,进行数据传输

image.png

三次握手是客户端与服务的建立连接的过程,当客户端向服务端发起连接时,会发一包链接数据过去询问(SYN包)能否与你建立连接?如 果服务端同意连接,则服务端会向客户端发送一包SYN+ACK包,客户端收到之后回复一包ACK包,然后连接建立。因为这个连接过程中互相发送了三包数据,所以称为3次握手。

为什么要三次握手

这是为了防止已经失效的报文突然传到服务器,引发错误,浪费资源。假如第一次的SYN包因为网络延误没有及时传到服务端,在已经建立连接后,这个SYN包又传送到了服务端,那么服务端会以为客户端发起了新的连接,而进入数据等待阶段,此时服务端以为有两个连接,而客户端以为只有一个,造成了状态不一致。

但如果是三次握手的话,服务端接收到延误的SYN包后向客户端发送SYN+ACK包,由于此时客户端已经建立连接,并不会返回ACK包,那么服务端就会默认连接失败,也就不会出现服务端状态发生改变的情况了。

四、发起http请求

1. 协商缓存

根据请求头上的缓存标识检查是否命中?

  • 缓存有效:服务器返回304重定向,并且响应头带上新的缓存标识,浏览器做出相应的缓存动作。
  • 缓存失效:服务器直接返回资源,状态码200,并带上新的缓存标识。

五、页面渲染阶段

在浏览器获取到HTML之后,因为浏览器无法直接理解与使用HTML,所以需要将HTML转为浏览器能够理解的结构----DOM树

  1. 将HTML结构转化为DOM树,将CSS转化为CSS树

  2. 根据dom树与css树生成render树(渲染树)

  3. 布局阶段(layout):然后我们根据render树生成layout tree(布局树),在布局树中,DOM树上所有不可见的节点都没有加到上面。layout tree上会显示出各个节点的坐标位置以及宽高

  4. 绘制阶段(painting):接着会根据layout tree来生成图层树 layer tree,它是根据你的不同节点所对应的层级关系来生成的。并根据你的不同的图层去生成painting表,它记录了绘制图层的指令和步骤

    img

  5. 栅格化操作:此时会将paint表commit到渲染进程的合成线程中去操作。

img

  1. 一个页面很大,用户只能看见其中一部分,我们叫这部分为视口,为了避免不必要的开销,我我们会优先渲染视口附近的区域。合成线程会将图层分层很多图块。然后合成线程会按照视口附近的图块来优先转换为位图,生成位图的操作是由栅格化来执行的。
  2. 栅格化:其实就是将图块转化为位图,图块是栅格化的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都在栅格化线程池内执行。
  3. 通常,栅格化过程都会使用GPU来加速生成,使用GPU生成位图的过程叫快速栅格化,或者GPU栅格化,生成的位图被保存在GPU内存中。
  4. 一旦所有的图块都被光栅化之后,合成线程就会生成一个绘制图块的命令----“DrawQuad”,将该命令提交给浏览器进程。浏览器进程里有一个组件用于接收合成器线程发过来的指令,根据那个命令将页面内容绘制到内存中,最后再显示在屏幕上。
  5. 好了,我们现在已经分析完了整个渲染流程,从HTML到DOM、样式计算、布局、图层、绘制、光栅化、合成和显示。圆满结束!

img

结合上图,一个完整的渲染流程大致可总结为如下

  • 渲染进程将HTML内容转换为能够读懂的DOM树结构。
  • 渲染引擎将CSS样式表转化为浏览器可以理解的styleSheets,计算出DOM节点的样式。
  • 创建布局树,并计算元素的布局信息。
  • 对布局树进行分层,并生成分层树。
  • 为每个图层生成绘制列表,并将其提交到合成线程。
  • 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
  • 合成线程发送绘制图块命令DrawQuad给浏览器进程。
  • 浏览器进程根据DrawQuad消息生成页面,并显示到显示器上

浏览器渲染卡顿怎么办?

1.采用requestAnimationFrame()来解决

这是浏览器的官方 API,此方法会在每一帧被调用,通过 API 的回调,我们可以把 JS 运行任务分成一些更小的任务快(分到每一帧),在每一帧时间用完前暂停 JS 执行,归还主线程,这样的话在下一帧开始时,主线程就可以按时执行布局和绘制。

React最新的渲染引擎React Fiber就用到了这个API做了很多优化(时间分片)

image.png

image.png

2. 为script标签加上async 或者 defer属性
  • async是异步加载,加载完进行执行
  • defer也是异步加载,不过会在dom解析完后才执行
3.这里就要提到 CSS 中的一个动画属性 transform

由于栅格化的整个流程是不占用主线程的,只在下面的 合成器线程 + 栅格线程中执行,意味着它无需和 JS 抢夺主线程,我们如果反复进行重排和重绘,可能会导致掉帧,这是因为有可能 JS 执行阻塞了主线程,而经 transform 实现的动画不会经过布局和绘制,而是直接运行在合成器线程和栅格线程,所以不会受到主线程中 JS 执行的影响,所以节省了很多时间,减轻了主线程的压力。

image.png

关于重排重绘

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

img

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

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

img

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

3.直接合成阶段

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

img

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