文章内容较多,某些未涉及内容,可结合我的另一篇文章(https://www.jianshu.com/p/f8ae03a295a2)一起看,有意外收获.
一、进程和线程
进程是操作系统资源分配的最小单位,进程包含线程
线程是受进程管理的,浏览器采用的是多进程模式
日常中我们使用浏览器是基于一个一个tab页来进行访问网站,如果说某一个tab页挂掉了,对于其他tab页是没有任何影响的,说明每一个tab页都是一个单独的进程,他们之间相互独立,互不影响.
浏览器中的进程:
浏览器中的进程分为5个:
1.浏览器进程:你可以理解浏览器进程是一个统一的‘调度大师‘,去调度其他进程,比如我们在地址栏中输入url时,浏览器进程首先会调度网络进程,他可以做一些子进程管理以及一些存储的处理.
2.渲染进程:这个进程对于我们来说是最重要的一个进程,每一个tab页都有一个独立的渲染进程,他的主要作用是渲染页面.
3.网络进程:这个进程是控制对于一些静态资源的请求,它将资源请求完成之后交给渲染进程进行渲染.
4.GPU进程:这个进程可以调用硬件进行渲染,从而实现渲染加速.比如translate3d等css3属性会骗取调用GPU进程从而开启硬件加速.
5.插件进程:chrome中的插件也是一个独立的进程
各个进程之间是相互独立,互不影响的.
从输入url到页面显示之间究竟发生了什么?
https://www.jianshu.com/p/f8ae03a295a2
二、网络资源层面
首先我们先抛开浏览器对于资源的处理过程,先来看看一次正常的url输入在资源加载方面经历的生命周期.
当我们在地址栏中输入一个url时,浏览器进程会监听到这次交互.紧接着它会分配一个渲染进程准备渲染页面,同时浏览器进程会调用网络进程加载资源.
等网络进程加载完资源后会将资源交给渲染进程进行页面渲染.从进程的角度来说整体的加载流程就是这样.
大的方面来说就是浏览器进程进行调度,网络进程加载完资源后交给渲染进程进行渲染加载的资源.
接下来我们详细看看输入url之后的请求过程中究竟发生了哪些事情.
网络七层协议
我们可以将这七层归为下列四层:
应用层:通常我们会将应用层、表示层、会话层统称为应用层,应用层的主要协议是http协议.
传输层:传输层中我们浏览器中http协议是基于tcp去进行网络传输.(常见传输协议的有tcp还有udp)
网络层:网络层中一般都是ip协议.
物理层:当然在数据链路层和物理层都被称为物理层.
我们先从7层协议来分析一下浏览器对于url加载的过程
首先当我们输入url输入一个域名,浏览器会在磁盘/内存缓存中去查找请求的文件,查找是否命中缓存.如果命中缓存则直接会从缓存中拿到对应的ip地址.
如果命中缓存则会直接返回对应资源不会进入下面的步骤
浏览器缓存:https://www.jianshu.com/p/ccb9c60354a8
这里我们先忽略缓存带来的影响,这里涉及一个协商缓存和强制缓存的知识点在下面的知识点中进行讲解.
假设我们首次访问这个页面,此时并没有任何缓存:
如果我们访问的这个域名没有被解析过,那么我们需要解析地址栏中输入的域名.解析域名主要依赖的是DNS协议,将域名解析为ip地址. ip地址才能找到域名对应的ip.
dns你可以理解它为一个映射表,将域名和ip地址进行映射,其实就是一个分布式的数据库,通过域名查找到对应的ip地址.
需要注意的是dns解析是基于udp协议而非tcp协议.
这里有个小问题需要提一下,为什么dns解析时基于udp而非tcp协议?
我们的dns解析过程是一个服务器的查找过程.因为域名分为一级/二级...域名,所以每一级域名都会迭代去查询,如果它采用tcp协议的话,每经过一次域名查询,域名服务器都会经过三次握手.但是udp就不会,他会直接发包然后确认
相较于udp,tcp是更加安全,可靠的(因为三次握手以及四次挥手)但是这也造成了它相较于udp消耗更多时间.
udp常用的场景是视频或者直播中,对于我们来说dns解析中使用的udp更多的原因是因为udp的速度,当然即使丢包了,我们重新发送就可以了.
tcp传输的过程称为分段传输,也就是会拆分为多个包,一个包一个包的进行发送得到响应之后就会发送下一个包.这样的方式无疑更加可靠和安全,但是在实效上并不如udp协议的实时(直接通信无需建立连接)
此时会根据dns解析通过域名+端口号解析出对应的ip地址.
我们拥有了ip地址之后,接下来我们就需要将利用ip进行寻找网页地址
此时如果我们的请求地址时候https,在通过ip寻址之后会额外增加一步ssl协商保证数据的安全性
当IP地址寻址成功后,浏览器知道了服务器的地址,此时并不会立即将数据发送过去,而是会进入一个排队的等待过程,比如一个域名下有多个请求,同一个域名在http1.1下最多只建立6个tcp链接,也就说同一个时间最多发送6个请求,他们首先会进入一个排队的等待时间.
排队结束后开始发送请求.此时就需要通过tcp先进性创建链接通过三次握手,建立完成链接后传输数据.
上边我们说过tcp是基于分段传输的,基于内容特别大的传输内容tcp会将数据包进行拆分成为多个数据包进行有序传输.
在tcp的传输过程中如果传输中出现了丢包,那么tcp会进行重传.
服务器接收到之后会按照顺序进行接收.
tcp建立完链接之后,浏览器会通过http请求发送请求数据
一次http请求包含
请求行
请求头
请求体
在http1.1中默认开启了connection:keep-alive,它的作用是在下次发送请求时在一定时间内可以复用上一次的tcp链接而不需要重新建立链接.(也就是在一定时间内保持相同域名tcp链接不断开).
此时服务器接收到请求发送的数据,根据请求行、请求头、请求体进行解析,解析完成后返回相应行、响应头、响应体.
注意:这里服务器返回状态码中有一些特殊的状态码
301/302这两个状态码都是表示重定向,如果返回这两个任意一个,就会根据响应头中的location返回的域名重新进行之前操作(https://blog.csdn.net/qq_43968080/article/details/107355758)
304状态码表示浏览器本次资源走缓存而不会重新请求下载资源.
这个过程便是一个最基础的浏览器针对一个url访问网络请求的过程。
以taobao.com为例让我们一探究竟
上边说了那么多枯燥的理论,接下来让我们在实际中去体会一下。
首先我们打开一个全新的浏览器tab页在地址栏输入taobao.com
因为我是首次进入这个页面,所以并没有任何缓存。前边说到过浏览器进程首先会开启一个页面渲染进程,同时开启网络进程去请求。
首先让我们打开chrome开发者工具:
建议大家在新的无痕浏览页中去进行这些操作,我们排除掉DNS缓存以及任何浏览器缓存的干扰机制去看结果会更加纯粹。
每一次重定向都会重新进行DNS解析以及TCP连接的建立是非常耗时的。所以在我们的真实项目中要尽量的避免进行资源重定向,如果有存在重定向的资源尽量还是将它直接替换成新的地址连接。
接下来我们以第三次https://www.taobao.com/这次请求为例来分析一下一次请求(无任何缓存)的各个阶段:
分析一次请求完整的瀑布图所代表的含义
我们先来看看对应chrome中的瀑布图:
Queueing 这个阶段表示排队阶段,浏览器在以下情况下对请求进行排队:
有更高优先级的请求。
已经为此源打开了六个 TCP 连接,这是限制。仅适用于 HTTP/1.0 和 HTTP/1.1。
浏览器在磁盘缓存中短暂分配空间
Stalled 表示停滞不前,请求可能因上述排队中描述的任何原因而停止。(比如说链接开始后,会进行一些tcp连接的复用处理一些代理相关的逻辑)
DNS Lookup 这一步就表示开始进行DNS解析,将我们的请求域解析为ip地址。
Initial connection 这阶段表示我们进行tcp链接/重试和ssl协商共同耗费的时间。
SSL 这一步就是当我们请求https域名时会进行ssl协商的耗时。
request sent 表示请求开始发送
watting for server代表 Time To First Byte。此时间包括 1 次往返延迟和服务器准备响应所用的时间。通俗来说就是当我们请求发送到接受到响应的第一个字节的时间。
waitting for server 这一步通常可以粗略表示本次请求服务器(后台)从接受到请求然后返回响应结果处理的耗时。
Content Download 就不必多说了,是我们下载本地响应的时间。
同时对于chrome而言在http1.1下同一个域的最多支持并发6个TCP链接,注意这里是TCP链接而不是HTTP请求。
我们用一个小例子来说明下,在同一个TCP连接里面,先发送A请求,然后等待服务器做出回应,收到后再发出B请求。管道机制则是允许浏览器同时发出A请求和B请求,但是服务器还是按照顺序,先回应A请求,完成后再回应B请求(https://www.zhihu.com/pin/1681622411215228928?utm_id=0)。
http发展的各个阶段
http 0.9
最早时候只支持传输html,请求中没有任何请求头。
http 1.0
引入了请求头和响应头,这样的话就可以根据请求头区分传输的内容是图片还是html又或是js。
http 1.1
针对http1.0每一次请求都会发送请求建立tcp链接,请求结束后断开tcp链接。这无疑是非常耗时的。所以在http 1.1中默认开启了一个请求头connect:keep-alive进行在一个tcp链接的复用。当然即使引入了长链接keep-alive,还存在一个问题就是基于http 1.0中是一个请求发送得到响应后才开始发送下一个请求,针对这个机制1.1提出了管线化pipelining机制,但是需要注意的是服务器对应同一tcp链接上的请求是一个一个去处理的,所以这就会导致一个比较严重的问题队头阻塞。
如果说第一个发送的请求丢包了,那么服务器会等待这个请求重新发送过来在进行返回处理。之后才会处理下一个请求。即使浏览器是基于pipelining去多个请求同时发送的。
http 2.0
提出了很多个优化点,其中最著名的就是解决了http1.1中的队头阻塞问题。
HTTP/2复用TCP连接则不同,虽然依然遵循请求-响应模式,但客户端发送多个请求和服务端给出多个响应的顺序不受限制,这样既避免了"队头堵塞",又能更快获取响应。在复用同一个TCP连接时,服务器同时(或先后)收到了A、B两个请求,先回应A请求,但由于处理过程非常耗时,于是就发送A请求已经处理好的部分, 接着回应B请求,完成后,再发送A请求剩下的部分。HTTP/2长连接可以理解成全双工的协议
http 2.0 的特点:
多路复用: 支持使用同一个tcp链接,基于二进制分帧层进行发送多个请求,支持同时发送多个请求,同时服务器也可以处理不同顺序的请求而不必按照请每个请求的顺序进行处理返回。这就解决了http 1.1中的队头阻塞问题。
头部压缩: 在http2协议中对于请求头进行了压缩达到提交传输性能。
Server push: http2中支持通过服务端主动推送给客户端对应的资源从而让浏览器提前下载缓存对应资源。
http3.0:
基于tcp下就难免存在阻塞问题,如果发生丢包就需要等待上一个包。在http3彻底解决了tcp的队头阻塞问题,它是基于udp协议并且在上层增加了一层QUIC协议。
关于http 3.0和2.0这部分我研究的不是很多,所以就不做详细的对比了。大家如果有更详细的建议可以在评论区留言。后续如果有必要我会补充这部分内容。
关于http 1.1的pipelining机制和http 2.0的多路复用,他们究竟存在什么区别?
HTTP/1.0 without pipelining: 必须响应 TCP 连接上的每个 HTTP 请求,然后才能发出下一个请求。HTTP/1.1 with pipelining: 可以立即发出 TCP 连接上的每个 HTTP 请求,而无需等待前一个请求的响应返回。响应将以相同的顺序返回。
HTTP/2 multiplexing: TCP 连接上的每个 HTTP 请求都可以立即发出,而无需等待先前的响应返回。可分帧先按照顺序返回处理好的部分内容,接着返回下一个请求等等,在返回之前处理好没返回的内容
Summary:HTTP/1.1 最大的变化就是引入了持久连接(persistent connection),在HTTP/1.1中默认开启 Connection: keep-alive,即TCP连接默认不关闭,可以被多个请求复用。
那么管道机制就是在同一个TCP连接中可以同时发送多个HTTP请求而不用等待上一个请求返回数据后,再发送下一个请求。虽然可以同时发送多个HTTP请求,但是服务器响应是按照请求的顺序进行响应的。
HTTP 2.0的多路复用是在同一个TCP连接中,可以发送多个HTTP请求,而且请求的响应不依赖于前一个请求。每个请求单独处理,不会出现HTTP1.1中上一个请求没有回应便一直等待的情况。
三、浏览器渲染
首先我们先来看一看关于浏览器加载资源的粗略图
参考 https://zhuanlan.zhihu.com/p/575565899
1、渲染进程
渲染进程有GUI线程、js引擎线程、定时器触发线程、异步HTTP请求线程、事件触发线程,参考下图了解每个线程的作用,以及各个线程相互协作完成渲染的过程。
2、GUI线程的工作
渲染过程描述
解析html以构建DOM树 -> 构建render树 ->布局render树 -> 绘制render树
首先,当浏览器拿到html文档时首先会进行HTML文档解析,构建DOM树
其次,遇到css样式如link标签或者style标签时会解析css,构建样式树
HTML解析构建和css的解析构建是相互独立的并不会造成冲突,因此我们通常将css样式放在head中,让浏览器尽早解析css
再次,当html的解析遇到scrip标签,就会停止DOM树的解析,开始下载js
因为js是会堵塞html解析的,是堵塞资源,其原因在于js可能会改变html现有结构,将控制权移交给javascript引擎;等javascript引擎运行完毕,浏览器会从中断的地方恢复DOM构建。而因此就会推迟页面首绘的时间。可以在首绘不需要js的情况下用async和defer实现异步加载,这样js就不会阻塞html的解析了。
注意:异步执行是指下载。执行js时仍然会堵塞。当html解析完成之后,浏览器会将文档标注为交互状态,并开始解析那些处于defferred模式的脚本,也就是那些应该在文档解析完后才执行的脚本。然后,文档状态将设置为完成,一个加载事件将随之触发。
再一次,在得到DOM树和样式树后就可以进行渲染树的构建了
应该注意的是渲染树和DOM元素相对应的,但并非一一对应,比如非可视化的DOM元素不会插入呈现树中,例如‘head’元素,如果元素的display属性为‘none’,那么也不会显示在呈现树中(但是visibility属性值为hidden的元素仍会显示)
再一次,渲染树构建完毕后将会进行布局
布局使用流模型的layout算法。所谓流模型即是指Layout的过程只需要进行一遍即可。但实际实现中,流模型会有例外,Layout是一个递归的过程,每个节点都负责自己及其子节点的Layout。Layout结果是相对父节点的坐标和尺寸。其过程可以简述为:
(1)父节点确定自己的宽度
(2)父节点完成子节点放置,确定其相对坐标
(3)节点确定自己的宽度和高度
(4)父节点根据所有的子结点高度
最后,render Tree已经构建完成
浏览器渲染树引擎并不直接使用渲染树进行绘制,为了方便处理定位(裁剪),溢出滚动(页内滚动),css转换/不透明/滤镜,蒙版或反射,Z(Z排序)等,浏览器需要生成另外一棵树 - 层树。
因此绘制过程如下:
获取DOM并将其分割为多个层(RenderLayer)将每个层栅格化,并独立的绘制进位图中将这些位图作为文理上传至GPU复合多个层来生成最终的屏幕图像(终极layer)
3、问题知识点汇总?
(1)关于浏览器渲染的容易误解点总结
1、DOM树的构建是边加载边解析的.
DOM树的构建是从接收到文档开始的,一边会将字节转化为字符,字符转化为标记,标记构建dom树
这个构成被分为标记化和构建化
这是个渐进的过程,为达到更好的用户体验,呈现引擎会力求尽快将内容显示在屏幕上。它不必等到整个HTML文档解析完毕之后,就会开始构建呈现树和设置布局。在不断接收和处理来自网络的其余内容的同时,呈现引擎会将部分内容解析并显示出来。
参考文档:http://taligarsiel.com/Projects/howbrowserswork1.htm2、渲染树的构建是一边加载,一边解析,一边渲染?
这三个过程在实际进行的时候又不是完全独立,而是会有交叉。会造成一边加载,一边解析,一边渲染的工作现象。参考文档:http://www.jianshu.com/p/2d522fc2a8f83、css的标签嵌套越多,越不容易定位到元素
css的解析是自右至左逆向解析的,嵌套越多越增加浏览器的工作量,而不会越快。
因为如果正向解析,例如「div div p em」,我们首先就要检查当前元素到 html 的整条路径,找到最上层的 div,再往下找,如果遇到不匹配就必须回到最上层那个 div,往下再去匹配选择器中的第一个 div,回溯若干次才能确定匹配与否,效率很低。
逆向匹配则不同,如果当前的 DOM 元素是 div,而不是 selector 最后的 em,那只要一步就能排除。只有在匹配时,才会不断向上找父节点进行验证。打个比如 p span.showing
你认为从一个p元素下面找到所有的span元素并判断是否有class showing快,还是找到所有的span元素判断是否有class showing并且包括一个p父元素快
(2) css与js对于dom的影响
js加载和解析是会阻塞后续dom的解析。
css加载会阻塞页面的渲染。
css加载不会阻塞后续dom解析(https://blog.csdn.net/qq_41462647/article/details/130161709)
css加载会阻塞后续js的执行
css是否会阻塞Dom?
1.对于css的加载是不阻塞dom的构建的。
2.对于css的加载时会阻塞之后的dom节点的渲染的(需要把样式加载完之后再渲染)。js是否会阻塞Dom?
其实毋庸置疑,js的执行过程一定是会阻塞Dom Tree和Css OM的。
其实这里大家只要把握一个原则,在渲染进程中JS线程和渲染线程是互斥的关系。
为什么css放上边而js放在下面?
上边我们讲到了css的加载和解析并不会阻塞Dom的构建,但是会阻塞页面上之后元素的渲染。这也就造成了如果css放在顶部的话,后续Dom元素的渲染需要依赖本次css代码执行解析完成之后才会渲染。
将css放在底部的话页面的确是会产生两次渲染的。但是第一次没有任何样式的渲染其实是一次“无效渲染”。
我们利用chrome浏览器performance去分析将css放在底部的代码中发现实际上浏览器进行了两次元素的绘制,也就是说如果将css代码放在底部是会发生重绘(以及可能会引发回流),这个操作是非常耗时的一个过程。
所以将css放在顶部的话:页面首次渲染浏览器仅仅会进行一次渲染,而不会造成多余的重绘和回流步骤。
为什么js需要放在底部?
上边我们说到了关于js实际上是会阻塞Dom Tree的构建和渲染的。同时js依赖于前边的css文件加载完成后才会进行执行。保证js可以操作样式的。所以css之后如果存在js那么css的加载过程也是可以间接性的阻塞DCL事件的。
将js放在了元素之前,首先在js执行完成之前是不会进行后续元素的构建和渲染的。只有等待js加载并且解析完成之后渲染线程才会继续之后的Dom Tree的构建以及页面的渲染
这里额外有一点:在页面解析Html之前浏览器会额外扫描外部链接,将外部链接交给网络进程进行下载。所以css和js的下载可以是并行的。所以,我们之所以将js放在底部。是因为js放在底部是会等待页面渲染完毕后再去执行后续js。
defer和async这两个属性的不同
参考:https://juejin.cn/post/6894629999215640583
https://zhuanlan.zhihu.com/p/622763093
参考:https://zhuanlan.zhihu.com/p/458664335