前端性能优化到底是在优化什么?

其实前端性能优化核心就是两点:

  • 保证资源更快的 加载速度:达到越快渲染越快,视图展现就越快
  • 保证视图更快的 渲染速度/交互速度:用户与页面交互,前提是页面要渲染出来,其次是页面需要尽早反馈,目的就是保证用户良好的体验性

而这些核心内容都可以从下面这个老生常谈的问题中延伸开来。

从输入 URL 到页面加载完成发生了什么?

相信到现在为止,大家对这个问题的回答可以说是能够做到滔滔不绝了吧(如果不能,请忽略)!不过每个人回答的方向和重点应该都不一样,比如之前在 B 站winter 大佬对这个问题的看法和解析的角度是更深、更广的。

在这还是要简单的总结一下核心内容:

  • 进行 DNS 解析
  • 建立 TCP 连接
  • 客户端发送 HTTP 请求
  • 服务端响应 HTTP 资源
  • 浏览器获取响应内容,进行解析和渲染

以上任意一点都可进行无限扩展、延伸,但点到为止才是现在真正需要的。

性能指标

RAIL 模型

Google 为前端页面性能的评估提出了 RAIL 模型,核心内容如下:

  • Response 响应
  • Animation 动画
  • Idle 空闲
  • Load 加载

imgimg

常规性能指标

性能指标其实有不少的内容,但在这我们指列举比较常用的几种:

  • 首次绘制(First Paint,FP
    • 在渲染进程确认要渲染当前响应资源后,渲染进程会先创建一个空白页面,通常把创建空白页面的这个时间点称为 First Paint,简称 FP
    • 所谓的 白屏时间 其实指的就是创建这个空白页面到浏览器开始渲染非空白内容的时间,比如页面背景发生变化等
  • 首次内容绘制(First Contentful Paint,FCP
    • 当用户看见一些 “内容” 元素被绘制在页面上的时间点,和白屏是不一样,它可以是 文本 首次绘制,或 SVG 首次出现,或 Canvas 首次绘制等,即当页面中绘制了第一个 像素 时,这个时间点称为 First Content Paint,简称 FCP
  • 首屏时间 / 最大内容绘制(Largest Contentful Paint, LCP
    • LCP 是一种新的性能度量标准,LCP 侧重于用户体验的性能度量标准,与现有度量标准相比,更容易理解与推理,当首屏内容完全绘制完成时,这个时间点称为 Largest Content Paint,简称 LCP
    • 最大内容绘制应在 2.5s 内完成
  • 首次输入延迟(First Input Delay, FID
    • FID 测量的是当用户第一次在页面上交互的时候(点击链接点击按钮自定义基于 js 的事件),到浏览器实际开始处理这个事件的时间
    • 首次输入延迟应在 100ms 内完成
  • 累积布局偏移(Cumulative Layout Shift, CLS)
    • CLS 是为了测量 视觉稳定性,以便提供良好的用户体验
    • 累积布局偏移应保持在 0.1 或更少
  • 首字节达到时间(Time to First Byte,TTFB
    • 指的是浏览器开始收到服务器响应数据的时间(后台处理时间 + 重定向时间),是反映服务端响应速度的重要指标
    • TTFB 时间如果超过 500ms,用户在打开网页的时就会感觉到明显的等待

性能指标工具

通过上述内容了解了性能指标的相关内容和一些阀值,那么接下来的问题是我们怎么获取一个网站的具体性能指标数据呢?

为了方便还是得使用工具或者说是 API,当然可以 自定义页面性能指标 的计算方式,比如有些就是通过计算当前页面 DOM总节点数嵌套层级 来计算一个网站的分数等,这里就不再额外介绍。

Performance 面板(Google)

具体参数介绍可以看 Big shark@LX 大佬的文章,里面介绍的非常详细,这里只列举一些核心点。

火焰图

Networks 指标

通过 Networks 指标可以查看到对应服务器加载资源的相关信息:

img

可以将鼠标 移动点击 到具体的请求上查看加载时间和加载速度,如下:

鼠标移入:

img

鼠标点击:

img

Frames 指标

通过 Frames 指标可以查看页面每一帧渲染时 CPU 所消耗的时间和持续时间 Duration 的信息,如下:

图一:

img

图二:

img

Timings 指标

通过 Timings 指标可以查看在上面列举的一些性能指标的值,如下:

img

  • 首次绘制(First Paint,FP
  • 首次内容绘制(First Contentful Paint,FCP
  • 首屏时间 / 最大内容绘制(Largest Contentful Paint, LCP
  • HTML 文档被完全加载 和 解析完成的时间(DOMContentLoaded, DCL

Main 指标

Main 指标包含了加载过程的三个阶段:

  • 导航阶段

    • 主要是处理响应头的数据,并执行一些老页面退出之前的清理操作
  • 解析

    1
    HTML

    文件阶段

    • 主要是解析 HTML 数据、解析 CSS 数据、执行 JavaScript 来生成 DOMCSSOM
  • 生成位图阶段

    • 主要是将生成的 DOMCSSOM 合并,包括了布局 (Layout)、分层、绘制、合成等一系列操作

img

Lighthouse 面板(Google)

Performance 面板最大的优点就是各种数据信息非常的全,但这也是它最大的缺点,数据信息庞大到需要自行过滤,对于不熟悉的开发者来说,还是需要一定的学习成本的。

相反,Lighthouse 面板中的信息就相对简洁一些,除了检测结果以外,还会提供对应的改进方案,真是考虑得妥妥的,主要检测五个方面的内容:

  • Performance(性能)
  • Accessibility(可访问性)
  • Best practice(最佳实践)
  • SEO(搜索引擎优化)
  • Progressive Web App(渐进式 Web 应用)

img

可以通过 Analyze page load 按钮来开始对页面应用进行检测,这里以掘金首页为例:

img

下面以 Performance 性能 为例简单看一下具体包含的内容,由于篇幅有限,其他内容可自行测试并进行阅读。

Performance 性能(举一反三)

从性能指标的数据来看,只有 累积布局偏移(Cumulative Layout Shift, CLS) 满足要求,其他指标显示 黄色红色,意味着仍有改进的空间,特别是 首屏时间2.9s 已经是超过了对应的阈值 2.5s

性能指标数据如下图所示:

img

甚至还提供了对应的诊断结果,比如提到的图片没有设置对应的宽高:

img

Using the Node CLI

甚至还支持在 Node 环境运行,感兴趣的自行去 npm 中查看 文档 即可,这里不过多介绍。 img

性能指标数据收集

上述性能指标工具的能力已经足够强大,覆盖信息也很全面,但如果我们需要将页面性能指标数据收集并上报又该怎么办呢?

首先排除的肯定是通过 性能指标工具 的方式来收集,一旦要检测性能指标数据意味着得是不同的客户端统计数据的结果合集(除非你愿意一台一台客户端来手动记录和收集数据,呸,你愿意你领导还不愿意呢),最理想的方式当然是自动收集和上报,那就意味着这应该是代码要干的活!!!

既然有这样的需求,那么必定有对应的解决方案,您接着往下看!

Performance API

实际上在浏览器端的全局对象 window 上有一个名为 performance 的属性,它是一个用于支持 IE9 以上及 webkit 内核浏览器中用于记录页面 加载解析 过程中关键时间点的机制,其兼容性在 caniuse 中的表现如下:

img

下面就简单介绍一下和 window.performance 相关一些核心属性和方法。

img

performance.timing 属性

performance.timing 属性中提供了很多关键的时间信息,我们可以通过这些时间节点来简单的计算出需要的性能指标数据(不一定准确),计算方式如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const {
domainLookupStart,
domainLookupEnd,
navigationStart,
loadEventEnd,
responseStart,
responseEnd,
connectStart,
connectEnd,
redirectStart,
redirectEnd,
domContentLoadedEventEnd,
domComplete,
} = performance.timing

// DNS 查询时间
DNS = domainLookupEnd - domainLookupStart

// TCP 建立连接时间
TCP = connectEnd - connectStart

// 页面重定向时间
Redirect = redirectEnd - redirectStart

// 首字节到底时间
TTFB = responseStart - navigationStart

// 首次渲染时间
FP = responseStart - navigationStart

// DOM 解析时间
DOM = domComplete - responseEnd

// 首屏时间
LCP = loadEventEnd - navigationStart

performance.getEntries() 方法

performance.getEntries() 方法可以获取所有资源请求的时间数据,如下:

img

点击可查看具体的资源信息,其他属性和上述内容有重复,就不在额外介绍计算方式了,具体如下:

img

performance.now() 方法

performance.now() 方法可以精确计算程序执行时间,它会返回以微秒(百万分之一秒)为单位的时间,即更加精准,这也是它和 Date.now() 是不同点:

  • Date.now() 返回自 1970年1月1日 00:00:00 (UTC) 到 当前时间毫秒数

    • 意味着 Date.now() 依赖于系统的当前时间,而系统时间可以被认为修改,因此它的毫秒数并不准确
  • performance.now() 的时间是以恒定速率递增的,不受系统时间的影响

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // Date.now()
    let a = 2, b = 3;
    const begin = Date.now();
    console.log(' a + b = ', a + b);

    console.log('time = ', Date.now() - begin); // 2

    // performance.now()
    let a = 2, b = 3;
    const begin = performance.now();
    console.log(' a + b = ', a + b);

    console.log('time = ', performance.now() - begin); // 0.10000002384185791

Web Vitals

web-vitals 库是 Google 推出的一个小型(约 1.5K)模块化库,用于测量真实用户的所有 Web Vitals 相关的指标,其重要核心指标信息如下(一图胜千言):

img

接下来,让我们通过 npx create-react-app my-react-app 来创建一个 react 项目,然后观察一下它的项目结构:

image.png

是不是超级显眼的 reportWebVitals.js,在进入文件查看你会发现我们需要的核心性能指标都在里面:

img

就针对优化方案进行总结,核心的方向还是上篇文章中提到的内容:

  • 保证资源更快的 加载速度:达到越快渲染越快,视图展现就越快
  • 保证视图更快的 渲染速度/交互速度:用户与页面交互,前提是页面要渲染出来,其次是页面需要尽早反馈,目的就是保证用户良好的体验性

其实将上述两点再进行翻译,那么其实指的就是 网络层面的优化浏览器层面的优化,这样看来其实前端性能优化方向还是很明确的,只不过明确的方向中还是会涉及不同方面的具体优化手段。

0B249F1E.jpg

还是不得不回顾 从输入 URL 到页面加载完成 的核心过程:

  • 进行 DNS 解析
  • 建立 TCP 连接
  • 客户端发送 HTTP 请求
  • 服务端响应 HTTP 资源
  • 浏览器获取响应内容,进行解析和渲染

从上述的内容来看,不难发现每一步都是需要消耗一定的时间,那么优化的方向就可以围绕着这些内容来考虑。

长文预警❗️❗️❗️ 长文预警❗️❗️❗️ 长文预警❗️❗️❗️ 长文预警❗️❗️❗️

1.gif

如何保证资源更快的加载速度?

下面内容主要针对 DNS 解析TCP 连接HTTP 请求/响应 等阶段来谈的优化,核心优化核心其实就是 网络层面

使用 dns-prefetch 减少 DNS 的查询时间

dns-prefetch 能够 提前解析 后续可能会用到的 不同域的域名,使解析结果 缓存到系统缓存 中,缩短 DNS 解析时间以提高网站的访问速度。

比如在掘金中的体现如下:

img

扩展DNS 解析的核心过程

当浏览器访问一个域名时需解析一次 DNS,以获得对应域名的 IP 地址:

  • 浏览器 会从自身的 缓存中 查询是否存在对应域名的 IP 地址,若存在返回,若不存在进入下一步
  • 客户机查询操作系统中的 /etc/hosts 文件中查询是否有对应域名的 IP 地址,若存在则返回,若不存在进入下一步
  • 客户机请求 本地域名服务器(LDNS) 进行解析,本地域名服务器收到客户机的请求后,先查询自己的缓存信息是否有对应域名的 IP 地址,若存在返回结果,没有则进行下一步
  • 本地域名服务器请求 根域名服务器 解析该域名,根域名告诉本地域名服务器去找对应的 一级域名服务器
  • 本地域名服务器请求一级域名服务器解析这个域名,一级域名服务器告诉它去找对应的 二级域名服务器
  • 本地域名服务器请求二级域名服务器解析这个域名,二级域名服务器告诉它去找对应的 子域名服务器
  • 本地域名服务器请求子域名服务器解析这个域名,子域名服务器返回对应的 IP 地址
  • 本地域名服务器将 IP 地址记录到缓存中,并返回给客户机(会缓存起来),客户机根据收到的 IP 地址访问该网站

使用 preconnect 提前建立连接

preconnect 的作用是提前和第三方资源建立连接,设置了它浏览器就会做好早期的连接工作,但这个连接通常只会维持 10 s

比如在当前域请求一个资源前,可能会涉及 DNS 寻址、TLS 握手、TCP 握手、重定向等,这过程也会花费一定的时间。

比如在掘金中的体现如下:

img

使用 preload / prefetch 预先加载资源

preload

preload 的作用是提前加载页面对应的 关键资源 加快页面的渲染,preload 的优先级顺序和 as 属性相关,具体可见

注意as 属性一定要设置,除了上面提到的设置优先级外,还涉及到浏览识别的问题:如果没有设置 as 属性,后续遇到该请求就会被作为一个 XHR 请求,把意味着资源预加载的功能可能会失效,因为可能会每次都发起新的请求获取

比如在掘金中的体现如下:

img

比如在 vue-cli 的默认 webpack 配置,如下:

image.png

prefetch

preload 是对资源的预加载,它虽提前加载但只在需要执行时执行,即这个资源一定是当前页面所需要的资源,如果是需要为下一个页面提前加载资源,那么应该使用 prefetch,它会在 浏览器空闲时 下载资源。

比如在 vue-cli 的默认 webpack 配置,如下:

image.png

压缩资源体积

资源是需要通过 http 数据包的方式在网络中进行传输的,那么只要能减少传输数据包的体积,也是能够使得资源更快到达客户端,这也是压缩资源体积的核心目的。

HTTP 压缩

HTTP 压缩中一个典型代表就是 gzip,它是一种优秀的压缩算法,可对 http 请求中的一些文件资源进行压缩处理,一般来讲是要在服务端处理的,可通过在响应头中设置 Content-encoding: gzip 表示当前资源使用的压缩方式(如:gzip、deflate、br 等),便于客户端使用正确的方式解压。

注意gzip 并不是万能的,它不能保证针对每个文件的压缩都能使其体积变小,关于 Content-Encoding 的内容 可点此查阅,或者可参考 content-encoding 除了gzip之外,你还知道哪些?

比如在京东中的体现:

image.png

比如在掘金中的体现:

image.png

Webpack 压缩

HTTP 压缩 不就够了吗?为什么还需要 Webpack 压缩?

首先必须要明确的是压缩的过程本身就是会消耗时间的,如果所有资源都等到被访问的时候再由服务端进行压缩,在压缩完成之前客户端还是得处于等待状态,即仍 不能保证资源以最快的速度到达客户端

那么优化方案就是将压缩资源的时间放到打包构建中,毕竟只有真正需要发布线上生产环境时才需要执行一系列的打包优化的操作,而这相比于 http请求/响应 速度,稍微延长产物打包时间没有什么大问题。

下面会列举一些 Webpack 插件,但并不会去讲其中的具体用法,因为这些只是达到目的的不同方案而已,每个方案要是细讲都可以独占一篇文章,在这是没有必要的,具体用法可自行查阅。

使用 CompressionPlugin 压缩文件

image.png

webpack 文档 提供插件合集中就包含了该插件,它的作用就是:Prepare compressed versions of assets to serve them with Content-Encoding.

使用 HtmlWebpackPlugin 压缩 HTML 文件

image.png

通常我们需要 HtmlWebpackPlugin 插件来生成对应 HTML 或 对已有的 HTML 模板自动注入 webpack bundles 资源,除此之外,它还可配置 minify 选项实现压缩模板的目的。

可以在 vue 项目下执行 vue inspect --mode production > webpack.config.js 来查看脚手架的默认 webpack 配置内容,比如:

image.png

使用 SplitChunksPlugin 自定义分包策略

Webpack 默认会将尽可能多的模块代码打包在一起,这种默认规则的带来的优点和缺点都很明显:

  • 优点:能减少最终页面的 HTTP 请求数
  • 缺点:
    • 页面初始代码包过大,影响首屏渲染性能
    • 无法有效应用浏览器缓存

SplitChunksPluginWebpack 4 之后内置实现的最新分包方案,与 Webpack 3 中的 CommonsChunkPlugin 相比,它能够基于一些更灵活、合理的启发式规则将 Module 编排进不同的 Chunk,最终构建出 性能更佳、缓存更友好 的应用产物。

比如在 vue-cli 的默认 webpack 配置,如下:

image.png

使用 MiniCssExtractPlugin 抽离和压缩 CSS

MiniCssExtractPlugin 会将 CSS 提取到单独的文件中,为每个包含 CSSJS 文件创建一个 CSS 文件,并且支持 CSSSourceMaps按需加载

比如在 vue-cli 的默认 webpack 配置,如下:

img

image.png

使用 ImageMinimizerWebpackPlugin 压缩图片资源

图片仍是一个 Web 应用中的必不可少的资源,而图片资源的体积也是首屏页面加载的瓶颈之一,因此,压缩图片也是性能优化需要考虑的内容。

ImageMinimizerWebpackPlugin 可用于使用 优化/压缩 所有图像,它可以支持 无损(不损失质量)有损(质量下降) 两种模式的压缩方式。

image.png

通过 Tree Shaking 移除无用代码

Tree Shaking 依赖于 ES6 模块语法的 静态结构 特性(如: importexport),当 webpack 的模式 mode"production" 时,就可以启用 更多优化项,包括 压缩代码Tree Shaking

但同时我们就必须保证:

  • 尽量使用 ES6 模块语法,即 importexport
  • 保证没有 编译器(如:babel)将对应的 ES6 模块语法转换为 CommonJS 的语法(如:@babel/preset-env 的默认行为)
  • 可在项目的 package.json 文件中添加 "sideEffects" 属性,标识当前内容是否存在副作用操作
  • 可在通过 /*#__PURE__*/ 注释,将函数调用标记为无副作用

减少 http 请求数量

不同协议请求数量 仍然可能成为 请求/响应 慢的原因:

  • 合并公共资源,如 雪碧图 等
  • 内置模块资源,如 生成 base64 图片、通过 symbol 引用 svg
  • 合并代码块,如构建工具分包策略配合 公共组件封装、组件复用逻辑抽离 等
  • 按需加载资源,如 路由懒加载、图片懒加载、上拉加载、分页加载 等

不必要的 cookie 来回传输会造成带宽浪费:

  • 减少 cookie 存储的内容
  • 对于静态资源采用 CDN 托管(即非同域),不同域名默认不携带 cookie

CDN 托管静态资源 + HTTP 缓存

CDN 加速的本质是缓存加速,将服务器上存储的静资源容缓存在 CDN 节点上,当后续访问这些静态内容时,无需访问服务器源站,选择就近访问 CDN 节点即可,从而达到加速的效果,同时减轻服务器源站的压力。

在掘金中体现如下:

img

image.png

协议升级为 Http2.0

http1.x 存在的问题: HTTP 的底层协议是 TCP,而 TCP 是面向连接即需要 三次握手 才能建立连接,其中:

  • http1.0 中使用的是 短连接,即 一次请求/响应 结束后就会断开连接,这个过程比较耗时

  • http1.1
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    中使用的是

    长连接

    ,在

    请求/响应头

    中设置

    Connection: keep-alive
    1
    2
    3
    4
    5
    6
    7

    即可开启,优点是

    长连接

    允许多个请求共用一个

    TCP
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25

    连接,缺点是带来了

    队头阻塞



    - 每个 `TCP` 连接中的多个请求,需要进行排队,只有队头的请求被响应,才能继续处理下一个请求
    - 其中一个缓解方案就是如果当前 `TCP` 连接中发生 **队头阻塞**,那就将部分请求放到其他 `TCP` 连接中
    - 浏览器一般会限制同一个域名建立 `6-8` 个 `TCP` 链接,这也就是为什么需要为应用划分子域名、静态资源托管 `CDN` 的原因之一

    - `http1.x` 中 `header` 部分的内容可能会很大,而且每一个请求可能都需要携带大量 **重复** `header` 的 **文本内容**,而这些也是导致 **请求/响应** 慢的原因之一

    > **以上问题 `http2.0` 都能够解决:**

    - 针对 **TCP 连接数** 被限制的问题,`http2.0` 采用 **多路复用** 一个域名只对应一个 `TCP` 连接

    - 针对 **http 队头阻塞** 问题,`http2.0` 中通过二进制分帧层为每个 **请求/响应** 添加 `stream id` 保证 **请求/响应** 一一对应,即不必等待前面的请求处理完成,并且还可以为每个请求添加 **优先级**

    - 针对

    `header`

    数据大的问题,

    http2.0
    1
    2
    3

    中传输的

    header
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    帧经过处理后会用

    二进制

    的方式表示,替换了原本的

    文本格式

    ,并使用

    HPACK
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116

    算法进行压缩

    - **接收/发送** 两端会维护一个 **索引表**,通过下标来标识 `header`,针对后续重复的 `header` 信息就可以用对应的索引来代替

    - 针对传统的 **请求 —> 响应** 模式,`http2.0` 中提供了 **服务端推送** 的能力,让服务端能够主动向客户端推送关键资源,加快资源加载

    比如在掘金中的体现:

    ![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b6db14af77a04c5d9ccecf6e450c680e~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp?)

    # 如何保证视图更快的渲染和交互?

    保证资源快速到达客户端后,接下来就需要针对 **浏览器的解析和渲染** 进行优化,当然还包括后续的页面交互的优化,这其实就是 **浏览器层面** 的优化。

    浏览器渲染 `HTML` 文件的核心过程:

    - **HTML 解释器**:将 **`HTML`** 文档经过词法分析输出 **`DOM Tree`**

    - **CSS 解释器**:解析 **`CSS`** 文档,生成样式规则 **`CSSOM`**

    - **样式计算**:将 **`DOM Tree`** 和 **`CSSOM`** 合并生成 **`Render Tree`**

    - **布局计算**:计算 **`Render Tree`** 节点在页面中的坐标位置,创建 **`Layout Tree`**

    - **划分图层**:页面中有很多复杂的效果,如一些复杂的 `3D` 变换、页面滚动,或使用 `z-index` 做 `z` 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,生成对应的图层树 **`Layer Tree`**

    - 图层绘制



    - 染引擎实现图层的绘制时,会把一个图层的绘制拆分成很多小的 **绘制指令**,并将这些指令按照顺序组成一个 **待绘制列表**
    - 当图层的绘制列表准备好后,**主线程** 会把 **待绘制列表** 提交(`commit`)给 **合成线程**

    - 栅格化 raster



    - 由于视口有限,用户只能看到页面的很小一部分,没必要绘制出所有图层内容,因此 **合成线程** 会将 **图层(layer)** 划分为 **图块(tile)**
    - **渲染进程** 把 **生成图块的指令** 发送给 `GPU` 并执行生成图块的 **位图**

    - 合成和显示



    - 一旦所有图块都被光栅化,**合成线程** 就会生成一个绘制图块的命令 —— `DrawQuad`,然后将该命令提交给 **浏览器进程**
    - **浏览器进程** 里面有一个 **viz 组件**,会根据 `DrawQuad` 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上

    ## 渲染层面

    ### 减少阻塞渲染的因素

    真正渲染视图之前,必然要生成 **`DOM Tree`** 和 **`CSSOM`**,因此必须保证 **HTML 解释器** 和 **CSS 解释器** 都尽早处理完成,同时 **`JavaScript`** 的加载和执行可能会阻塞这个过程:

    - `HTML` 文档中首次渲染的节点数量要尽量少,避免深层次的嵌套结构,避免大量使用慢标签(如:`iframe`)等
    - `CSS` 资源放文档头部,降低 `CSS` 复杂度,比如 合理使用 `CSS` 选择器
    - `JavaScript` 资源放文档底部,合理使用 `defer、async` 的加载方式

    ### 懒加载

    懒加载主要是针对数量大、资源加载慢的情况,比如图片资源、大量列表数据展示等:

    - **图片资源**:优先加载在可视区范围内的图片,可视区外的图片 **`延后加载`**,或者说当移入的可视区时再加载
    - **列表数据**:列表数据通常数据里量大,不可能一次渲染完所有数据,一般通过 **`分页加载、上拉加载`** 等方式分批次渲染

    ### 白屏优化

    白屏是由于 `SPA` 应用需要等待 `JavaScript` 加载并执行完成后才会生成具体的页面结构内容导致的,即初始化模板中没有任何有意义需要被渲染的 `HTML` 结构:

    - 添加 **白屏 `loading`**,可在模板中添加默认的 `loading` 效果,等到真正页面内容被渲染就可以替换 `loading` 内容
    - 添加 **骨架屏**,和上述方案一致,在真正页面内容展示出来之前,先展示默认的视图内容,避免白屏

    ### 服务端渲染(server-side rendering)

    现代框架默认是属于客户端应用框架,即组件的代码会在浏览器中运行,然后向页面输出 **DOM** 元素,也叫 **客户端渲染(client-side rendering,CSR)**:

    - 优点
    - **`用户体验更好`**,基于 **前端路由** 的方式并不会真正进行 **页面跳转**,即不会使页面重新刷新、加载,带来更高的流畅度
    - **`占用服务端资源少`**,**CSR 渲染** 是交由客户端进行处理,服务端不需要关心渲染计算的过程,减轻了服务端的压力
    - 缺点
    - **`"白屏" 时间较长`**,主要是因为 **CSR** 渲染需要 `*.js` 的支持,而 `*.js` 又必须保证 `*.html` 被接收和解析, `*.html` 又强依赖于当前的 **网络环境**,因此,在差网环境下回导致 **白屏时间过长**,特别是在移动网络环境下
    - **`对 SEO 的支持不友好`**,因为 **白屏时间较长** 导致在一段时间内没有重要的内容能够交由 **搜索引擎** 进行分析、分类、打标签等,并且 **搜索引擎** 并不会等待页面渲染完成,因此对 **SEO** 优化并不友好

    **服务端渲染(server-side rendering,SSR)** 可将相同组件在服务渲染成相应的 `HTML` 字符串,并发送给浏览器进行渲染,即客户端不需要等待所有的 **JavaScript** 都被下载并执行之后才显示,所以用户可以更快看到完整的渲染好的内容。

    ### 预渲染(prerender)

    上述 **服务端渲染(server-side rendering,SSR)** 虽然能够解决一些客户端存在的问题,但它也带来了别的问题:

    - **`需要保证开发一致性`**,比如 **服务端** 和 **客户端** 能够执行的组件生命周期钩子不同,一些外部库在 **服务端渲染** 应用中可能需要经过特殊处理
    - **`需要更多的构建设定和部署要求`**,一个完全静态的 **SPA** 可以部署在任意的静态文件服务器,但服务端渲染应用需要一个能够运行 **Node.js** 服务器的环境
    - **`更多的服务端负载`**,在 **Node.js** 中渲染一个完整的应用,会比仅供应静态文件产生更密集的 **CPU** 运算,并且需要考虑访问流量过大的情况等

    因此,并不是所有应用都合适 **服务端渲染**,如果只是希望通过 **SSR** 来改善一些 **推广页面** (如 `/`、`/about`、`/contact` 等) 的 **SEO**,那么应该优先考虑 **预渲染** 的方式:

    - **预渲染** 是在打包构建过程中(离屏状态),针对对应的 `routes` 路由预先生成对应的页面内容
    - **预渲染** 需要和 **打包构建工具(webpack、rollup 等)** 进行配合,如 [**`webpack`**](https://link.juejin.cn/?target=https%3A%2F%2Fwebpack.js.org%2F),就可通过 [**`prerender-spa-plugin`**](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fchrisvfritz%2Fprerender-spa-plugin) 来支持 **预渲染**

    ## 交互层面

    ### 减少回流/重绘

    **重绘**:页面中元素样式的改变并不影响它在文档流中的位置时(如:`color、background-color、visibility` 等),浏览器会将新样式赋予给元素并 **重新绘制**

    **回流**:当 `Render Tree` 中部分或全部元素的 **尺寸、结构、某些属性** 发生改变时,浏览器 **重新渲染** 部分或全部文档

    - 减少对 `DOM` 进行频繁操作

    - 使经常变动的元素脱离文档流,如具有持续性的动画效果,会一直触发回流和重绘

    - 避免访问或减少访问会导致浏览器

    强制刷新队列

    的属性,如:

    offsetTop、offsetLeft、offsetWidth

    • 扩展】浏览器的渲染队列机制会通过 队列 将会触发 回流或重绘 的操作进行存储,等到一定的时间或一定的数量时再执行这些操作
  • 避免对 css 进行单个修改,如在 JavaScript 修改多个样式时,尽量使用 css 选择器实现样式的集中变更

  • 使用 will-change 开启 GPU 加速,will-change 指定的属性使得浏览器可在元素属性真正发生变化之前提前做好对应的优化

  • 预先设定图片尺寸,避免图片资源加载完成后引发回流

防抖/节流

防抖:多次频繁触发执行操作,以 最后一次 为准,忽略中间过程

节流:在指定的时间间隔内,只允许 执行一次对应的操作

合理使用 防抖/节流 优化应用中的操作,比如 节流 可用于优化 滚动事件、模糊搜索等,防抖 可用于优化一些按钮点击操作等。

Web Worker

JavaScript 是单线程的,如果存在需要大量计算的场景(如视频解码),UI 线程就会被阻塞,甚至浏览器直接卡死。

Web Worker 可以使脚本运行在新的线程中,它们独立于主线程,可以进行大量的计算活动,而不会影响主线程的 UI 渲染,但不能滥用 Web Worker

虚拟列表

最常用的还是 分页加载 的方式:

  • 基于 table 表格的渲染,只会渲染固定数量的 DOM
  • 基于 上拉加载 列表的渲染,随着加载数据的增多,对应的 DOM 节点也会增多,达到某个限制页面一定会发生卡顿

虚拟列表 核心就是固定渲染的 DOM 数,通过动态切换数据内容实现视图的更新,并保证文档中真实 DOM 的数量不随着数据量增大而增大(其实和 table 分页很像,但它支持滚动)。

想了解其核心实现的,可查看 虚拟滚动是怎么做性能优化的?

大文件分片上传

大部分的项目总少不了文件上传功能,但对大文件的上传还是有必要进行优化,所谓的 断点续传秒传 都要基于 分片上传 这个核心功能。

想了解其核心实现的,可查看 请问:怎么实现大文件快速上传?

Excel 导入/导出

针对 Excel 导入/导出 的功能相信很多人第一印象是后端的活,但大多数情况下,后端接口的处理速度会受各种影响,导致速度方面不是很理想,有时候也是需要前端来进行优化处理的,比如导入时前端不发送文件只发送解析后的 JSON 数据,导出时不需要单独发送额外接口,直接使用当前展示数据实现导出等。

想了解其核心实现的,可查看 给我实现一个前端的 Excel 导入和导出功能

Vue 项目的优化

这部分内容相信大家都不陌生,下面就简单列举一些内容(包括但不限于):

  • 减少响应式数据的生成,对于纯展示、又需要使用在 template 模板中使用的数据,可使用 Object.freeze() 进行冻结,避免被转为 不必要的响应式数据
  • Vue 组件初始化是比较损耗性能的,使用 函数式组件 减少组件初始化的过程,适用于实现没有业务逻辑只展示内容的简单组件
  • 合理使用 v-showv-if、为 v-for 组件设定唯一 key(非 index)、v-forv-if 不要一起使用等
  • 使用 KeepAlive 复用组件,避免组件重复的创建、销毁带来的性能损耗
  • 使用 () => import(xxx) 方式实现路由懒加载
  • 使用 ESM 的方式封装自定义工具库等
  • 针对第三方库做到按需引入
  • 合理使用闭包,避免造成内存泄漏
  • 及时清除组件中的副作用,比如 setTimeout、setInterval、addEventListener

基于 vue.config.jswebpack 进行优化,具体可见 如何优化你的 vue-cli 项目?

总结

以上优化方案对应到 上一篇 中提到的性能指标,如下:

  • 首字节达到时间(Time to First Byte,TTFB
  • 首次绘制(First Paint,FP
  • 首次内容绘制(First Contentful Paint,FCP
  • 首屏时间 / 最大内容绘制(Largest Contentful Paint, LCP
  • 累积布局偏移(Cumulative Layout Shift, CLS)
  • 首次输入延迟(First Input Delay, FID

关键资源越早到达客户端,证明 TTFB 时间越短,而这也能间接的减少 FPFCP 的时间;对资源进行了压缩处理意味着能够尽可能提升 LCP 的时间;减少了页面的 回流/重绘 就能使得 CLS 的数值越小,视图越趋于稳定;FID 是一个用于跟踪浏览器对用户输入做出反应之前的延迟时间的指标,包括点击和敲击,保证资源的快速加载和页面尽早渲染,其对应的数值就越小,视图响应就越快。

最后

前端性能优化 的范围实在太大,以上列举的优化主要围绕着 资源加载、页面渲染/交互 两个大的方向,而具体的优化方案其实有很多(包括但不限于上述内容),很多内容随着关注的方向不同而不同。