浏览器原理 24 # 页面性能:如何系统地优化页面?
Posted 凯小默
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浏览器原理 24 # 页面性能:如何系统地优化页面?相关的知识,希望对你有一定的参考价值。
说明
浏览器工作原理与实践专栏学习笔记
前言
通常一个页面有三个阶段:
- 加载阶段:是指从发出请求到渲染出完整页面的过程,影响到这个阶段的主要因素有网络和 javascript 脚本。
- 交互阶段:主要是从页面加载完成到用户交互的整合过程,影响到这个阶段的主要因素是 JavaScript 脚本。
- 关闭阶段:主要是用户发出关闭指令后页面所做的一些清理操作。
下面重点看一下加载阶段和交互阶段的优化。
加载阶段
加载阶段渲染流水线
对关键资源进行优化
能阻塞网页首次渲染的资源称为关键资源。
JavaScript、首次请求的 html 资源文件、CSS 文件是会阻塞首次渲染的,而图片、音频、视频等文件就不会阻塞页面的首次渲染。
影响页面首次渲染的核心因素:
关键资源个数
关键资源个数越多,首次页面的加载时间就会越长。
如何减少关键资源的个数?
- 将 JavaScript 和 CSS 改成内联的形式,比如上图的 JavaScript 和 CSS,若都改成内联模式,那么关键资源的个数就由 3 个减少到了 1 个。
- 如果 JavaScript 代码没有 DOM 或者 CSSOM 的操作,则可以改成 async 或者 defer 属性;同样对于 CSS,如果不是在构建页面之前加载的,则可以添加媒体取消阻止显现的标志。
当 JavaScript 标签加上了 async 或者 defer、CSSlink 属性之前加上了取消阻止显现的标志后,它们就变成了非关键资源了。
关键资源大小
通常情况下,所有关键资源的内容越小,其整个资源的下载时间也就越短,那么阻塞渲染的时间也就越短。
如何减少关键资源的大小?
可以压缩 CSS 和 JavaScript 资源,移除 HTML、CSS、JavaScript 文件中一些注释内容,也可以通过前面讲的取消 CSS 或者 JavaScript 中关键资源的方式。
请求关键资源需要多少个 RTT(Round Trip Time)
RTT 是网络中一个重要的性能指标,表示从发送端发送数据开始,到发送端收到来自接收端的确认,总共经历的时延。通常 1 个 HTTP 的数据包在 14KB 左右,所以 1 个 0.1M 的页面就需要拆分成 8 个包来传输了,也就是说需要 8 个 RTT。
注意:由于渲染引擎有一个预解析的线程,在接收到 HTML 数据之后,预解析线程会快速扫描 HTML 数据中的关键资源,一旦扫描到了,会立马发起请求,可以认为 JavaScript 和 CSS 是同时发起请求的,所以它们的请求是重叠的,那么计算它们的 RTT 时,只需要计算体积最大的那个数据就可以了。例如上面渲染图中最大的是 CSS 文件(9KB),所以我们就按照 9KB 来计算,同样由于 9KB 小于 14KB(1 个 RTT),所以 JavaScript 和 CSS 资源也就可以算成 1 个 RTT。也就是说,图中关键资源请求共花费了 2 个 RTT。
如何减少关键资源 RTT 的次数?
可以通过减少关键资源的个数和减少关键资源的大小搭配来实现。除此之外,还可以使用 CDN 来减少每次 RTT 时长。
总的优化原则:减少关键资源个数,降低关键资源大小,降低关键资源的 RTT 次数。
交互阶段
交互阶段的优化,其实就是在谈渲染进程渲染帧的速度,因为在交互阶段,帧的渲染速度决定了交互的流畅度。
交互阶段渲染流水线
关于如何生成一帧图像:这个就不多讲了,可以去参考上一篇文章浏览器原理 23 # 分层和合成机制:为什么CSS动画比JavaScript高效?
如何优化渲染帧的速度
1. 减少 JavaScript 脚本执行时间
- 一种是将一次执行的函数分解为多个任务,使得每次的执行时间不要过久。
- 另一种是采用 Web Workers。
在 Web Workers 中是可以执行 JavaScript 脚本的,不过 Web Workers 中没有 DOM、CSSOM 环境,这意味着在 Web Workers 中是无法通过 JavaScript 来访问 DOM 的,可以把一些和 DOM 操作无关且耗时的任务放到 Web Workers 中去执行。
拓展:
JavaScript 语言采用的是单线程模型,也就是说,所有任务只能在一个线程上完成,一次只能做一件事。前面的任务没做完,后面的任务只能等着。随着电脑计算能力的增强,尤其是多核 CPU 的出现,单线程带来很大的不便,无法充分发挥计算机的计算能力。Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。————来自阮一峰网络日志:Web Worker 使用教程
2. 避免强制同步布局
讲这个之前,我们先看正常情况下的布局操作。
正常情况下的布局操作:
例子:
<!DOCTYPE html>
<html>
<body>
<div id="mian_div">
<li id="time_li">time</li>
<li>kxm</li>
</div>
<p id="demo">强制布局demo</p>
<button onclick="foo()">添加新元素</button>
<script>
function foo() {
let main_div = document.getElementById("mian_div")
let new_node = document.createElement("li")
let textnode = document.createTextNode("kxm-test")
new_node.appendChild(textnode);
document.getElementById("mian_div").appendChild(new_node);
}
</script>
</body>
</html>
Performance 记录添加元素的执行过程:
我自己测试了一下:从图中可以看出来,执行 JavaScript 添加元素是在一个任务中执行的,重新计算样式布局是在另外一个任务中执行,这就是正常情况下的布局操作。
强制同步布局,是指 JavaScript 强制将计算样式和布局操作提前到当前的任务中。
例子:在上面代码的基础上改造一下
function foo() {
let main_div = document.getElementById("mian_div")
let new_node = document.createElement("li")
let textnode = document.createTextNode("kxm-test")
new_node.appendChild(textnode);
document.getElementById("mian_div").appendChild(new_node);
//由于要获取到offsetHeight,
//但是此时的offsetHeight还是老的数据,
//所以需要立即执行布局操作
console.log(main_div.offsetHeight)
}
触发强制同步布局 Performance 图:
我自己测试了一下:从图中可以看出来,计算样式和布局都是在当前脚本执行过程中触发的,代码里要获取到 main_div 的高度,就需要重新布局,所以这里在获取到 main_div 的高度之前,JavaScript 还需要强制让渲染引擎默认执行一次布局操作,这种就是强制同步布局。
如何避免强制同步布局
比如上面代码,可以调整策略,在修改 DOM 之前查询相关值。
3. 避免布局抖动
所谓布局抖动,是指在一次 JavaScript 执行过程中,多次执行强制布局和抖动操作。
我们继续在上面的例子的基础上修改:
function foo() {
let time_li = document.getElementById("time_li")
for (let i = 0; i < 100; i++) {
let main_div = document.getElementById("mian_div")
let new_node = document.createElement("li")
let textnode = document.createTextNode("kxm-test")
new_node.appendChild(textnode);
new_node.offsetHeight = time_li.offsetHeight;
document.getElementById("mian_div").appendChild(new_node);
}
}
Performance 中关于布局抖动的表现:
我自己测试了一下:从图中可以看出,在 foo 函数内部重复执行计算样式和布局,这会大大影响当前函数的执行效率。
避免方式和强制同步布局一样,都是尽量不要在修改 DOM 结构时再去查询一些相关值。
4. 合理利用 CSS 合成动画
合成动画是直接在合成线程上执行的,这和在主线程上执行的布局、绘制等操作不同,如果主线程被 JavaScript 或者一些布局任务占用,CSS 动画依然能继续执行。
比如:如果能提前知道对某个元素执行动画操作,那就最好将其标记为 will-change
,这是告诉渲染引擎需要将该元素单独生成一个图层。
5. 避免频繁的垃圾回收
JavaScript 使用了自动垃圾回收机制,如果在一些函数中频繁创建临时对象,那么垃圾回收器也会频繁地去执行垃圾回收策略。这样当垃圾回收操作发生时,就会占用主线程,从而影响到其他任务的执行,严重的话还会让用户产生掉帧、不流畅的感觉。
怎么避免?
尽可能优化储存结构,尽可能避免小颗粒对象的产生。
以上是关于浏览器原理 24 # 页面性能:如何系统地优化页面?的主要内容,如果未能解决你的问题,请参考以下文章