url输入到页面显示作为一个前端经典问题被许多文章分析过,但每次看都走马观花不得要领,正巧最近看了某付费专栏对于浏览器相关知识点讲解的十分到位,于是抽空自己做个总结与记录。
0. 前置知识
浏览器多进程架构
- 浏览器进程。主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
- 渲染进程。核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
- GPU 进程。其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。
- 网络进程。主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
- 插件进程。主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。
1. 用户输入
用户在地址栏输入一串字符,地址栏会判断输入关键字是搜索内容还是合法的URL
- 如果是搜索内容,浏览器会使用默认的搜索引擎,来合成带搜索关键字的URL并开始导航
- 如果是URL,会根据规则加上协议,如baidu.com -> www.baidu.com
之后当前页面会调用beforeunload事件,可以执行一些数据清理操作、表单提交确认等。
如果页面没有监听beforeunload事件或者事件被确认则进入下一流程。
以上流程均在浏览器进程进行,接下来要开始在网络进程发起真正的URL请求
2. URL请求
浏览器进程提供进程间通信(IPC)把请求URL发送至网络进程。
2.1 检查缓存
- 此时浏览器会先检查缓存,如果URL命中缓存则直接返回缓存资源。(@shuangyu todo)
- 如果无缓存命中则进入DNS解析阶段
2.2 DNS解析
以下流程如果上一步没有查询到ip地址,则向下继续。
- 读取浏览器的DNS缓存
- 读取客户端系统缓存(包括hosts文件)
- 客户端向 本地域名服务器(一般是由接入的电信运营商提供,以下简称ISP)查询
- 如果还查不到,则由 ISP 向 跟域名服务器查询(全球仅有13台根域名服务器,1个主根域名服务器,其余12为辅根域名服务器)
- 根域名服务器会根据查询的域名返回所查询域的dns服务器地址
- ISP 继续查询跟域返回的服务器地址
- 重复第6步,知道找到正确的记录(注意重复的步骤全部都是由ISP发起的,而不是客户端)
这里放一张很著名的图(已经不知道传播了第几手)
- 客户端向ISP的查询叫 递归查询,特点是如果查不到,则后续的查询都由ISP来发起(递),如果查到了则返回给客户端(归)
- ISP 向根域名服务器的查询叫 迭代查询,特点是每次都是被查询的服务器告诉 ISP 下一个服务器地址,然后由ISP 来发起查询请求
参考资料:
2.3 TCP连接
在开启新的TCP连接之前,浏览器会检查当前域名下tcp连接数是否已达到最大6个的限制,如果达到则需要排队。
三次握手的过程为:
- 客户端随机生成一个 sequence number = client_isn,并发送SYN报文到服务端,发起连接(客户端进入 SYN_SENT 状态)
- 服务端发送 SYN + ACK = client_isn + 1,也随机生成一个 sequence number = server_isn 并发送(服务端进入 SYN_RECV 状态)
- 客户端发送 ACK = server_isn + 1(客户端发送后进入ESTABLISHED,服务端收到后也进入ESTABLISHED)
这里解释下两个经典的问题:
Q:为什么需要三次握手?
A:因为TCP是双向的字节流传输协议,通信双方都需要知道对方能正确发送和接受数据,所以都需要给对方发送SEQ和确认接收的ACK,其中一次的SEQ和ACK可以合并一起发送,所以最终表现为三次。(这里如果对比四次挥手其实更容易明白)
Q:为什么不需要四次、甚至五次?
A:因为三次握手之后双方已经确认对方有发送和接受的能力,无需进行更多次的握手。
这个问题的本质是 信道不可靠,三次通信是理论上的最小值,所以三次握手不是TCP本身的要求, 而是为了满足"在不可靠信道上可靠地传输信息"这一需求所导致的。
参考资料:
2.4 TLS握手(如果是HTTPS页面)
1. 缘起
首先为什么需要引入HTTPS,因为HTTP(明文传播)的风险有三点:
- 窃听风险:第三方可以获知通信内容
- 篡改风险:第三方可以修改通信内容
- 冒充风险:第三方可以冒充他人身份参与通信
总的来说,安全层有两个最主要的职责:对发起的请求进行加密、对接收的响应进行解密。
2. 前置知识
对称加密:加密和解密使用相同的密钥
非对称加密:加解密使用不同的密钥,一把公钥一把私钥。公钥加密的信息 只有私钥才能解密,反之 私钥加密的信息,只有公钥才能解密
3. TLS握手过程
上图中红色部分即为握手阶段(handshake),可以看到握手涉及 4 次通信。
-
客户端发起请求(ClientHello)
- 客户端生成的随机数 client-random
- 支持的加密方法(如RSA公钥加密)
-
服务端回应(ServerHello)
- 服务端生成的随机数 client-random
- 确认使用的加密方法
- 数字证书(其中包含公钥)
-
客户端回应
首先验证服务器证书。如果证书验证不通过会向访问者显示一个警告,由其选择是否还要继续通信(关于数字证书的申请、分发、验证、信任链是另一块比较大的知识点,在此不做详细讨论)
如果证书没问题,客户端会从证书中取出公钥,然后再生成一个随机数 pre-master,并由公钥加密后发送。
- 公钥加密的 pre-master
- 客户端握手结束通知,表示客户端的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供客户端校验。
-
服务端最后回应
- 服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供客户端校验。
在第4步服务端收到 pre-master 之后,两端都同时拥有了共同的 client-random、server-random 和 pre-master三个随机数,两端使用这三组随机数生成对称密钥,有了对称密钥后客户端与服务端进入后续的对称加密通讯。
参考资料:
最后我们来看一个真实案例,浏览器输入baidu.com,html请求依次包含了以上所说的步骤,关于Queueing、Stalled、Proxy negotiation等其他几个时间指标的官方解释在这里。
2.5 处理响应
1. 重定向(301、302)
在接收到服务器返回的响应头后,网络进程开始解析响应头,如果发现返回的状态码是 301 或者 302,那么说明服务器需要浏览器重定向到其他 URL。这时网络进程会从响应头的 Location 字段里面读取重定向的地址,然后再发起新的 HTTP 或者 HTTPS 请求,一切又重头开始了。
2. Content-Type决定处理方式
浏览器对不同Content-Type的响应有不同的处理方式,举个🌰:
如果 Content-Type 字段的值为 application/octet-stream,会被浏览器判断为下载类型,那么该请求会被提交给浏览器的下载管理器,同时该 URL 请求的导航流程就此结束。但如果是 HTML,那么浏览器则会继续进行导航流程。
3. 准备渲染进程
关于浏览器多进程架构可以看下这篇文章(@shuangyu todo),默认情况下Chrome中每个标签页对应一个渲染进程,但也有一些特殊情况。
首先了解一个概念叫 same-site,同时满足 根域名相同 和 协议相同 两个条件即可。以下三个URL就符合 same-site 的定义。
https://i.baidu.com
https://www.baidu.com
https://www.baidu.com:8080
Chrome的默认策略是,每个标签对应一个渲染进程。但是如果从一个页面打开了新页面,而新页面和当前页面属于同一站点时,那么新页面会复用父页面的渲染进程(process-per-site-instance策略)。
实测只有直接点击链接会复用渲染进程,选择新标签页打开还是会开两个进程。
4. 提交文档
提交文档是指浏览器进程将网络进程收到的html响应数据发送给渲染进程。提交文档的流程如下:
- **发起消息:**浏览器进程接收到网络进程的响应头数据后 -> 向渲染进程发起 提交文档 的消息
- **建立管道:**渲染进程接收到“提交文档”的消息 会和网络进程建立传输数据的 管道
- **确认提交:**等文档数据传输完成之后,渲染进程会返回 确认提交 的消息给浏览器进程
- **更新页面:**浏览器进程在收到 确认提交 的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新 Web 页面
这也就解释了回车后页面不立即更新的问题,浏览器进程需要等到渲染进程返回 确认提交 消息后才会更新各种导航状态,告诉用户下一个页面要开始渲染了。
5. 页面渲染
首先说明页面渲染的大部分内容和插图都来自极客时间的 浏览器工作原理与实践 课程,这一部分也称渲染流水线,页面渲染过程对于前端页面的优化有很大的指导意义。
5.1 构建DOM树
DOM树构建由渲染引擎内部的HTML 解析器(HTMLParser)模块负责,输入为html字节流,输出为树形结构的DOM节点,开始加载html文件时,网络进程和渲染进程之间会建立一个共享数据的管道,字节流边加载边解析,而不是等全部加载完成之后再解析。
字节流转换成DOM树的过程:
-
通过分词器将字节流转换为Token
-
浏览器会维护一个 Token 栈结构通过不断的入栈出栈将Token转换为DOM树,具体流程为,Token不断入栈:
- 如果压入到栈中的是 StartTag Token,HTML 解析器会为该 Token 创建一个 DOM 节点,然后将该节点加入到 DOM 树中,它的父节点就是栈中相邻的那个元素生成的节点。
- 如果分词器解析出来是文本 Token,那么会生成一个文本节点,然后将该节点加入到 DOM 树中,文本 Token 是不需要压入到栈中,它的父节点就是当前栈顶 Token 所对应的 DOM 节点。
- 如果分词器解析出来的是 EndTag 标签,比如是 EndTag div,HTML 解析器会查看 Token 栈顶的元素是否是 StarTag div,如果是,就将 StartTag div 从栈中弹出,表示该 div 元素解析完成。
需要注意的是,如果上述过程遇到script标签,会暂停DOM解析,我们假设该标签是一个非异步的外联js,那么会有一个比较模糊的问题是:该js的下载会阻塞DOM解析吗?执行会阻塞DOM解析吗?
结论是js的下载和执行都会阻塞DOM解析,对于下载过程,Chrome对此做了优化,当渲染引擎收到字节流之后,会开启一个预解析线程,用来分析 HTML 文件中包含的 JavaScript、CSS 等相关文件,解析到相关文件之后,预解析线程会提前下载这些文件,这个优化叫 预解析,对于这个问题有个知乎回答很不错:🔗链接。
5.2 构建CSSOM树
1. 生成 CSSStyleSheet
一个页面的css来源有三个(其实还有默认样式 UserAgent),见下图:
可以看到这些来源其实都是字符串的形式,浏览器需要把它们转化成自己能够理解的内容,即 CSSStyleSheet 对象。
可以看到document.styleSheets打印出一个由 CSSStyleSheet 组成的数组
2. 标准化属性值
其实就是把一些语义化的描述转化成更加标准的度量单位。
3. 节点样式计算
一个节点的样式可能来自于任一个css来源,并且存在 CSS 的继承、层叠、优先级计算等规则,浏览器最终会计算出每个节点的最终样式,可以通过chrome dev tool的 Computed 标签查看最终应用到具体DOM节点的样式。
5.2 布局阶段
目前我们有了 DOM 和 CSSOM,但这还不足以显示页面,接下来需要 计算DOM树中可见元素的几何位置。
1. 创建布局树
浏览器会把DOM 和 CSSOM 结合生成渲染树(Render Tree,有的也叫布局树,这里采用Google开发者文档里的叫法)。
图片出处:developers.google.com/web/fundame…
2. 布局计算
现在我们有了渲染树,需要计算出每个节点确切的坐标。影响布局的因素有很多,比如布局类型(正常流式布局、Flex布局、Grid布局),盒模型(Content Box、Border Box)等,布局计算的具体过程非常复杂,有兴趣的同学可以自行研究。
5.3 分层
我们都知道 css 其实可以描述很多复杂效果,包括各种3D变幻、页面滚动、悬浮等,所有其实页面在底层并不是一个二维空间,而是一个有 z 轴的三维空间,我们作为观察者看到的是所有可见图层的叠加效果。一些满足了特定条件的元素会被绘制在单独的一层,具体的可视化效果我们使用 chrome dev tool 的 Layers 工具去查看当前页面的图层关系。
通常满足下面两点中任意一点的元素就可以被提升为单独的一个图层:
-
拥有层叠上下文属性的元素会被提升为单独的一层,关于层叠上下文的详细规则可以看这里:🔗链接
-
需要剪裁(clip)的地方也会被创建为图层,这里可以简单理解为如果元素内部发生溢出,即产生了裁剪
5.4 绘制
绘制阶段其实并没有执行真正的绘图操作,而是 对每一个图层生成由一个个绘制指令组成的绘制操作列表,当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程去执行真正的绘图操作(这也是为什么经常主线程卡住了,但是 CSS 动画依然能执行的原因)。
5.5 栅格化
一个页面可能比浏览器视口要大得多,从效率的角度来说浏览器没有必要提前绘制那些我们看不到的区域,事实也是如此,合成线程会将页面划分为多个图块,并且优先将视口附近的图块生成位图。
所谓 栅格化 其实就是图块生成位图的过程,渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的。通常栅格化的过程会调用GPU加速,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中,其中涉及到跨进程通信。
5.7 合成与显示
这里需要先了解一下显示器是如何显示图像的。每个显示器都有固定的刷新频率,通常是 60HZ,也就是每秒更新 60 张图片,更新的图片都来自于显卡中一个叫 前缓冲区 的地方,显示器每秒固定读取 60 次前缓冲区中的图像,并将读取的图像显示到显示器上。
接着上述的渲染流水线,一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。
浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,显卡会合成图像并且保存到显卡的 后缓冲区 中,一旦显卡把合成的图像写到后缓冲区,系统就会让后缓冲区和前缓冲区互换。进而显示器就能够读取到新的图片。
至此我们已经分析完整个渲染流程,下面一张图可以很好的概括: