前端 性能优化 二
前端性能优化到底是在优化什么?
其实前端性能优化核心就是两点:
- 保证资源更快的 加载速度:达到越快渲染越快,视图展现就越快
- 保证视图更快的 渲染速度/交互速度:用户与页面交互,前提是页面要渲染出来,其次是页面需要尽早反馈,目的就是保证用户良好的体验性
而这些核心内容都可以从下面这个老生常谈的问题中延伸开来。
从输入
URL
到页面加载完成发生了什么?
相信到现在为止,大家对这个问题的回答可以说是能够做到滔滔不绝了吧()!不过每个人回答的方向和重点应该都不一样,比如之前在 如果不能,请忽略
B 站
听 winter
大佬对这个问题的看法和解析的角度是更深、更广的。
在这还是要简单的总结一下核心内容:
- 进行
DNS
解析 - 建立
TCP
连接 - 客户端发送
HTTP
请求 - 服务端响应
HTTP
资源 - 浏览器获取响应内容,进行解析和渲染
以上任意一点都可进行无限扩展、延伸,但点到为止才是现在真正需要的。
性能指标
RAIL 模型
Google
为前端页面性能的评估提出了 RAIL
模型,核心内容如下:
Response
响应Animation
动画Idle
空闲Load
加载
常规性能指标
性能指标其实有不少的内容,但在这我们指列举比较常用的几种:
- 首次绘制(
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
指标可以查看到对应服务器加载资源的相关信息:
可以将鼠标 移动 或 点击 到具体的请求上查看加载时间和加载速度,如下:
鼠标移入:
鼠标点击:
Frames 指标
通过 Frames
指标可以查看页面每一帧渲染时 CPU
所消耗的时间和持续时间 Duration
的信息,如下:
图一:
图二:
Timings 指标
通过 Timings
指标可以查看在上面列举的一些性能指标的值,如下:
- 首次绘制(
First Paint,FP
) - 首次内容绘制(
First Contentful Paint,FCP
) - 首屏时间 / 最大内容绘制(
Largest Contentful Paint, LCP
) HTML
文档被完全加载 和 解析完成的时间(DOMContentLoaded, DCL
)
Main 指标
Main
指标包含了加载过程的三个阶段:
导航阶段
- 主要是处理响应头的数据,并执行一些老页面退出之前的清理操作
解析
1
HTML
文件阶段
- 主要是解析
HTML
数据、解析CSS
数据、执行JavaScript
来生成DOM
和CSSOM
- 主要是解析
生成位图阶段
- 主要是将生成的
DOM
和CSSOM
合并,包括了布局 (Layout
)、分层、绘制、合成等一系列操作
- 主要是将生成的
Lighthouse 面板(Google)
Performance
面板最大的优点就是各种数据信息非常的全,但这也是它最大的缺点,数据信息庞大到需要自行过滤,对于不熟悉的开发者来说,还是需要一定的学习成本的。
相反,Lighthouse
面板中的信息就相对简洁一些,除了检测结果以外,还会提供对应的改进方案,真是考虑得妥妥的,主要检测五个方面的内容:
- Performance(性能)
- Accessibility(可访问性)
- Best practice(最佳实践)
- SEO(搜索引擎优化)
- Progressive Web App(渐进式 Web 应用)
可以通过 Analyze page load
按钮来开始对页面应用进行检测,这里以掘金首页为例:
下面以 Performance 性能 为例简单看一下具体包含的内容,由于篇幅有限,其他内容可自行测试并进行阅读。
Performance 性能(举一反三)
从性能指标的数据来看,只有 累积布局偏移(Cumulative Layout Shift, CLS
) 满足要求,其他指标显示 黄色 和 红色,意味着仍有改进的空间,特别是 首屏时间 是 2.9s
已经是超过了对应的阈值 2.5s
。
性能指标数据如下图所示:
甚至还提供了对应的诊断结果,比如提到的图片没有设置对应的宽高:
Using the Node CLI
甚至还支持在 Node
环境运行,感兴趣的自行去 npm
中查看 文档 即可,这里不过多介绍。
性能指标数据收集
上述性能指标工具的能力已经足够强大,覆盖信息也很全面,但如果我们需要将页面性能指标数据收集并上报又该怎么办呢?
首先排除的肯定是通过 性能指标工具 的方式来收集,一旦要检测性能指标数据意味着得是不同的客户端统计数据的结果合集(除非你愿意一台一台客户端来手动记录和收集数据,呸,你愿意你领导还不愿意呢),最理想的方式当然是自动收集和上报,那就意味着这应该是代码要干的活!!!
既然有这样的需求,那么必定有对应的解决方案,您接着往下看!
Performance API
实际上在浏览器端的全局对象 window
上有一个名为 performance
的属性,它是一个用于支持 IE9
以上及 webkit
内核浏览器中用于记录页面 加载 和 解析 过程中关键时间点的机制,其兼容性在 caniuse
中的表现如下:
下面就简单介绍一下和 window.performance
相关一些核心属性和方法。
performance.timing 属性
performance.timing
属性中提供了很多关键的时间信息,我们可以通过这些时间节点来简单的计算出需要的性能指标数据(不一定准确),计算方式如:
1 | const { |
performance.getEntries() 方法
performance.getEntries()
方法可以获取所有资源请求的时间数据,如下:
点击可查看具体的资源信息,其他属性和上述内容有重复,就不在额外介绍计算方式了,具体如下:
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
相关的指标,其重要核心指标信息如下(一图胜千言):
接下来,让我们通过 npx create-react-app my-react-app
来创建一个 react
项目,然后观察一下它的项目结构:
是不是超级显眼的 reportWebVitals.js
,在进入文件查看你会发现我们需要的核心性能指标都在里面:
就针对优化方案进行总结,核心的方向还是上篇文章中提到的内容:
保证资源更快的 加载速度
:达到越快渲染越快,视图展现就越快保证视图更快的 渲染速度/交互速度
:用户与页面交互,前提是页面要渲染出来,其次是页面需要尽早反馈,目的就是保证用户良好的体验性
其实将上述两点再进行翻译,那么其实指的就是 网络层面的优化 和 浏览器层面的优化,这样看来其实前端性能优化方向还是很明确的,只不过明确的方向中还是会涉及不同方面的具体优化手段。
还是不得不回顾 从输入 URL
到页面加载完成 的核心过程:
- 进行
DNS
解析 - 建立
TCP
连接 - 客户端发送
HTTP
请求 - 服务端响应
HTTP
资源 - 浏览器获取响应内容,进行解析和渲染
从上述的内容来看,不难发现每一步都是需要消耗一定的时间,那么优化的方向就可以围绕着这些内容来考虑。
长文预警❗️❗️❗️ 长文预警❗️❗️❗️ 长文预警❗️❗️❗️ 长文预警❗️❗️❗️
如何保证资源更快的加载速度?
下面内容主要针对 DNS
解析、TCP
连接、HTTP
请求/响应 等阶段来谈的优化,核心优化核心其实就是 网络层面。
使用 dns-prefetch
减少 DNS 的查询时间
dns-prefetch
能够 提前解析 后续可能会用到的 不同域的域名,使解析结果 缓存到系统缓存 中,缩短 DNS
解析时间以提高网站的访问速度。
比如在掘金中的体现如下:
【扩展】
DNS
解析的核心过程
当浏览器访问一个域名时需解析一次 DNS
,以获得对应域名的 IP
地址:
- 浏览器 会从自身的 缓存中 查询是否存在对应域名的
IP
地址,若存在返回,若不存在进入下一步 - 客户机查询操作系统中的
/etc/hosts
文件中查询是否有对应域名的IP
地址,若存在则返回,若不存在进入下一步 - 客户机请求 本地域名服务器(LDNS) 进行解析,本地域名服务器收到客户机的请求后,先查询自己的缓存信息是否有对应域名的
IP
地址,若存在返回结果,没有则进行下一步 - 本地域名服务器请求 根域名服务器 解析该域名,根域名告诉本地域名服务器去找对应的 一级域名服务器
- 本地域名服务器请求一级域名服务器解析这个域名,一级域名服务器告诉它去找对应的 二级域名服务器
- 本地域名服务器请求二级域名服务器解析这个域名,二级域名服务器告诉它去找对应的 子域名服务器
- 本地域名服务器请求子域名服务器解析这个域名,子域名服务器返回对应的 IP 地址
- 本地域名服务器将
IP
地址记录到缓存中,并返回给客户机(会缓存起来),客户机根据收到的IP
地址访问该网站
使用 preconnect
提前建立连接
preconnect
的作用是提前和第三方资源建立连接,设置了它浏览器就会做好早期的连接工作,但这个连接通常只会维持 10 s
。
比如在当前域请求一个资源前,可能会涉及 DNS
寻址、TLS
握手、TCP
握手、重定向等,这过程也会花费一定的时间。
比如在掘金中的体现如下:
使用 preload / prefetch
预先加载资源
preload
preload
的作用是提前加载页面对应的 关键资源 加快页面的渲染,preload
的优先级顺序和 as
属性相关,具体可见。
【注意】
as
属性一定要设置,除了上面提到的设置优先级外,还涉及到浏览识别的问题:如果没有设置as
属性,后续遇到该请求就会被作为一个XHR
请求,把意味着资源预加载的功能可能会失效,因为可能会每次都发起新的请求获取
比如在掘金中的体现如下:
比如在 vue-cli
的默认 webpack
配置,如下:
prefetch
preload
是对资源的预加载,它虽提前加载但只在需要执行时执行,即这个资源一定是当前页面所需要的资源,如果是需要为下一个页面提前加载资源,那么应该使用 prefetch
,它会在 浏览器空闲时 下载资源。
比如在 vue-cli
的默认 webpack
配置,如下:
压缩资源体积
资源是需要通过 http
数据包的方式在网络中进行传输的,那么只要能减少传输数据包的体积,也是能够使得资源更快到达客户端,这也是压缩资源体积的核心目的。
HTTP 压缩
HTTP 压缩中一个典型代表就是 gzip
,它是一种优秀的压缩算法,可对 http
请求中的一些文件资源进行压缩处理,一般来讲是要在服务端处理的,可通过在响应头中设置 Content-encoding: gzip
表示当前资源使用的压缩方式(如:gzip、deflate、br
等),便于客户端使用正确的方式解压。
【注意】
gzip
并不是万能的,它不能保证针对每个文件的压缩都能使其体积变小,关于Content-Encoding
的内容 可点此查阅,或者可参考 content-encoding 除了gzip之外,你还知道哪些?
比如在京东中的体现:
比如在掘金中的体现:
Webpack 压缩
有 HTTP
压缩 不就够了吗?为什么还需要 Webpack
压缩?
首先必须要明确的是压缩的过程本身就是会消耗时间的,如果所有资源都等到被访问的时候再由服务端进行压缩,在压缩完成之前客户端还是得处于等待状态,即仍 不能保证资源以最快的速度到达客户端。
那么优化方案就是将压缩资源的时间放到打包构建中,毕竟只有真正需要发布线上生产环境时才需要执行一系列的打包优化的操作,而这相比于 http
的 请求/响应 速度,稍微延长产物打包时间没有什么大问题。
下面会列举一些 Webpack 插件,但并不会去讲其中的具体用法,因为这些只是达到目的的不同方案而已,每个方案要是细讲都可以独占一篇文章,在这是没有必要的,具体用法可自行查阅。
使用
CompressionPlugin
压缩文件
webpack
文档 提供插件合集中就包含了该插件,它的作用就是:Prepare compressed versions of assets to serve them with Content-Encoding.
使用
HtmlWebpackPlugin
压缩HTML
文件
通常我们需要 HtmlWebpackPlugin
插件来生成对应 HTML
或 对已有的 HTML
模板自动注入 webpack bundles
资源,除此之外,它还可配置 minify
选项实现压缩模板的目的。
可以在 vue
项目下执行 vue inspect --mode production > webpack.config.js
来查看脚手架的默认 webpack
配置内容,比如:
使用
SplitChunksPlugin
自定义分包策略
Webpack
默认会将尽可能多的模块代码打包在一起,这种默认规则的带来的优点和缺点都很明显:
- 优点:能减少最终页面的
HTTP
请求数 - 缺点:
- 页面初始代码包过大,影响首屏渲染性能
- 无法有效应用浏览器缓存
SplitChunksPlugin
是 Webpack 4
之后内置实现的最新分包方案,与 Webpack 3
中的 CommonsChunkPlugin
相比,它能够基于一些更灵活、合理的启发式规则将 Module
编排进不同的 Chunk
,最终构建出 性能更佳、缓存更友好 的应用产物。
比如在 vue-cli
的默认 webpack
配置,如下:
使用
MiniCssExtractPlugin
抽离和压缩CSS
MiniCssExtractPlugin
会将 CSS
提取到单独的文件中,为每个包含 CSS
的 JS
文件创建一个 CSS
文件,并且支持 CSS
和 SourceMaps
的 按需加载 。
比如在 vue-cli
的默认 webpack
配置,如下:
使用
ImageMinimizerWebpackPlugin
压缩图片资源
图片仍是一个 Web
应用中的必不可少的资源,而图片资源的体积也是首屏页面加载的瓶颈之一,因此,压缩图片也是性能优化需要考虑的内容。
ImageMinimizerWebpackPlugin
可用于使用 优化/压缩 所有图像,它可以支持 无损(不损失质量)、有损(质量下降) 两种模式的压缩方式。
通过
Tree Shaking
移除无用代码
Tree Shaking
依赖于 ES6
模块语法的 静态结构 特性(如: import
和 export
),当 webpack
的模式 mode
为 "production"
时,就可以启用 更多优化项,包括 压缩代码 与 Tree Shaking。
但同时我们就必须保证:
- 尽量使用
ES6
模块语法,即import
和export
- 保证没有 编译器(如:
babel
)将对应的ES6
模块语法转换为CommonJS
的语法(如:@babel/preset-env
的默认行为) - 可在项目的
package.json
文件中添加"sideEffects"
属性,标识当前内容是否存在副作用操作 - 可在通过
/*#__PURE__*/
注释,将函数调用标记为无副作用
减少 http 请求数量
不同协议 下 请求数量 仍然可能成为 请求/响应 慢的原因:
- 合并公共资源,如 雪碧图 等
- 内置模块资源,如 生成
base64
图片、通过symbol
引用svg
等 - 合并代码块,如构建工具分包策略配合 公共组件封装、组件复用逻辑抽离 等
- 按需加载资源,如 路由懒加载、图片懒加载、上拉加载、分页加载 等
减少不必要的 cookie
不必要的 cookie
来回传输会造成带宽浪费:
- 减少
cookie
存储的内容 - 对于静态资源采用
CDN
托管(即非同域),不同域名默认不携带cookie
CDN 托管静态资源 + HTTP 缓存
CDN
加速的本质是缓存加速,将服务器上存储的静资源容缓存在 CDN
节点上,当后续访问这些静态内容时,无需访问服务器源站,选择就近访问 CDN
节点即可,从而达到加速的效果,同时减轻服务器源站的压力。
在掘金中体现如下:
协议升级为 Http2.0
http1.x
存在的问题:HTTP
的底层协议是TCP
,而TCP
是面向连接即需要 三次握手 才能建立连接,其中:
http1.0
中使用的是 短连接,即 一次请求/响应 结束后就会断开连接,这个过程比较耗时http1.1
Connection: keep-alive1
2
3
4
5
6
7
8
9
10
11
中使用的是
长连接
,在
请求/响应头
中设置TCP1
2
3
4
5
6
7
即可开启,优点是
长连接
允许多个请求共用一个http2.01
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`
数据大的问题,header1
2
3
中传输的HPACK1
2
3
4
5
6
7
8
9
10
11
帧经过处理后会用
二进制
的方式表示,替换了原本的
文本格式
,并使用offsetTop、offsetLeft、offsetWidth1
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` 进行频繁操作
- 使经常变动的元素脱离文档流,如具有持续性的动画效果,会一直触发回流和重绘
- 避免访问或减少访问会导致浏览器
强制刷新队列
的属性,如:等
- 【扩展】浏览器的渲染队列机制会通过 队列 将会触发 回流或重绘 的操作进行存储,等到一定的时间或一定的数量时再执行这些操作
避免对
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-show
和v-if
、为v-for
组件设定唯一key
(非index
)、v-for
和v-if
不要一起使用等 - 使用
KeepAlive
复用组件,避免组件重复的创建、销毁带来的性能损耗 - 使用
() => import(xxx)
方式实现路由懒加载 - 使用
ESM
的方式封装自定义工具库等 - 针对第三方库做到按需引入
- 合理使用闭包,避免造成内存泄漏
- 及时清除组件中的副作用,比如
setTimeout、setInterval、addEventListener
等
基于 vue.config.js
或 webpack
进行优化,具体可见 如何优化你的 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
时间越短,而这也能间接的减少 FP
和 FCP
的时间;对资源进行了压缩处理意味着能够尽可能提升 LCP
的时间;减少了页面的 回流/重绘 就能使得 CLS
的数值越小,视图越趋于稳定;FID
是一个用于跟踪浏览器对用户输入做出反应之前的延迟时间的指标,包括点击和敲击,保证资源的快速加载和页面尽早渲染,其对应的数值就越小,视图响应就越快。
最后
前端性能优化 的范围实在太大,以上列举的优化主要围绕着 资源加载、页面渲染/交互 两个大的方向,而具体的优化方案其实有很多(包括但不限于上述内容),很多内容随着关注的方向不同而不同。