浏览器原理 24 # 页面性能:如何系统地优化页面?

Posted 凯小默

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浏览器原理 24 # 页面性能:如何系统地优化页面?相关的知识,希望对你有一定的参考价值。

说明

浏览器工作原理与实践专栏学习笔记

前言

通常一个页面有三个阶段:

  • 加载阶段:是指从发出请求到渲染出完整页面的过程,影响到这个阶段的主要因素有网络和 javascript 脚本。
  • 交互阶段:主要是从页面加载完成到用户交互的整合过程,影响到这个阶段的主要因素是 JavaScript 脚本。
  • 关闭阶段:主要是用户发出关闭指令后页面所做的一些清理操作。

下面重点看一下加载阶段和交互阶段的优化。

加载阶段

加载阶段渲染流水线

在这里插入图片描述

对关键资源进行优化

能阻塞网页首次渲染的资源称为关键资源

JavaScript、首次请求的 html 资源文件、CSS 文件是会阻塞首次渲染的,而图片、音频、视频等文件就不会阻塞页面的首次渲染

影响页面首次渲染的核心因素:

关键资源个数

关键资源个数越多,首次页面的加载时间就会越长。

如何减少关键资源的个数?

  1. 将 JavaScript 和 CSS 改成内联的形式,比如上图的 JavaScript 和 CSS,若都改成内联模式,那么关键资源的个数就由 3 个减少到了 1 个。
  2. 如果 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 脚本执行时间

  1. 一种是将一次执行的函数分解为多个任务,使得每次的执行时间不要过久。
  2. 另一种是采用 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 # 页面性能:如何系统地优化页面?的主要内容,如果未能解决你的问题,请参考以下文章

前端性能优化之--页面渲染优化全面解析

iOS页面间跳转的性能优化

从浏览器渲染层面解析css3动效优化原理

浏览器渲染基本原理:优化渲染性能

Web页面加载,如何分析页面性能?如何进行优化?

H5性能优化