本文目录:
- 一、Chrome架构
- 二、导航阶段
- 三、渲染阶段
- 四、渲染流程概括
从输入URL到页面渲染需要Chrome浏览器的多个进程配合,所以我们先来谈谈现阶段Chrome浏览器的多进程架构。
一、Chrome架构
目前Chrome采用的是多进程的架构模式,可分为主要的五类进程,分别是:浏览器(Browser)主进程、 GPU 进程、网络(NetWork)进程、多个渲染进程和多个插件进程;
- 浏览器进程。主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
- 渲染进程。核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎Blink和JavaScript引擎V8都是运行在该进程中,默认情况下,Chrome会为每个Tab标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
- GPU进程。其实,Chrome刚开始发布的时候是没有GPU进程的。而GPU的使用初衷是为了实现3D CSS的效果,只是随后网页、Chrome的UI界面都选择采用GPU来绘制,这使得GPU成为浏览器普遍的需求。最后,Chrome在其多进程架构上也引入了GPU进程。
- 网络进程。主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
- 插件进程。主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响
了解了Chrome的多进程架构,就能够从宏观上理解从输入URL到页面渲染的过程了,这个过程主要分为导航阶段和渲染阶段。
二、导航阶段
Ⅰ.浏览器主进程
1.用户输入URL
1、浏览器进程检查url,组装协议,构成完整的url,这时候有两种情况:
- 输入的是搜索内容:地址栏会使用浏览器默认的搜索引擎,来合成新的带搜索关键字的URL。
- 输入的是请求URL:地址栏会根据规则,给这段内容加上协议,合成为完整的URL;
2、浏览器进程通过进程间通信(IPC)把url请求发送给网络进程;
Ⅱ.网络进程
2.URL请求过程
3、网络进程接收到url请求后检查本地缓存是否缓存了该请求资源,如果有则将该资源返回给浏览器进程;
4、准备IP地址和端口:进行DNS解析时先查找缓存,没有再使用DNS服务器解析,查找顺序为:
- 浏览器缓存;
- 本机缓存;
- hosts文件;
- 路由器缓存;
- ISP DNS缓存;
- DNS递归查询(本地DNS服务器 -> 权限DNS服务器 -> 顶级DNS服务器 -> 13台根DNS服务器)
5、等待TCP队列:浏览器会为每个域名最多维护6个TCP连接,如果发起一个HTTP请求时,这 6个 TCP连接都处于忙碌状态,那么这个请求就会处于排队状态;解决方案:
采用域名分片技术:将一个站点的资源放在多个(CDN)域名下面。
升级为HTTP2,就没有6个TCP连接的限制了;
6、通过三次握手建立TCP连接:
第一次:客户端先向服务器端发送一个同步数据包,报文的TCP首部中:标志位:同步SYN为1,表示这是一个请求建立连接的数据包;序号Seq=x,x为所传送数据的第一个字节的序号,随后进入SYN-SENT状态;
第二次:服务器根据收到数据包的SYN标志位判断为建立连接的请求,随后返回一个确认数据包,其中标志位SYN=1,ACK=1,序号seq=y,确认号ack=x + 1表示收到了客户端传输过来的x字节数据,并希望下次从x+1个字节开始传,并进入SYN-RCVD状态;
第三次:客户端收到后,再给服务器发送一个确认数据包,标志位ACK=1,序号seq=x+1,确认号ack=y+1,随后进入ESTABLISHED状态;
为什么要第三次握手?避免服务器等待造成资源浪费
7、构建并发送HTTP请求信息;
8、服务器端处理请求;
9、客户端处理响应,首先检查服务器响应报文的状态码:
- 如果是301/302表示服务器已更换域名需要重定向,这时网络进程会从响应头的Location字段里面读取重定向的地址,然后再发起新的HTTP或者HTTPS请求,跳回第4步。
- 如果是200,就检查Content-Type字段,值为text/html说明是HTML文档,是application/octet-stream说明是文件下载;
10、请求结束,当通用首部字段Conection不是Keep-Alive时,即不为TCP长连接时,通过四次挥手断开TCP连接:
- 第一次:客户端(主动断开连接)发送数据包给服务器,其中标志位FIN=1,序号位seq=u,并停止发送数据;
- 第二次:服务器收到数据包后,由于还需传输数据,无法立即关闭连接,先返回一个标志位ACK=1,序号seq=v,确认号ack=u+1的数据包;
- 第三次:服务器准备好断开连接后,返回一个数据包,其中标志位FIN=1,标志位ACK=1,序号seq=w,确认号ack=u+1;
- 第四次:客户端收到数据包后,返回一个标志位ACK=1,序号seq=u+1,确认号ack=w+1的数据包。
为什么要四次挥手?由于服务器不能马上断开连接,导致FIN释放连接报文与ACK确认接收报文需要分两次传输,即第二次和第三次"挥手";
如果文档中有其他资源 则重复建立请求与获得相应的动作,直至资源全部加载完毕。
3.准备渲染进程
11、准备渲染进程:浏览器进程检查当前url是否与之前打开了渲染进程的页面的根域名相同,如果相同,则复用原来的进程,如果不同,则开启新的渲染进程;
4.提交文档
12、提交文档:
渲染进程准备好后,浏览器向渲染进程发起“提交文档”的消息,渲染进程接收到消息后与网络进程建立传输数据的“管道”
渲染进程接收完数据后,向浏览器发送“确认提交”
浏览器进程接收到确认消息后更新浏览器界面状态:安全状态、地址栏url、前进后退的历史状态、更新web页面
三、渲染阶段
在渲染阶段通过渲染流水线在渲染进程的主线程和合成线程配合下,完成页面的渲染;
Ⅲ.渲染进程
5.构建DOM树
13、先将请求回来的数据解压,随后HTML解析器将其中的HTML字节流通过分词器拆分为一个个Token,然后生成节点Node,最后解析成浏览器识别的DOM树结构。
可以通过Chrome调试工具的Console选项打开控制台输入document查看DOM树;
首次解析HTML时渲染进程会开启一个预解析线程,遇到HTML文档中内嵌的JavaScript和CSS外部引用就会同步提前下载这些文件,下载时间以最后下载完的文件为准。
6.构建CSSOM
14、CSS解析器将CSS转换为浏览器能识别的styleSheets也就是CSSOM:可以通过控制台输入document.styleSheets查看;
这里要考虑一下阻塞的问题,由于JavaScript有修改CSS和HTML的能力,所以,需要先等到 CSS 文件下载完成并生成 CSSOM,然后再执行 JavaScript 脚本,最后再继续构建 DOM。由于这种阻塞,导致了解析白屏;
优化方案:
- 移除js和css的文件下载:通过内联 JavaScript、内联 CSS;
- 尽量减少文件大小:如通过 webpack 等工具移除不必要的注释,并压缩 js 文件;
- 将不进行DOM操作或CSS样式修改的 JavaScript 标记上 sync 或者 defer异步引入;
- 使用媒体查询属性:将大的CSS文件拆分成多个不同用途的 CSS 文件,只有在特定的场景下才会加载特定的 CSS 文件。
可以通过浏览器调试工具的Network面板中的DOMContentLoaded查看最后生成DOM树所需的时间
7.样式计算
15、转换样式表中的属性值,使其标准化。比如将em转换为px,color转换为rgb;
16、计算DOM树中每个节点的具体样式,这里遵循CSS的继承和层叠规则;可以通过Chrome调试工具的Elements选项的Computed查看某一标签的最终样式;
8.布局阶段
17、创建布局树,遍历DOM树中的所有节点,去掉所有隐藏的节点(比如head,添加了display:none的节点),只在布局树中保留可见的节点。
18、计算布局树中节点的坐标位置
9.分层
19、对布局树进行分层,并生成分层树(Layer Tree),可以通过Chrome调试工具的Layer选项查看。分层树中每一个节点都直接或间接的属于一个图层(如果一个节点没有对应的层,那么这个节点就从属于父节点的图层)
10.图层绘制
20、为每个图层生成绘制列表(即绘制指令),并将其提交到合成线程。以上操作都是在渲染进程中的主线程中进行的,提交到合成线程后就不阻塞主线程了
11.切分图块
21、合成线程将图层切分成大小固定的图块(256x256或者512x512)然后优先绘制靠近视口的图块,这样就可以大大加速页面的显示速度
Ⅳ.GPU进程
12.栅格化操作
22、在光栅化线程池中将图块转换成位图,通常这个过程都会使用GPU来加速生成,使用GPU生成位图的过程叫快速栅格化,或者GPU栅格化,生成的位图被保存在GPU内存中
Ⅴ.浏览器主进程
13.合成与显示
23、合成:一旦所有图块都被光栅化,合成线程就会将它们合成为一张图片,并生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。
为什么要经历栅格化和合成操作?因为栅格和合成的过程是在GPU 进程和渲染进程的合成线程中完成的,不会影响到渲染进程的主线程执行;
24、显示:浏览器进程里面有一个叫viz的组件,用来接收合成线程发过来的DrawQuad命令,然后根据DrawQuad命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。
到这里,经过这一系列的阶段,编写好的HTML、CSS、JavaScript等文件,经过浏览器就会显示出漂亮的页面了。
四、渲染流程概括
浏览器在拿到html和css文件后,分别进行解析,然后形成DOMTree和StyleRules(CSSOMTree),然后进行结合(Attachment)进行renderTree,renderTree生成了,在没有layout之前,浏览器还不知道具体的dom以及dom的具体位置,layout之后dom元素和基本布局已经形成,然后进行Painting,最终展示(display)到页面上。
解析html文件 -> 构建render树 -> 布局render树 -> 绘制render树
构建render树的过程:
- DOM Tree:浏览器将HTML解析成树形的数据结构。
- CSS Rule Tree:浏览器将CSS解析成树形的数据结构。
- Render Tree: DOM和CSSOM合并后生成Render Tree。
布局(layout): 有了Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的CSS定义以及他们的从属关系,从而去计算出每个节点在屏幕中的位置。
绘制(painting): 按照算出来的规则,通过显卡,把内容画到屏幕上。
回流(reflow):回流也叫做重排,当浏览器发现某个部分发生了点变化影响了布局,需要倒回去重新渲染,内行称这个回退的过程叫 reflow。reflow 会从 <html> 这个 root frame 开始递归往下,依次计算所有的结点几何尺寸和位置。reflow 几乎是无法避免的。现在界面上流行的一些效果,比如树状目录的折叠、展开(实质上是元素的显 示与隐藏)等,都将引起浏览器的 reflow。鼠标滑过、点击……只要这些行为引起了页面上某些元素的占位面积、定位方式、边距等属性的变化,都会引起它内部、周围甚至整个页面的重新渲 染。通常我们都无法预估浏览器到底会 reflow 哪一部分的代码,它们都彼此相互影响着。
Reflow的触发基本上都会触发Repaint
重绘(repaint):改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性时,屏幕的一部分要重画,但是元素的几何尺寸没有变。
注意:
(1)display:none 的节点不会被加入Render Tree,而visibility: hidden 则会,所以,如果某个节点最开始是不显示的,设为display:none是更优的。
(2)display:none 会触发 reflow,而 visibility:hidden 只会触发 repaint,因为没有发现位置变化。
(3)有些情况下,比如修改了元素的样式,浏览器并不会立刻reflow 或 repaint 一次,而是会把这样的操作积攒一批,然后做一次 reflow,这又叫异步 reflow 或增量异步 reflow。但是在有些情况下,比如resize 窗口,改变了页面默认的字体等。对于这些操作,浏览器会马上进行 reflow。
尽管不同浏览器的渲染引擎不同,如chrome用的Webkit和Firefox用的Gecko,使用的术语有所不同,但渲染的过程基本都是相同的
- 浏览器会将HTML解析成一个DOM树,DOM 树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。
- 将CSS解析成 CSS Rule Tree 。
- 根据DOM树和CSSOM来构造 Rendering Tree。注意:Rendering Tree 渲染树并不等同于 DOM 树,因为一些像Header或display:none的东西就没必要放在渲染树中了。
- 有了Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的CSS定义以及他们的从属关系。下一步操作称之为layout,顾名思义就是计算出每个节点在屏幕中的位置。
- 再下一步就是绘制,即遍历render树,并使用UI后端层绘制每个节点。