从输入URL到页面展示,中间发生了什么
地址栏输入URL
检查URL
浏览器的浏览器进程检查URL,组装协议,构成完整的URL,通过进程间通信IPC把URL请求发送给网络进程
发起http请求
网络进程接收到URL发起http请求
网络进程首先查找本地是否存在缓存
如果没有缓存就进行网络请求
网络请求首先进行DNS解析,DNS查过过程也存在缓存的问题,最终拿到目标服务器的ip
网络进程通过ip地址和服务器建立tcp链接
网络进程构建请求行,请求头,请求体,发起网络请求
拿到响应后
浏览器根据相应的状态吗判断是否进行重定向
浏览器根据相应的content-type决定如何显示相应的内容
如果是text/html,浏览器为每个页面分配一个渲染进程,如果属于same-site(根域名和协议完全一致就属于same-site),就在同一渲染进程进行渲染。
渲染进程准备好之后浏览器进程发出提交文档指令,这里的文档指的是响应体数据,渲染进程收到指令后建立和网络进程的数据管道
等文档数据传输完成后,渲染进程返回确认提交的消息给浏览器进程
浏览器进程收到确认提交消息后,更新浏览器的界面,包括前进后退的历史状态,安全状态,地址栏的URL,和正真的web页面
文档提交后渲染进程开始页面解析和子资源加载
渲染流程:
构建 DOM 树
输入:HTML 文档;
处理:HTML 解析器解析;
输出:DOM 数据解构。
样式计算
输入:CSS 文本;
处理:属性值标准化,每个节点具体样式(继承、层叠);
输出:styleSheets(CSSOM)。
布局(DOM 树中元素的计划位置)
DOM & CSSOM 合并成渲染树;
布局树(DOM 树中的可见元素);
布局计算。
分层
特定节点生成专用图层,生成一棵图层树(层叠上下文、Clip,类似 PhotoShop 里的图层);
拥有层叠上下文属性(明确定位属性、透明属性、CSS 滤镜、z-index 等)的元素会创建单独图层;
没有图层的 DOM 节点属于父节点图层;
需要剪裁的地方也会创建图层。
绘制指令
输入:图层树;
渲染引擎对图层树中每个图层进行绘制;
拆分成绘制指令,生成绘制列表,提交到合成线程;
输出:绘制列表。
分块
合成线程会将较大、较长的图层(一屏显示不完,大部分不在视口内)划分为图块(tile, 256256, 512512)。
光栅化(栅格化)
在光栅化线程池中,将视口附近的图块优先生成位图(栅格化执行该操作);
快速栅格化:GPU 加速,生成位图(GPU 进程)。
合成绘制
绘制图块命令——DrawQuad,提交给浏览器进程;
浏览器进程的 viz 组件,根据DrawQuad命令,绘制在屏幕上。
页面完成
页面生成完成后,渲染进程发送消息给浏览器进程,收到后会停止标签栏上图标的加载动画
over
优化
渲染进程的主线程按照顺序首先构建dom树,样式计算,布局,绘制
如果发生重排(比如改变元素大小等)会直接从样式计算开始从新执行,如果发生重绘(例如改变颜色)会直接从绘制阶段开始从新执行,所以频繁的占用渲染进程的主进程,就会产生较大的开销。所以在性能优化中,减少重排和重绘的属性,渲染引擎将跳过布局和绘制,只执行非主线程的合成,例如:CSS的transform就避开重排和重绘,直接在非主线程上执行合成操作,没有占用主线程资源,所以效率最高
HTML渲染流程
在上图中一并画了出来,需要经过以下几个阶段:构建 DOM 树--->样式计算--->布局--->分层--->绘制--->分块--->光栅化--->合成
DNS
DNS 的解析是一个递归流程,顺序如下图中数字标记所示:
image- 根 DNS 服务器:返回顶级域 DNS 服务器的 IP 地址。
扯一句:世界上有13台根服务器,而有N台根镜像服务器,所以有些国外地址会被墙 ~~~,说明域名解析第一步并不是走的根服务器,而是由根镜像服务器控制住做了过滤,有选择性地进行了域名解析,所以如果根域名服务器出了问题,我觉得我们国内仍然可以做到正常访问,当然只是新更新的域名无法访问。 - 顶级 DNS 服务器:返回权威 DNS 服务器的 IP 地址
- 权威 DNS 服务器:返回相应主机的 IP 地址
JS 栈 垃圾数据回收
- 示例代码1:
function hello () {
let name = '数据'
}
hello()
console.log(name) // Uncaught ReferenceError: name is not defined 在执行上下文时,存放在栈内存中的name地址和数据【内存块】已经回收掉了,所以在全局上下文中是访问不到。
- 示例代码2:
function hello () {
let name = '前端食堂'
let food = { name: '回锅肉' }
function world () {
var description = { slogan: '吃好每一顿饭' }
}
world()
}
hello()
上面的代码所对应的内存堆栈空间如下图所示:
image栈中的垃圾回收比较简单,当一个函数执行结束后,JavaScript 引擎会通过向下移动 ESP 来销毁函数调用栈中所保存的执行上下文,ESP 就是记录当前执行状态的指针。
- 垃圾回收操作会暂停 JavaScript 的运行,回收完毕后才会恢复执行,这种行为就是全停顿。
为了降低全停顿所带来的卡顿,V8 引擎采用了增量标记(Incremental Marking) 算法进行优化,将标记过程分为一个个小任务,这些小任务的执行时间比较短,可以穿插在其他的 JavaScript 任务中间执行,这样就不会有明显的卡顿了。
当然,V8 所采用的优化方案不只这一种,而是多种方案综合使用的,除了增量回收还有并行回收、并发回收等。
并行回收:垃圾回收器会使用多个辅助线程来并行执行垃圾回收
并发回收:回收线程在执行 JavaScript 的过程中,辅助线程在后台执行垃圾回收
如果你了解【** React 的 Concurrent 模式中时间切片的原理 **】,它的实现思想是不是与增量标记算法有异曲同工之妙呢。
其中在页面渲染以及网络请求响应的性能优化方面可以做的优化工作有:
1.DNS优化
2.将js写在body尾部,使不阻塞渲染页面
3.预加载或懒加载
4.资源压缩,减少请求次数