关于构建SPA项目的一些优化
# 前言
本文将主要介绍一下我个人对
SPA
应用打包优化的拙见以及在实际项目中应该如何去落地这些优化手段
# 关于构建
对于SPA
应用来说,构建是不可绕过的一环,SPA
本质上就是一个html
文件,通过前端模拟路由实现类似多页面网页。这种方式是当下大热的开发模式,也同时衍生除了几大框架比如React
、Vue
、Angular
等。
几大框架中都在使用虚拟DOM
,所有的内容都是由js
来完成的。所以需要通过构建这一步去整合资源
,合理的规划模块之间的依赖关系
。而构建又是一个庞大的工程体系,里头有许多的弯弯绕绕。关于优化手段更是层出不穷,譬如懒加载
,代码分包
,代码压缩
,资源CDN
引入等等。
但其实本质上来说,前端在构建优化这一块要做的其实就一件事,那就是尽可能的减少
包的体积。只有在这件事上对网站的加载速度是实打实无副作用
的优化,至于为什么我说其他操作都会带有副作用
呢,下文会慢慢剖析。
# 优化手段
下面来说一下平常工作中能用到哪些优化手段,以及该如何把它们落地到项目中。下文皆通过vite+vue3+element-plus
项目为例
# 体积优化
这一点其实比较简单,就是尽可能的减少你的包的体积,把不必要的东西都扔掉。该压缩的压缩
,该按需引入的按需引入
。具体的包括本地静态资源的压缩,组件库的按需引入等等。
- 压缩图片:这一步其实
不太适合
在构建阶段做,会影响速度。所以如果非要放在本地的话最好是提前压缩好再放到本地,不过最好还是建议通过cdn
访问。 - 按需引入:vite官方提供了几个
自动引入
组件的插件,用到了某个组件就会自动引入组件和样式,没用到的不会引入。这样可以有效的减少
最终产物的体积
,下面拿element-plus
举例
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()]
})
]
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 加载优化
- 路由的懒加载:这一步相信大家都已经烂熟于心了,由于
SPA
应用的特殊性,懒加载可以有效的减少首屏时间
。首屏不去加载所有的组件,而是只加载必要的内容。他的用法也很简单,只需要在路由表里配置异步导入的函数就行
{
path: '/filesPace',
component: ()=>import('@/views/filePage/index.vue'),
}
2
3
4
tree shaking:大家都称呼他为
摇树优化
,就像摇一颗树一样,把没用的树叶都抖落。同样他的作用就是剔除
一些你没有用上的代码。现在一些构建工具都会默认使用这个优化方案,无需自行配置。下面介绍一下他的原理tree shaking
是利用了es6
的模块化
实现的,es6
模块化的import
语句由于是静态导入
,在编译时
就确定了依赖关系
,所以可以通过代码就分析出哪些代码有用哪些代码无用,也就是只保留你导入的模块里的内容,剔除其他的比如下面这样
import { cloneDeep } from 'lodash' const obj = cloneDeep({})
1
2
3lodash
中有非常非常多的代码,而我们只用到了cloneDeep
这个函数,经过tree shaking
的优化,我们最终的构建产物中就只会有cloneDeep
而不会有其他没用上的代码代码分割:默认情况下
webpack
会将所有的资源都打入一个js
文件中,导致这个js
文件体积非常的大,而vite
虽然尊崇unbundled
,但是最终产物也是一样的,有关联的模块都会打包在一起。而我们要做的就是通过一些分割代码,把这个最终产物拆分
成多个体积更小的产物,来加快加载速度。 在vite
中可以自行配置rollup
的底层分割选项,也可以通过分包插件vite-plugin-chunk-split
来完成
import { chunkSplitPlugin } from 'vite-plugin-chunk-split'
export default defineConfig({
plugins: [
chunkSplitPlugin({
strategy: 'default',
customSplitting: {
'vue-render': ['vue', 'vue-router'],
ElementPlus: ['element-plus'],
echarts: ['echarts', 'vue-echarts'],
},
}),
]
})
2
3
4
5
6
7
8
9
10
11
12
13
14
这样分割之后会把你配置的这些依赖给抽成独立
的js
文件出来,减少最终产物的体积。
- cdn加速:这一步其实和上一步的分割代码差不多,只不过上一步虽然是把代码分割出来了,但最终还是一起放在服务器上,而通过
cdn
加速则是把一些不怎么变动的代码抽离出来放到cdn
上,这样可以减少服务器的压力。同时我们的构建产物体积也会小很多。但是需要你有一个可靠的cdn
不然cdn
挂了你的网站也就挂了
//使用 CDN 也比较简单,一个插件就可以搞定:vite-plugin-cdn-import
// vite.config.js
import { defineConfig } from 'vite'
import viteCDNPlugin from 'vite-plugin-cdn-import'
export default defineConfig({
plugins: [
viteCDNPlugin({
// 需要 CDN 加速的模块
modules: [
{
name: 'lodash',
var: '_',
path: `https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js`
}
]
})
]
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- 预加载资源:这一步优化主要是通过
Preload
和Prefetch
这两个属性来完成,使用方法如下
<head>
...
<link rel="prefetch" as="font" href="<%= require('/assets/fonts/AvenirNextLTPro-Demi.otf') %>" crossorigin>
<link rel="preload" as="font" href="<%= require('/assets/fonts/AvenirNextLTPro-Regular.otf') %>" crossorigin>
...
</head>
2
3
4
5
6
7
preload
表示请求级别更高,需要优先请求
。浏览器在识别到后会优先去请求这个资源。这样我们在下次用到该资源的时候就不用二次请求了
prefetch
则是告诉浏览器,在空闲
时候来加载这些资源。同样也可以实现我们提前加载资源的目的。
关于这两种方案的使用场景和弊端
我会在下文进行解析
- gzip压缩:前面几个方案都是前端做的,接下来几个则需要服务端配合,首先浏览器是支持解析压缩文件,顾名思义,我们可以把资源经过
压缩
然后交由浏览器解析,这样依赖压缩过后的文件体积小了,加载速度也就更快了
import viteCompression from 'vite-plugin-compression'
plugins: [vue(), viteCompression()]
2
3
4
配置完打包你就会看到有.gz
文件的产出,当然还需要服务器支持
// 在nginx配置中
http {
# 启用 Gzip 压缩
gzip on;
gzip_min_length 1000;
gzip_types text/plain text/css application/javascript application/json application/xml application/xml+rss application/x-javascript image/svg+xml;
# 配置 Gzip 压缩级别(可选)
gzip_comp_level 6;
# 配置 Gzip 压缩缓冲区大小(可选)
gzip_buffers 16 8k;
# 配置 Gzip 压缩压缩文件的级别(可选)
gzip_disable "msie6";
# 配置 Gzip 压缩文件的最小大小(可选)
gzip_vary on;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- 启用http2协议:
HTTP2
是HTTP协议的第二个版本,相较于HTTP1
速度更快、延迟更低,功能更多。通常浏览器在传输时并发请求数是有限制的,超过限制的请求需要排队,以往我们通过域名分片、资源合并来避开这一限制,而使用HTTP2
协议后,其可以在一个TCP
连接分帧处理多个请求(多路复用
),不受此限制。(其余的头部压缩
等等也带来了一定性能提升) 如果网站支持HTTPS
,请一并开启HTTP2
,成本低收益高,对于请求多的页面提升很大,尤其是在网速不佳时。
# 用户体验优化
关于体验优化那当然就是在资源未加载过来的时候加loading
了。不然浏览器就会白屏啦。对于spa
应用我们只需要处理首屏的白屏
问题就行了。当首屏加载完毕之后再切换路由即使很慢也不会出现白屏问题了,因为有之前已经加载过的组件兜底了
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<link rel="icon" type="image/svg+xml" href="/vite.svg"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>notCool</title>
<style>
html,
body,
#app {
height: 100%;
}
* {
margin: 0;
padding: 0;
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
"Microsoft YaHei", "微软雅黑", Arial, sans-serif;
}
.preload__wrap {
display: flex;
flex-direction: column;
height: 100%;
letter-spacing: 1px;
background-color: #2f3447;
position: fixed;
left: 0;
top: 0;
height: 100%;
width: 100%;
z-index: 9999;
}
.preload__container {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
width: 100%;
user-select: none;
flex-grow: 1;
}
.preload__name {
font-size: 30px;
color: #fff;
letter-spacing: 5px;
font-weight: bold;
margin-bottom: 30px;
}
.preload__title {
color: #fff;
font-size: 14px;
margin: 30px 0 20px 0;
}
.preload__sub-title {
color: #ababab;
font-size: 12px;
}
.preload__footer {
text-align: center;
padding: 10px 0 20px 0;
}
.preload__footer a {
font-size: 12px;
color: #ababab;
text-decoration: none;
}
.preload__loading {
height: 30px;
width: 30px;
border-radius: 30px;
border: 7px solid currentColor;
border-bottom-color: #2f3447 !important;
position: relative;
animation: r 1s infinite cubic-bezier(0.17, 0.67, 0.83, 0.67),
bc 2s infinite ease-in;
transform: rotate(0deg);
}
@keyframes r {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.preload__loading::after,
.preload__loading::before {
content: "";
display: inline-block;
position: absolute;
bottom: -2px;
height: 7px;
width: 7px;
border-radius: 10px;
background-color: currentColor;
}
.preload__loading::after {
left: -1px;
}
.preload__loading::before {
right: -1px;
}
@keyframes bc {
0% {
color: #689cc5;
}
25% {
color: #b3b7e2;
}
50% {
color: #93dbe9;
}
75% {
color: #abbd81;
}
100% {
color: #689cc5;
}
}
</style>
</head>
<body>
<div class="preload__wrap" id="preload__wrap_Loading">
<div class="preload__container">
<p class="preload__name">NOT-COOL-ADMIN</p>
<div class="preload__loading"></div>
<p class="preload__title">正在加载资源...</p>
<p class="preload__sub-title">初次加载资源可能需要较多时间 请耐心等待</p>
</div>
<div class="preload__footer">
<a href="" target="_blank"> https://暂时还没有官网.com </a>
</div>
</div>
<div id="app" />
<script type="module" src="/src/main.ts"></script>
</body>
</html>
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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
只需要在html
文件里加一个loading
的动画就行了,你可以像我这样选择手动的控制动画的关闭,或者直接把动画放入app
标签中,这样一但组件加载完毕,就会自动替换掉跟标签里的内容,你写的loading
当然也会被抹去。
# 各种优化手段的弊端
- 懒加载:懒加载在
弱网
情况下会出现切换路由缓慢
的问题,因为你的路由组件是用到了才去加载的,所以当弱网情况下就会导致他加载的很慢
,你的页面切换的也就很慢。但是懒加载的本质是为了降低
首屏时间,这其中的利弊就需要自己去取舍了。 - 代码分割:这里其实有点矛盾,代码不分割的话,体积大加载慢。分割的话又会导致请求变多,给浏览器增加负担。这都需要服务器配合比如开启
http2
或者增加带宽等来规避问题。当然大多数情况下这还是一个非常有效的优化手段,利大于弊
。 - cdn引入资源:
cdn
主要就一个稳字,只要你能保证你的cdn
稳,那你就无脑冲。妥妥的减少包的体积又减轻服务器压力 - Preload 和 Prefetch :这俩兄弟也是需要分场景去使用,用得好优化效果杠杠的,没用对,那就是背道而驰,性能越来越差。
- 首先大家要明白,
Preload
是提前加载资源,那就意味着首屏
的时候就会去加载他,这样一来虽然后续再访问资源速度变快了,但是首屏的速度就降低
了。而Prefetch
是利用空闲时间
去加载,这样虽然不会影响到首屏速度,但是由于你不知道用户未来会访问哪个资源,那就只有在空闲时间把剩下的所有
资源都加载过来。这样就会浪费
多余的带宽,加载一些可能用不上的资源。所以关于这两者的使用需要区分场景,对症下药,不可以盲目的使用
- 首先大家要明白,
# 结语
关于优化我想说的是,前端只要尽可能的减少包的体积就是优化的真谛,剩下的服务器的优化效果大于前端的优化。而其中一些优化手段都有各自的利弊,需要自己去权衡什么场景用哪些手段。