从浏览器输入网址到渲染页面xx

136 阅读9分钟

简单而又不简单的面试题:浏览器输入网址到渲染页面的全过程

如果面试中你被问到这个问题,也许你脑海里已经有了以下答案:

  1. 浏览器DNS解析URL,发起网络请求对应IP地址+port端口号(80,默认端口);
  2. 浏览器经过TCP三次握手与服务器建立通信(输出一波TCP三次握手过程);
  3. 获取页面资源,拿到HTML文档;
  4. 解析HTML,生成DOM树、css树,生成reander树(或者layout-tree),并渲染页面。

[一气呵成,简单高效,还有细节在里面。]

那么,想一下:

  • 打开浏览器,最少启动哪几个进程,各自负责什么功能?
  • 渲染页面过程中,什么导致的动画卡顿,有什么优化方法?
  • 重排(回流)和重绘的触发机制?
  • 从渲染层面看前端如何进行页面优化?

带着这些问题,我们先看一下浏览器输入网址到渲染页面,发生了什么:

输入网址
  1. 首先,我们打开浏览器,往地址栏输入网址,此时浏览器进程下的UI线程捕获地址栏内容,获取用户输入内容并解析
  2. 当我们按下回车,浏览器进程将我们输入的有效网址的URL通过IPC管道发送到网络进程(如果输入非网址则启用搜索引擎搜索)
  3. 强制缓存影响
    如果当前所请求资源之前请求过,浏览器缓存数据未清除过,在强制缓存有效期内,那么直接使用缓存资源,不会发起网络请求,直接快进到渲染界面流程。
控制强制缓存的HTTP字段有两个:
Expires:Http1.0开始支持
Cache-Control:max-age=设置的缓存有效期,http1.1开始支持,优先级大于Expires

当资源在强制缓存有效期内,对资源的请求响应如下:

[General]
Request URL: http://127.0.0.1:8000/
Request Method: GET
Status Code: 200 OK (from disk cache)
Remote Address: 127.0.0.1:8000
Referrer Policy: strict-origin-when-cross-origin

[Response Headers]
content-Type: application/json;charset=utf-8
Date: Mon, 01 Aug 2022 09:43:36 GMT
Etag: 1212
Expires: Fri, 19 Aug 2022 00:00:00 GMT

从from disk cache可以看到,资源来源于磁盘缓存,并不是从服务端获取的数据
  1. 浏览器网络进程通过DNS解析域名获取对应ip,开始发起tcp连接,进入TCP三次握手环节:

image.png

扩展:
【SYN】:
HTTP首部字段的标志字段,标识是否是新的TCP连接;
如果是新的连接,置为1,在第三次握手时置为0(此时已建立连接);
  1. TCP连接建立完成,如果是https协议,那么会继续进行TLS/SSL连接
    [扩展-TLS握手]

未命名文件 (10).png

  1. 协商缓存影响
    **协商缓存: **
如果服务器校验所请求资源在协商缓存有效期内,返回304状态码,浏览器根据该状态码使用本地缓存资源;
如果服务器校验所请求资源不在有效期内,则直接返回最新的资源,状态码为200。


由此我们知道:
强缓存和协商缓存的共同点是,都是从客户端缓存中读取资源;区别是强缓存不会发请求,协商缓存会发请求
  1. [重定向]
    如果服务器返回301,浏览器根据响应头location字段,重定向到location的URI地址,并重新进行之前请求步骤

  2. 服务器返回请求资源,浏览器会根据响应头的Content-type做响应的处理,如果HTML资源,那么进入解析HTML文档的流程

当解析Content-type为application/octet-stream时,浏览器会触发下载器进行文件下载
解析渲染HTML
  1. 浏览器页面渲染流程 从解析HTML文档开始
- 构建DOM树

遍历HTML-DOM节点,解析成树形的数据结构

为什么要构建DOM树:
    因为浏览器无法直接理解和使用HTML,需要转换成浏览器能够处理的树形数据结构;
    (构建css-tree同理)
- 构建CSS树与样式计算
  1. 加载解析样式生成CSSOM(CSS Object Model)树
  2. 转换css属性值,使其标准化(比如em, rem, blue, bold...)
  3. 计算DOM树中每个节点对应的具体样式(涉及css的样式继承以及优先级规则->即样式权重,!important > 内联 > ...),生成渲染树-render tree
1. css的加载会阻塞页面的显示吗:
   -不会阻塞DOM树的解析(DOM解析和CSS解析是两个并行的操作)
   -会阻塞DOM树的渲染
   [由于Render Tree是依赖于DOM TreeCSSOM Tree的,
   所以必须等待到CSSOM-Tree构建完成,也就是CSS资源加载完成(或者CSS资源加载失败)后,才能开始渲染
   因此,CSS加载是会阻塞Dom的渲染的]
   -会阻塞后续js的执行
   
2. 下载CSS文件阻塞了,会阻塞DOM树的合成吗?会阻塞页面的显示吗?
   -不会阻塞DOM树的合成
   [因为下载过程由浏览器网络进程控制,渲染进程继续解析HTML]
   -会阻塞页面的显示
   [样式计算需要等待css资源请求回来之后进行样式层叠,直到网络超时,渲染进程继续层叠样式计算]
   
3. JavaScript阻塞解析
   解析器碰到<script>标签,会暂停解析HTML文档,即js抢占主线程执行时间
   <因为js会通过dom节点API对文档进行修改,从而改变DOM结构,所以HTML解析器必须停下来执行js,然后再解析HTML>

- 布局阶段
  1. 遍历DOM和计算好的样式(浏览器查看元素样式的computed)创建布局树[layout-tree](只包含可见元素的布局树, 比如head标签,不可见元素都不包含)
    image.png
  2. 布局计算
    计算布局树节点的坐标位置,从根节点开始遍历,结合之前计算得到的样式,确定每个节点对象在页面上的具体大小和位置,将这些信息保存在布局树中
  • 分层
    对于复杂页面效果如3D变换、页面滚动,或者使用z-index等,渲染引擎还需要为这些特定的节点生成专用的图层,并生成一棵对应的图层树(layer-tree)

需要生成单独图层的节点:
- 拥有层叠上下文z-index属性的节点(z-index值不为auto,且叠加在根图层之上)
- position:fixed,固定定位元素
- opacity小于1
- 根元素
- transfrom的值不为none
- 需要裁剪的区域,eg: 给一个div设置固定宽高,div内容文字超出了div范围,渲染引擎会为文字部分单独生成一个单独的图层
- 滚动条
- ...

为什么需要分层?
如果直接layout-tree生成目标图标,后续对dom的微小改动,都会造成对整个页面的重排或者重绘

- 绘制图层

渲染进程根据图层树,对每一个图层进行拆分绘制,图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表(绘制列表可以在浏览器控制台layers中查看)

image.png

之后渲染进程的主线程会把待绘制列表提交给合成线程,合成线程会将图层划分为图块(tile),这些图块的大小通常为(256x256)或者(512x512).

为什么拆分成图块:
   一次性直接绘制出所有的图层内容,开销太大,所以只能将图层拆分成多个图块,优先绘制靠近视口位置的图块
- 图块栅格化

渲染进程下的栅格化线程池对所有图块进行栅格化(rastering),每个栅格化后的图块信息都保存在GPU内存中

[栅格化]是将矢量图形格式表示的图像转换成位图以用于显示器或者打印机输出的过程,即矢量图->位图
- 合成与绘制

当所有图块栅格完成后,合成器线程生成draw quads指令(在屏幕的指定位置绘制图块的指令,记录了图块在内存中的位置,以及在页面的哪个位置绘制图块的信息),并生成一个合成器帧(多个draw quads命令的集合,组成页面的一帧),只有通过IPC管道将合成器帧发送给浏览器进程,浏览器进程将该合成器帧发送给GPU进程,GPU进程进行渲染,然后就渲染到页面上了,就此,页面内容就生成了。

【渲染进程绘制流程】 未命名文件 (9).png

重排(回流)和重绘

重排即重新布局,影响layout布局阶段后面的全部流程;
重绘即重新渲染,影响绘制图层之后的流程

[导致重排的操作]:
- 页面首次渲染
- 浏览器窗口大小发生变化
- 元素改变布局相关的属性或操作,比如尺寸、位置、内容(文字数量或图片大小的变化)
- DOM元素的添加或删除
- CSS伪类的状态,如:hover
- ...

[导致重绘的操作]:
- 元素样式的改变不影响它在文档流的定位布局, 如color,background-color、visibility...

[如何减少重排和重绘]:
- 避免编写复杂的DOM结构,减少DOM层级
- 脱离文档流(尤其是对具有复杂动画的元素,否则会引起父元素及后续元素频繁重排)
- 样式集中修改,一次性修改style属性或将变更样式集成到一个class下,通过添加class类名修改样式
- 能用transfrom实现动画效果就不用其他(transform<+transition>实现的动画不会经过布局和绘制,而是直接运行在合成器线程中)

*重排一定会触发重绘,重绘不一定触发重排(比如font-size设置过大可能会导致重排)
打开浏览器,最少启动哪几个进程
最少四个进程:
    浏览器进程(用户界面) + 网络进程(发起网络请求) + 渲染进程(页面展示) + GPU进程(页面渲染)
渲染层面看前端优化
1. js执行占用主线程
在浏览器架构下,js也是运行在渲染进程的主进程中,跟DOM解析、样式布局计算到绘制图层操作抢占执行时间,
长时间的js执行占用会导致页面渲染不及时出现卡顿

优化方式:
- 使用requestIdleCallback和requestAnimationFrame浏览器API
https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback
https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame
2. 页面重排和重绘频繁
解决办法见上面关于如何减少重排和重绘
3. 资源加载阻塞
优化方式:
1. 资源加载顺序
- js标签放body末尾
2. 资源打包体积优化
3. 配置CDN