本文根据极客时间李冰老师的<浏览器工作原理与实践>,参照MDN,网上各种资料综合而成。尚未整理完毕。
这个过程主要分为两个方面,一个是导航流程,也就是输入url到浏览器获取到资源的部分,一个是渲染流程,也就是浏览器把获取到的资源渲染到页面的部分。
导航流程
首先是导航流程,当用户在地址栏中输入一个查询关键字时,地址栏会判断输入的关键字是搜索内容,还是请求的 URL。如果是搜索内容,地址栏使用浏览器默认搜索引擎合成带搜索关键字的url,如果判断输入内容符合 URL 规则,就会根据规则添加协议合成完整的url,比如输入baidu.com,浏览器会加上https://www。当用户按下回车后,标签页图标进入加载状态,但是页面内容并未替换,需要等待提交文档阶段才会替换。
之后浏览器进程通过IPC进程间通信把url请求发送到网络进程,网络进程接收后,会先在浏览器缓存中查询是否有要请求的文件。如果资源已经缓存,会检测是否是强缓存,主要通过expires和cache-control控制。
- HTTP1.0提供Expires,值为一个绝对时间表示缓存新鲜日期比如1M
- HTTP1.1增加了Cache-Control: max-age=,值为以秒为单位的最大新鲜时间
如果命中强缓存就从浏览器读取, 否则命中协商缓存或者没有缓存时,进入网络请求流程。
因为如果想要把数据发送到对方电脑,我们需要知道对方的IP地址和端口号,才能使用TCP协议建立连接。而IP地址是通过DNS解析域名得出。所以浏览器会请求DNS返回域名的对应IP
- 浏览器缓存
- 本机缓存
- hosts文件
- 路由器缓存
- ISP DNS缓存
- DNS递归查询(可能存在负载均衡导致每次IP不一样)本地DNS服务器,
- 本地迭代查询
- 根DNS服务器(13台)
- 顶级域名服务器(.com)
- 权威域名服务器(qq.com)
拿到 IP 之后,接下来就需要获取端口号了。通常情况下,如果 URL 没有特别指明端口号,那么 HTTP 协议默认是 80 端口。
当TCP/IP需要的IP和端口获取完毕后浏览器会组装成一个get请求报文。
由于Chrome机制,同一域名最多6个tcp连接,如果当前请求数量小于6,会进入tcp连接,否则需要等待tcp队列。排队等待结束后,建立tcp连接。
- 客户端发送SYN=1(请求同步),seq=x(客户端序列号)到服务器
- 服务器返回SYN=1(同意建立连接),seq=y(服务端序列号),ACK=1(你的请求已确认),ack=x+1(期望序列号)
- 客户端发送ACK=1(你的请求我已经接收),seq=x+1(客户端序列号),ack=y+1(我已经接收,期望你发送y+1
之所以要经历上述三个阶段,是为了浏览器和服务器双方都要知道自己是否能够正常的收发。
ACK 0 确认序号无效 1 确认序号成立(请求建立连接)
SYN 1 new connection 建立连接
FIN 1 close 关闭连接
RST 1 distory connection 当前连接异常
PSH 1 数据无需缓存直接给进程
URG 0 紧急指针无效 1 禁忌之中有效,这个包不需要排队,直接进入缓存
连接建立之后,浏览器端会构建请求行、请求头等信息,并把和该域名相关的 Cookie 等数据附加到请求头中,然后向服务器发送构建的请求信息。如果是post命令还会构建请求体
GET /index.html HTTP1.1
如果头信息中有
Connection:Keep-Alive
则 TCP 连接在发送后将仍然保持打开状态,这样浏览器就能继续通过这个连接发送请求。
之后断开连接
- 主动方发送Fin=1, Ack=Z, Seq= X报文
- 被动方发送ACK=X+1, Seq=Z报文
- 被动方发送Fin=1, ACK=X, Seq=Y报文
- 主动方发送ACK=Y, Seq=X报文
服务器接收到请求信息后,会根据请求信息生成响应数据(包括响应行、响应头和响应体等信息),并发给网络进程。等网络进程接收了响应行和响应头之后,就开始解析响应的内容了。
网络进程如果发现响应行的状态码是301或者302(301会把重定向的地址缓存,也就是永久重定向),说明网址变更,需要重定向到其他url,根据响应头的loaction字段信息重定向。如果是200或者304(etag没变说明文件哈希值没变,或者Last-Modified最后修改时间没变),表示浏览器可以继续处理这个请求。之后通过content-type字段告知浏览器如何处理这个请求。详细内容可以在w3c搜mime,那里一大堆。如果 Content-Type 字段的值被浏览器判断为下载类型,那么该请求会被提交给浏览器的下载管理器,同时该 URL 请求的导航流程就此结束。但如果是HTML,那么浏览器则会继续进行导航流程。由于 Chrome 的页面渲染是运行在渲染进程中的,所以接下来就需要准备渲染进程了。
打开一个新页面采用的渲染进程策略就是:
- 通常情况下,打开新的页面都会使用单独的渲染进程;
- 如果从 A 页面打开 B 页面,且 A 和 B 都属于同一站点的话,那么 B 页面复用 A 页面的渲染进程;如果是其他情况,浏览器进程则会为 B 创建一个新的渲染进程。
渲染进程准备好之后,还不能立即进入文档解析状态,因为此时的文档数据还在网络进程中,并没有提交给渲染进程,所以下一步就进入了提交文档(“文档”是指 URL 请求的响应体数据)阶段。
- “提交文档”的消息是由浏览器进程发出的,渲染进程接收到“提交文档”的消息后,会和网络进程建立传输数据的“管道”。
- 等文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程。
- 浏览器进程在收到“确认提交”的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新 Web 页面。
一旦文档被提交,渲染进程便开始页面解析和子资源加载,页面生成完成,渲染进程会发送一个消息给浏览器进程,浏览器接收到消息后,会停止标签图标上的加载动画。之后进入渲染阶段。
按照渲染的时间顺序,流水线可分为如下几个子阶段:构建 DOM 树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成。
构建DOM树dom
浏览器无法直接使用HTML,需要把HTML转化为浏览器可以理解的DOM树结构。
构建 DOM 树的输入内容是一个非常简单的 HTML 文件,然后经由 HTML 解析器解析,最终输出树状结构的 DOM,和 HTML 不同的是,DOM 是保存在内存中树状结构,可以通过 JavaScript 来查询或修改其内容。
DOM树构建完毕后就进入节点样式计算
样式计算style
样式计算的目的是为了计算出 DOM 节点中每个元素的具体样式,这个阶段大体可分为三步来完成。
首先是把 CSS 转换为浏览器能够理解的stylesheets,这个过程先把字节流转换成Tokenizing标记,根据标记创建节点,最后根据节点组成stylesheets,之后转换样式表中的属性值,使其标准化,因为CSS 文本中有很多属性值,如 2em、blue、bold,这些类型数值不容易被渲染引擎理解,所以需要将所有值转换为渲染引擎容易理解的、标准化的计算值,比如2em 被解析成了 32px,red 被解析成了 rgb(255,0,0),bold 被解析成了 700等等。最后计算出 DOM 树中每个节点的具体样式,比如继承规则,font-size的继承,color的继承,还有层叠优先级规则。这个阶段最终输出的内容是每个 DOM 节点的样式。
布局layout
计算出 DOM 树中可见元素的几何位置,我们把这个计算过程叫做布局。
首先是创建布局树,遍历 DOM 树中的所有可见节点,并把这些节点加到布局中;而不可见的节点会被布局树忽略掉,如 head 标签下面的全部内容,再比如 body.p.span 这个元素,因为它的属性包含 dispaly:none,所以这个元素也没有被包进布局树。之后进行布局计算,布局的计算过程非常复杂,我们这里先跳过不讲,等到后面章节中我再做详细的介绍。计算布局树节点的坐标位置。在执行布局操作的时候,会把布局运算的结果重新写回布局树中,所以布局树既是输入内容也是输出内容,这是布局阶段一个不合理的地方,因为在布局阶段并没有清晰地将输入内容和输出内容区分开来。针对这个问题,Chrome 团队正在重构布局代码,下一代布局系统叫 LayoutNG,试图更清晰地分离输入和输出,从而让新设计的布局算法更加简单。
分层layer
元素有了层叠上下文的属性或者需要被剪裁(文字溢出、文字内容有滚动条),满足这任意一点,就会被提升成为单独一层。
如一些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,为了更加方便地实现这些效果**,**渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。
图层绘制(paint)
渲染引擎把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表。所以在图层绘制阶段,输出的内容就是这些待绘制列表。
分块
当图层的绘制列表准备好之后,主线程会把该绘制列表**提交(commit)**给合成线程,绘制列表生成后,合成线程会把图层划分成一个个的图块。
栅格化(raster)操作
出于性能考虑,绘制出所有图层内容的话,会产生太大的开销,而且用户也不一定完全浏览完一个网页,所以合成线程会优先选择viewport视口附近的图块,把这些图块生成位图。栅格化就是把图块生成位图的过程。图块则是栅格化进行的最小单位,通常栅格化过程是用gpu加速生成的。
合成显示
一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,并把这个命令提交给浏览器进程,浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。
而我们平时说的重排,需要重新触发布局layout之后的一系列流水线,重绘则是跳过了布局和分层,从绘制开始触发后面的一系列流水线。而使用css3的一些属性,则是会直接跳过布局、分层、绘制的阶段,直接进入合成线程执行后面的一系列流水线。