综述
以Chrome浏览器为例,Chromium多进程架构模型在打开浏览器的时候,就会启动浏览器进程(用以管理其他进程和维护浏览器的状态),多个网络进程,多个渲染进程,GPU进程,插件进程。在一个tab页,一般来说都有一个渲染进程,但是也可能出现一个tab页中多个渲染进程和多个tab页共用一个渲染进程的情况。我们先大致说明浏览器从输入URL之后的过程,随后再进行每个模块的进一步讨论。
整体流程
大致简明流程如下:
1 交付给网络进程处理
2 若是域名,则首先进行DNS解析工作(无处不在的缓存,资源缓存,DNS缓存等<1>);按照Url协议头进行HTTP连接建立(HTTP的队首阻塞<2>,HTTP2的优势等)
3 获取到对应数据,我们以HTML资源为例(Content-type识别资源类型和415的状态码<3>),网络进程下载文件到浏览器内存中,随后IO进程通知渲染进程进行渲染(进程间通信<4>)
4 渲染进程接受到HTML,就调用对应加载器进行网络进程下载下来的资源的加载(load<5>),随后调用解释器进行解释工作(词法分析)。html从上往下进行解析工作,其中同样会涉及到JS和CSS的加载和解析(<6>)
5 head头部处理完之后,就正式进入DOM树的解释创建过程了,这里html解释器会先进行token词语分割工作,也就是tokens的生成,之后在tokens的基础上再计算出来dom的树形结构数据,以chrome为例,这里也涉及着两个算法:符号识别算法(The tokenization algorithm)和 树的构建算法(Tree construction algorithm)(<7>)。我们知道浏览器渲染进程处理的渲染,然而在解析构建cssom和dom的时候确实并行的,这里主要的原因可以看成css是并不会影响dom树的结构的,但是在进入render tree的合并的时候,cssom又是阻塞的。同样的css由css加载器加载,css解析器解析,历经转化->词法分析tokens->cssom树形数据构造的流程,css我们在书写的时候就是仿照对象的格式书写的,所以这里在tokens分割的时候就较简洁一点了,我们既然说是tree数据结构,那么浏览器是怎么计算出来css中声明的各项样式的父子关系的呢?这里我们可以大胆假设一下,我们知道通常在书写css样式的时候,针对css的选择器一般是id class tag等,这里伪类和伪元素先不说,毕竟其前面书写的时候紧紧跟着作用的元素的,我们按照上述三种常用的选择器来进行假设,cssom可以是多个树吗?或者是多个数据结构表示的吗?像id声明的样式,我们完全可以使用一个Map或者Set的数据结构进行构建数据的嘛,class为主的样式在声明的时候我们如果按照子类选择器的书写来看,class的样式确实可以形成以body为根节点的树形结构数据的,但是我们如果都书写成单个class的样式呢(重复率不高且优化css文件大小),这样理论上来说我们也是只能得到一个高度为2的树了。再说tag,和class同理,如果我们不使用嵌套的格式和来,那么处理过程和class可能是一致的,这可能也是css提倡按照子类选择器的格式书写的原因吧。这样看来我们所说的css树完全没有什么优势了嘛,时间复杂度O(N)的一个查找过程显然是劣于Map/Set的O(1)的查找的。假设之后我们就需要进一步反问,css构建的数据结构真的是树吗?这里感谢 https://zhuanlan.zhihu.com/p/25380611 的源码层面的css介绍,可以看到根据css文件构造出来的是 id、class、标签、伪类选择器 的四个Map,确实在查找多的场景下,适当的牺牲一点空间能换取时间加速也是极好的,一切为了速度[/笑哭],dom树的样式赋值的速度。
6 终于经过dom树和css样式Map同时都构建完成之后,就进入了render tree的合并阶段了,理论上来说这里就只是一个树的遍历和Map的key索引和render tree节点的创建过程了
7 render tree完成之后,主线程就进入到了布局计算的阶段了(存疑:render tree不能和layer cal同步进行嘛),这个阶段的产物就是render layer tree。布局计算顾名思义就是确定每个dom的位置和大小(回流和重绘<8>),此时的依据就是盒模型,盒模型分为块级元素,行内元素,替换元素,非替换元素和伸缩盒元素,替换元素指的就是以img video为例的元素,它们的位置定位是可以忽略src的,在资源准备好渲染的时候直接替换元素的盒内容;非替换元素一般就指span这类的文本元素。我们先战术忽略box-sizing的对标准盒子和怪异盒子的区别,块级元素在宽度计算上,默认是充满父盒子的,同时css显示声明的话宽度就按照声明的来,当然单位一定要是浏览器能识别的: px em ex ch等,而块级元素的高度呢,则是有声明按照声明,没有的话按照盒子内元素的高度叠加计算和最大值取值得到的。行内元素的宽度计算则是依赖于字体的字宽大小和white-space计算出来对应的em,高度则是依赖于字体的fontsize,宽高都是依赖字体(行内元素)。说到行内元素,就不得不提到input,img这种行内元素了,在上面也说到过,其实替换元素,可以设置宽高的,所以在计算宽高的时候也是类似块级的计算盒子的尺寸的,不过又因为其替换行为,就可能导致图片移除盒子的现象,不过计算溢出盒子也是不影响后续布局的,这里侧面说明替换元素的盒子计算不是依赖替换之后的资源的尺寸的。宽高的计算可以按照上述的表述,而布局的计算则不得不提到文档流和文本流的概念了,渲染线程在计算布局的时候,对不加改变声明的元素是按照文档流从上往下,文本流从左往右进行布局计算的,如同上述所说的块级元素独占一行,多个行内元素在同一行布局,既然说到文档流,那么就当然有脱离文档流的方式了,这里我们大致提一下:fixed、absolute,float设置后都可以脱离当前的文档流,对应的元素在计算的时候就要重新选择基准点的位置进行布局了。同时在布局计算中,也涉及到上下文的切换和不同的计算模式:BFC(块级格式化上下文 Block Formatting Context),IFC(行内格式化上下文 Inline Formatting Context),这两种不同的格式化上下文在计算盒子的大小和边距上有着对应的规则,BFC中元素是块级元素,当然默认是按照文档流进行竖直的排列,竖直方向上的边距通过marigin来进行设置,同时存在着边距重叠的现象;排列的时候第一个块级元素的marigin的左基线是父级元素的border-left;针对float可以避免其重叠,同时高度计算上也是受内float元素的高度影响的;同时对布局上比较重要的特性:BFC是块级隔离域,在计算外盒子的大小和定位的时候,内层的排列方式可以不用考虑的。那么怎么设置成BFC呢:float、position不是static和relative的、display的值是inline-block、table-cell、flex、table-caption或者inline-flex、overflow不是visiable的都可以将外层的盒子声明成一个BFC,看到这些设置,我们也会意识到,浮动元素在丢失高度的时候为外层设置overflow为hidden的时候可以指定出来高度的依据了。当然IFC对于行内元素的布局计算也是有着重要的作用的,IFC外层同样是一个块级区域,里面的行内元素是按照行one by one的排列的,当出现一行放不下的时候,就拆分成两个块级区域,分为两行展示,同时两行之前没有间隙,总而言之,行内元素显示的时候默认是按照"抱团取暖"的,只有开发者可以拆散[/尴尬],值得一提的是,面试的时候可能也会问到,行内元素可以设置margin,padding和border吗?从上面的BFC和行内元素尺寸计算中,我们可以得出,marigin在左右上是可以设置的,也是生效的,padding同样在左右上是会生效的,上下方向设置了也是可以看出来效果的,但是并不能影响外层盒子的高度的,border是可以生效的。自此layer树开始构建了,这里同样要提到一下层叠上下文的概念,以webkit为例,其在渲染的时候本身就采用的是分层构建渲染最终合并重叠显示的机制的,布局树在形成的过程中,主线程也同时创建了一个虚拟的绘图上下文开始进行层级的绘制工作,这里提一下为什么要分层进行绘制,第一我们在编写代码的时候可能用到z-index的css属性,这时候浏览器为了实现层叠的效果,是需要将元素绘制在单独的一层的,这样方便后期的层级调整,同样我们如果设置了translateZ的时候,当前元素可能在Z轴发生变换,也是需要单独绘制成一层的,这里就解释了为什么要用transfom来替换top等的第一个要点了,可以绘制成单独的一层,那么GPU加速呢?GPU主要是用于多层级的合成图像的阶段。好了。经由上述的虚拟的绘图上下文的努力之后,最后就是生成对应的图像了。这里的绘图实现是很复杂的过程,浏览器为了更快的能显示出来图像也进行了大量的优化实践。ok绘图渲染这里,我们可以发现,性能优化中可视区的更快渲染是及其重要的,这里提一下css的一个属性content-visibility,设置后可以让元素不出现在屏幕上的时候先跳过对元素的计算和渲染,这样也可以加快渲染的速度。同样的使用tranform和opecity等都可以将元素提升成单独一层并且避免回流。虚拟上下文生成图像之后就开始显示到浏览器了。
综上所述,浏览器从url到页面图像的显示的大致过程就理清楚了,让我们简化一下整个流程:
网络相关(DNS解析,HTTP应用层链接,构造TCP链接,接受HTML流) -> HTML解析过程(HTML首先是字符串流,token分割,dom树创建,遇到内联css就加载和解析,遇到不修饰的js就阻塞加载解释,cssMap构建,render tree合并(样式解析赋值),布局计算layer tree,绘制图像,层组合,显示图像),这就是浏览器第一次进行加载的大致流程了。
标记点知识
<1> 浏览器实现的缓存
1 DNS缓存:需要解析DNS的地方就会有缓存的存在,本地建立DNS缓存可以有效加快网站的连接速度,可以通过chrome://chrome-urls/查看到其本地的cache
2 浏览器资源缓存:强制缓存和协商缓存,无状态的http协议想要确定沟通缓存,就需要在headers中附加上相关信息,大致相关的字段是:Request中的Cache-Control、If-Modified-Since和Response中的Cache-Control、Expires、Date、Last-Modified、ETag。
强制缓存: 强制缓存http1.0使用的是Expires进行控制的,其表示资源到期时间,浏览器在识别资源未达到过期时间的时候,就不会发送请求到服务端;http1.1中使用Cache-Control来进行控制,主要因为Expires对比的时间是本地和服务器的时间,可能存在本地时间被篡改导致缓存不会命中的情况。Cache-Control是多字段组成的,主要是max-age控制过期时间,public/private控制能否被代理缓存,no-cache使用缓存前去校验,no-store每次都去服务器请求。在使用max-age的时候,浏览器在倒计时范围内也是不进行资源的请求的。强制缓存是有着极好的节省带宽和较少请求的特点的,因为在命中后就连请求都不发出了,只在内存中或者硬盘上读取数据,其缺点也很明显,在服务端资源发生改变的时候,客户端未到到期时间是不会主动去拉取最新数据的。
协商缓存:每次请求实际都是要到达服务端的,但是服务端会决定是否返回数据,节省的是响应数据的body体的传输带宽。协商缓存主要由Last-Modify/If-Modify-Since和ETag/If-None-Match控制。Last-Modify/If-Modify-Since代表的是上次修改时间,服务器响应时会加上文件的最后修改时间Last-Modify,服务器接受响应的时候会获取If-Modify-Since识别浏览器上的资源的修改时间是否发生改变,改变的话会返回200和最新的Last-Modify,不改变的话就返回304;同理ETag也是类似的原理,Etag主要解决上述修改时间可能修改内容后不发生改变的问题,其由文件的inode,文件大小,文件上次修改时间进行防碰撞的hash后的值。inode是linux上识别文件的编号,使用vim会生成swp副本在保存后覆盖同名文件从而改变inode,这里在docker上是有这个问题的,在docker run -v 将linux上的文件挂载到容器中后,在物理机上vim文件容器是响应不到变化的,但是使用echo和sed是可以响应变化的,这里主要就是因为容器挂载进去的其实是根据inode和响应改变的,vim之后文件的inode就发生改变了,自然传递不了历史inode的内容变化了,而echo和sed是内容改变,不会进行inode的修改。
<2> 队首阻塞
浏览器在实现的时候,按照http1.1协议来看,Crome同一时刻支持的是6个并发连接,第七个开始就需要阻塞着等着前六个连接的释放了,故在页面实现的时候,尽可能的控制资源的网络请求数量也是优化中的一部分。不过在http2.0实现上,这个问题就已经不会再出现了,下面简单介绍下http2.0的提升
1 IO多路复用:http1.1虽然已经较http0.9实现了基于keep-alive为主的tcp连接复用,但是在http请求响应模型和串行文本传输数据下,http报文的传输还是一问一答的形式的,后面的请求在共用的tcp连接上也是不会先于前一个完成的,http2.0引入了二进制数据帧和流的概念,在tcp上请求是基于流传输的,同时二进制数据帧进行了顺序标识,使得流数据传输可以无序传输,在另一端会进行顺序合成数据的,使用现代计算机的处理器能力来降低数据传输中的等待和耗损。
2 首部压缩:http协议在传输短数据的时候,头部的占用字节是远远大于数据的占用的,虽然浏览器可能也会实现http传输的等待和聚合,但是在两端每次请求都要携带大量字节的头部信息也是有着很大的损失的。http2.0使用HTTP2头部压缩算法对头部信息进行编码,并在http的客户端和服务端同时维护headers对应的hash表,这样在传输中就只用传输对应hash表中的key值就可以了。经典的空间换时间啊。
3 服务端推送:http2.0帧传输同样使得一个请求可以有多个响应。利用服务器推送可以对页面进一步优化,在html获取的时候,服务端提前解析到html中需要加载的其他资源(js css等),主动推送给浏览器。
<3> Content-type
这里引用MDN上的解释
The Content-Type representation header is used to indicate the original media type of the resource (prior to any content encoding applied for sending).
In responses, a Content-Type header tells the client what the content type of the returned content actually is. Browsers will do MIME sniffing in some cases and will not necessarily follow the value of this header; to prevent this behavior, the header X-Content-Type-Options can be set to nosniff.
这里说明下415的情况,Unsupported Media Type 在python中使用request包发送请求的时候,如果不手动设置headers中的Content-type的时候,很容易获取到这个返回结果,其解决方式其实就是在headers中指定Media Type。
下述未完 待补充中