又是个老生常谈的问题辣。笔者记录一下自己的理解。
输入
首先,在输入的过程中,浏览器会实时捕捉输入的内容,如果输入的不是网址或者协议不合法的话,就会使用浏览器默认的搜索引擎,来合成新的带搜索关键字url,同时对非法字符进行转义,准备进行搜索。
接着回车前执行一次当前页面的beforeunload事件,可以让页面退出之前执行一些数据清理工作,或者,有表单没有提交的情况下提示用户是否确认离开。
接着浏览器主进程通过ipc(进程间通信)把url发送给网络进程。
重定向
页面不是首次访问的话,可能会出现重定向(注意,这是没有发起网络请求,是被缓存了的)。
如果使用了HSTS,检查HSTS(HTTP
Strict Transport Security),通过重定向的方式强制使用https进行通信。
检查缓存
接着网络进程检查缓存, 如果有缓存,并且没有过期,就不发送请求,直接解码再开始渲染流程。
缓存顺序:Service Workers---内存---硬盘---push Cache(http2,会话中,时间短)
url解析
从url中解析出域名,准备dns解析
DNS解析
客户端进行递归查询,浏览器缓存---本地host缓存---路由器缓存---ISP服务器
没有找到进行迭代查询,ISP服务器---根服务器---顶级域名服务器---次一级域名服务器。ISP服务器发送请求后,对应服务器返回响应给ISP服务器并告诉下一个服务器是哪个,ISP服务器再向下一个服务器发送请求。最终得到dns映射ip地址。
若没有配置cdn就直接返回解析到的ip给浏览器,否则,得到一个CName别名记录,它指向cdn网络中的智能dns负载均衡系统,然后负载均衡系统通过智能算法,将最佳的cdn节点ip返回。最后浏览器向目标主机发送请求。
建立tcp连接
TCP报文段格式
1.源端口和目的端口:各占两字节,分别写入源端口和目的端口。ip地址 + 端口号就可以确定一个进程地址。
2.序号/序列号:在一个tcp连接中传送的字节流中的每一个字段都按顺序编号。该字段表示本报文段所发送的数据的第一个字节的序号。初始序号称为ISN(Init Sequense Number)
比如一报文段的序号为101,共有100字节的数据。这就表明,本报文段的数据的第一个字节的序号是101,最后一个字节的序号是200。所以下一个报文段的数据序号应当从201开始,即下一个报文段的序号字段值应为201。
3.确认号ack:期望收到对方下一个报文段的第一个数据字节的序号。若确认好为N,则表示,到序号N-1为止的所有数据都已经正确收到。
4.数据偏移(首部长度):它表示tcp报文段的数据起始处距离tcp报文段的起始处有多远。这个字段实际上是指出tcp报文段的首部长度
5.保留段:占6位,应设置为0,保留为今后使用。
此外还有六个控制位,这是tcp用来说明该报文段性质的。
紧急位URG:当URG = 1时,表明此报文段中有紧急数据,是高优先级的数据,应尽快发送,不用在缓存中排队。该控制位需配合紧急指针使用(紧急指针指出本报文段中紧急数据的字节数)。
比如说,我们需要取消一个已经发送了很长程序的运行,因此用户从键盘发出中断命令。如果不使用紧急数据,那么这个指令将存储在接收tcp缓存末尾,只有在所有的数据被处理完毕后这两个字符才被交付接收方的应用程序,这样做就无法实现立即中断。
确认ACK:仅当ACK = 1时确认号字段才有效,当ACK = 0时确认号无效。tcp规定,在连接建立后所有传输的报文段都必须把ACK置为1。
推送PSH:当两个应用程序进行交互式的通信时,有时在一端的应用进程希望在键入一个命令后立即就能收到对方的响应。在这种情况下,tcp就可以使用推送(push)操作。这时,发送方tcp把PSH置为1,并立即创建一个报文段发送出去。接收方tcp收到PSH
= 1的报文段,就尽快地交付接受应用进程。而不用等到整个缓存都填满了后再向上交付。
复位RST:当RST = 1时,表明tcp连接中出现了严重错误(由于主机崩溃或其它原因),必须释放连接,然后在重新建立传输连接。
同步SYN:SYN = 1表示这是一个连接请求或连接接受报文。
当SYN = 1而ACK = 0时,表明这是一个连接请求报文段。对方若同意建立连接,则应在响应的报文段中使SYN = 1且ACK = 1。
终止FIN:用来释放一个连接。当FIN = 1时,表明此报文段发送的数据以传输完毕,并要求释放传输连接。
为什么要三次握手?
三次握手的目的是建立可靠的通信通道,只有三次握手才能确认双方的收发功能都正常,缺一不可。
第一次握手,客户端什么都不能确认;服务端确认自己接收,客户端发送正常。
第二次握手,客户端确认自己接收发送正常,服务端接收发送正常;服务端还是和第一次一样,确认自己接受正常,对方发送正常
第三次握手,服务端也确认了自己发送接收正常,对方发送接收正常。
三次握手
1.客户端向服务器发送SYN报文,一个客户端初始化随机序列号seq
2.服务器收到后向客户端发送SYN报文、ACK报文,一个服务端初始化随机序列号seq,一个确认号ack=客户端发来的序列号+1,表示自己收到了
3.客户端收到服务器的确认应答后,向服务端发送ACK报文,序号seq等于第二次握手服务端传来的ack,序号ack等于第二次握手服务端传来的seq+1
如果是http,则连接成功进入传输状态,如果是https,这是还需要一个TLS加密协议的握手过程。
https:
1.服务端发送数字证书(包含公钥,签名---由CA机构通过摘要算法生成服务器公钥的摘要(摘要算法指加密后有损无法还原的算法,例如md5)再由CA私钥加密摘要得到签名)。
2.客户端验证是否合法(浏览器自带CA机构的公钥,先用公钥解密签名得到摘要,再用相同的摘要算法加密服务器公钥看和摘要是否一致),不合法就发出警告。合法,生成一个随机数,用公钥加密,发送给服务器(非对称加密)。
3.服务端用私钥解密得到随机数,之后客户端与服务器通信都用该随机数加密。
协商缓存
浏览器发起请求,服务器解析,如果没有对应的资源则返回404。否则检查请求头有没有协商缓存的信息。如果验证缓存没有更新,返回304和空的请求体。
如果没有缓存或资源更新了,且没有cdn,返回200。读取完整请求并准备http响应,进行查询数据库等操作。
若连接的是cdn节点,若正好有该资源就直接返回。若没有资源或者资源更新了,cdn就去源站获取文件,源站也没有了,就404。有的话就返回给cdn缓存起来。
将响应的数据通过tcp连接,返回给浏览器网络进程。
浏览器接受到响应数据后,如果是http1.1以下直接关闭连接,否则浏览器默认保持连接(keep-alive)
关闭tcp连接
1.浏览器发送FIN报文,初始化序列号seq,此时客户端停止发送数据,但仍可以接收服务器响应的数据
2.服务器收到后,发送ACK报文,初始化随机序列号seq,序列号ack=客户端发来的seq+1
3.服务器数据发送完了,发送FIN、ACK报文,seq初始化随机序列号,序列号ack=客户端发来的seq+1(与第二次挥手一致)
4.浏览器收到后,发送ACK报文,序列号seq等于服务器传过来的ack,序列号ack=第三次挥手的seq+1
四次挥手完毕后,客户端等待2MSL后自动关闭,服务器收到ACK报文后立即关闭。
解析响应数据
如果返回的状态码是301或302就需要重定向到其他的url,重定向地址会在响应头的Location字段中,然后一切从头开始,否则根据情况关闭tcp连接或者保持连接。
响应成功,返回状态码2xx,判断资源能不能被缓存,如果可以,先缓存起来。
然后对响应解码,比如gzip压缩,根据资源类型(Content-Type)决定如何处理。如果浏览器判断是下载文件,那么请求就会提交给浏览器的下载管理器,同时url请求结束。
否则网络进程通知浏览器进程,浏览器进程会创建一个渲染器进程准备渲染页面。
渲染进程收到确认消息后,会和网络进程建立传输数据的管道,开始执行解析数据,下载资源等。
渲染---构建DOM树
构建dom树的流程:
1.由html解析器接收html,将原始字节数据转换成字符
2.根据html规范对字符词法分析,将字符解析成标记也称为令牌token
3.对标记进行语法分析,转成dom节点对象并定义属性和规则
4.解析器会维护一个解析栈,栈底为document,也就是dom树的根节点
5.然后根据节点对象关系按顺序依次向解析栈添加,形成dom树。
这个过程中,display:none的元素,script标签,注释也都会添加到dom树中
解析过程中遇到没有async和defer的script标签引用时,会暂停解析过程,通过网络线程加载文件,文件加载后切换至js引擎执行相应代码,代码执行完成后再切换回渲染引擎继续渲染流程
因为js可能会修改dom,所以js执行结束前,没有必要继续解析html。
img,video等标签不阻塞html解析。
有了dom树,还需要为每个dom节点计算样式。
渲染---样式计算
因为浏览器同样不认识css样式文本,所以渲染引擎拿到css之后,首先格式化css,将css转为一个浏览器认识的结构styleSheets。
有兴趣可以在控制台输入document.styleSheets查看。
再进行标准化操作,比如rem、颜色(blue、red)、字体(bold)转成统一的渲染引擎更容易理解的值。
css样式格式化和标准化之后,就可以计算每个节点的具体样式信息了
计算规则主要是继承和层叠
继承:每个子节点默认继承父节点的样式属性,如果没有定义样式,浏览器对每个节点添加默认的样式
层叠:是css的基本特性,css翻译过来就是层叠样式表,从这就可以看出来,默认情况下css是流式布局的,元素与元素之间不会重叠,可有一些情况下流式布局会被打破,比如浮动float,定位position,所以需要计算出哪些脱离了文档流的元素,并记住它们的层叠信息,以便后面进行分层
总之计算阶段的目的就是计算出dom树中每个节点具体样式
然后css解析和dom解析是可以同时进行的,但是script执行和css解析不能同时进行,css会阻塞js执行
因为js执行时可能在文档的解析过程中获取样式信息,如果样式信息没有加载和解析完毕,js就会得到错误的值。所以会延迟js执行
知道dom结构和dom树中元素的样式后,接下来需要计算dom树中可见元素的几何位置等信息,这个过程叫布局。
渲染---layout布局
创建布局树
1.遍历dom树中所有节点,将可见节点添加到布局树中,不可见的包括meta、script、display:none
2.然后根据dom结构和元素样式对布局树中的几何位置信息计算
计算过程非常复杂,如果直接按布局树的顺序渲染就会导致很多错误,比如说z-index很高的先渲染了,总不能被后渲染的给压制住了,所以还需要分层。
渲染----分层
按照层叠上下文进行分层,然后为每一个图层计算样式,把每一个图层的绘制拆分成很多小的绘制指令,生成绘制表,这个表记录了绘制的顺序和绘制指令。
渲染---栅格化
有了绘制表后,因为一个图层可能会太大,所以会将图层分成图块
将分割好的图块栅格化,栅格化之后的图块存储在GPU内存中
对不同的图块栅格化有优先级,出现在视口内的图块会优先栅格化
渲染---合成和显示
图块都被栅格化完成后,合成再显示到屏幕上。
当屏幕内容发生变化,比如滚动了页面。就会将栅格化好的图块再合成后渲染到页面上。
渲染之后---重排和重绘
重排也叫回流,就是改变一个元素的尺寸位置属性时,会重新进行样式计算,布局,绘制以及后面所有流程
重绘比如改变元素的颜色时,就会触发重绘,重绘不会重新触发布局,但还是会触发样式计算和绘制
所以重排一定会触发重绘,重绘不一定会触发重排
在页面首次加载时,必然会触发重排和重绘
tips---避免不必要的重排和重绘
重排和重绘都是运行在主线程上,而js线程也是在主线程上执行,就是出现抢占执行时间的问题
如果写了一个不断触发重排重绘的动画,那浏览器需要在每一帧都运行样式计算布局和绘制的操作
我们知道每秒60帧才不会让用户感觉到卡顿,如果在运行动画时还有大量js需要执行,因为布局绘制和js都是在主线程上运行的,不乏在一帧的时间内布局和结束后,如果还有剩余时间,js就会拿到主线程的使用权
如果js执行时间过长,就会导致下一帧动画开始js还没有执行完,而出现下一帧动画没有按时渲染导致动画卡顿的现象
1.通过requestAnimationFrame来解决这个问题
因为这个方法会在每一帧被调用,通过api的回调,我们可以把js运行任务分成一些更小的任务块(分到每一帧),在每一帧时间用完前暂停js执行,归还主线程,这样的话在下一帧开始时主线程就可以按时执行布局和绘制
react的fiber就是用到了这个api来做了很多优化
2.二是通过transform,栅格化的整个流程是不占用主线程的只在合成器线程和栅格化线程中运行
这也就意味着它不用js抢主线程,刚才提到反复重绘和重排会导致掉帧,是因为js阻塞了主线程,而通过css中的transform实现的动画不会经过布局和绘制,而是直接运行在合成器线程和栅格化线程中,所以不会受到主线程js执行的影响
最重要的是通过transform实现的动画由于不需要经过布局和绘制样式计算等操作,所以节省了很多运算时间。
使用transfrom转换为3d,增加一个图层。
will-change这个css样式也可以,但是有兼容性要求。
记录记录!