特别感谢
参考视频:
本文是观看链接上的B站up主所总结的,再结合自身的理解,所写的一篇文字版整理,供自己以及各位看到的小伙伴在后续的学习中可以随时翻阅,文中会配上一些该视频上的图片来说明清楚某个概念,如果有不合适的地方请麻烦联系我删除,再一次感谢!
前言
在前端面试中经常会碰到该问题:输入网址后,发生了什么?这里其实可以从两个方面作答,一方面是从网络方面来阐述当你输入网址后,是如何取到服务端数据的;另一方面是从浏览器方面的角度来进行作答,即当你输入网址后,浏览器做了什么事,让你可以把从服务端取到的数据给展示到页面上。本文主要从浏览器的角度出发来进行描述。
解析
1. 网络
从网络方面的角度来看,以较为简单的流程来说,如下:
- [DNS解析] 输入网址后,DNS域名解析服务器进行解析。
- [寻址] 将解析得到的IP地址在网络中,向上级进行查找。
- [建立连接] 若能找到对应的地址,则与其建立连接;否则,向上层继续寻址,直到到NSP骨干网的路由器(拥有最大的路由表),查到地址后与其建立连接。
- [发起请求] 通过TCP的三次握手机制,建立连接后,将请求的数据包发给服务器;服务器接收到请求后,返回对应的资源给客户端。
通俗的例子
假设你需要与一位德高望重的老者通话,但你只知道他的名字(相当于域名),却不知道他的电话号码(相当于IP地址)
- [DNS解析] 你拨打了114,问客服说名字为这个的人,电话号码为多少?这时候客服查询之后,告诉了你他的电话号码。(名字 转换成 电话号码)
- [寻址] 坏的是,114没有告诉你这个电话号码的区号,你需要自己一个一个去找去试(寻址);如果实在找不到,你得到拥有最全的联系方式表的人那里去询问(即拥有最大路由表的NSP)那里去询问,完整的号码到底是多少?
- [建立连接] OK,当你得到完整的号码之后,这时候你就可以拨通电话那位德高望重的人,尝试与他建立起了连接。
- [发起请求] 当你拨通电话后,发现这位老者信号不太好,为了与他保持稳定的通话,你与他反复确认了彼此是否能听到彼此(TCP三次握手),直到你们确认完毕了(建立可靠连接后),就可以开始谈正事了。你与他交流的过程,也就是客户端与服务端进行请求与应答的过程。
2. 浏览器
了解完网络方面,我们再来聊聊浏览器方面会发生什么。
1)首先需要先了解下浏览器中的进程
。
早期旧的浏览器中,采用的是单进程
,那它有什么样的缺点呢?
- [不安全] 进程内可以通过 IPC 进行通信,因此 JS 可以访问页面内其他进程,导致私密信息泄露
- [不稳定] 由于 JS 运行要依赖主线程,当代码阻塞了主线程时,一个标签页的卡死会导致整个浏览器卡死
- [不流畅] 一个进程负责太多事情,性能降低
现代浏览器,采用的是多进程
,优点在于:
- [性能快] 由于分开进程处理各个事务,性能比单进程会快得多
- [稳定] 每个新打开的标签页都是一个独立进程,一个标签页的卡死不影响其他标签页的使用
通俗的例子
[单进程] 一个人的精力有限,如果你同时既做前端、又做后端、运维、数据库,那么你在一定时间内的效率并不快(不流畅);而如果你在其中一个环节卡住思路,那么你的其他工作都会受影响而被阻塞(不稳定);由于你在作为程序员的同时,你又拥有数据库的最高权限,这时候你便可以动歪心思,来窃取数据库中用户的资料(不安全)。
[多进程] 后来,你的老板多招聘了几个人,每个人分工明确,有人做前端,有人做后端,有人做数据库,并且都有单独的权限分配,各司其职。你们的工作可以并行开展,因此效率飞快(性能好);其中你们一个人如果请假了,也不影响其他人各自的工作(稳定性)。
2)了解完进程,我们再来看 浏览器
内部发生了什么。(下面的内容较难理解,可以打开链接中的视频,up主做了很详细的动画来帮助理解)
流程
-
[捕捉地址] 输入地址后,浏览器进程的UI线程捕捉输入的地址
-
[发送请求] 网络线程进行
DNS
解析,并将请求发送到服务端 -
[接收数据] 服务端返回资源(以
HTML
、JavaScript
和CSS
为例) -
[渲染器进程] 浏览器进程将返回的资源通过
IPC
管道传给渲染器进程 -
[DOM解析] 渲染器进程的主线程解析
HTML
文件,生成DOM
节点树
-
[样式计算] 浏览器解析
CSS
文件,进行样式计算,确定每个节点的样式 -
[布局] 根据
DOM
节点树和计算好的样式,生成LayoutTree
,来确定每个节点放在页面上的哪个位置及大小 -
[绘制] 遍历
LayoutTree
生成绘制记录表(Layer Tree
),确定以何种顺序来绘制节点(如z-index
) -
[分图层] 合成器线程按规则分图层,并分为更小的图块给栅格线程
-
[栅格化] 将绘制记录表的信息转化成
draw quads
图块信息(记录了图块在内存中的位置、摆放顺序、页面位置等信息,用户可以直接看到的部分),并传回给合成器线程 -
[合成] 合成器线程将图层进行组合,形成合成器帧,并传回给浏览器进程
-
[渲染] 浏览器进程再将帧传回给
GPU
进行渲染展示
注意
- [JavaScript 会阻塞主线程]
DOM
解析过程中遇到CSS
、Image
等资源不会阻塞HTML
加载,因为其不会出现在DOM
树节点中,而当遇到script
标签时,会停止解析DOM
,原因在于:浏览器并不知道这段JavaScript
是否会改变当前的DOM
树,因此会执行完再继续解析DOM
,这就会引起页面渲染的卡顿。 - [DOM和Layout树]
DOM Tree
和Layout Tree
不一一对应。如设置了display: none
的节点会出现在DOM
树上,而不会出现在Layout
树上;而设置了伪类如::after
的节点只会出现在Layout
树上,而不会出现在DOM树。原因如下:DOM
由HTML
解析获得,不关联样式Layout
树是根据DOM
和计算好的样式来生成
拓展
-
[重绘] 改变节点的
颜色
属性时,触发了样式的重新计算,但不触发布局和绘制,称为重绘。 -
[重排] 改变节点的
位置
属性信息时,会触发样式计算、布局、绘制及后面所有流程,称为重排。 -
[占用主线程引起卡顿] 绘制、布局、JavaScript 都会占用主线程来执行,在每一帧的有限时间内,如果绘制和布局结束后还有多余的时间,则
JavaScript
会利用空余时间来执行,如果JavaScript
执行时间过长,不归还主线程,会导致下一帧渲染延迟,我们都知道,60帧是肉眼看起来比较不卡顿的帧率,而下一阵渲染延迟,也就是会引起画面卡顿。可以通过requestAnimationFrame
的API来将JavaScript
分割成更小的任务以不影响每一帧的运行,如下两图所示:
-
[transform] 合成器线程和栅格化线程都不和 JS 抢占主线程。通过
transform
运行的动画只会在合成器和栅格化中执行,不会触发重绘和重排。
性能优化思路
- [避免重绘 & 重排] 尽量避免用 JavaScript 去改变
DOM
的样式、位置属性,操作量大时会引起页面卡顿。可以使用transform
进行代替。 - [避免JavaScript阻塞主线程] 使用
requestAnimationFrame API
分割JavaScript
任务,或用异步调用的形式,来让JavaScript
不阻塞主线程。