共计 8888 个字符,预计需要花费 23 分钟才能阅读完成。
Vue 项目优化总结
- 代码优化
- Webpack 配置优化
- 其它优化
代码
v-show 和 v-if 区分使用
v-show 根据表达式之正假值,切换元素display
CSS property。当条件变化时该指令触发过渡效果。适用于频繁操作,不会触发浏览器的重排。
v-if 根据表达式的值 truthiness 来有条件地渲染元素。在切换时元素及它的数据绑定 / 组件被销毁并重建。如果是 <template>
元素,则提出它的内容作为条件块。当条件变化时该指令触发过渡效果。适用于不频繁操作,它会动态添加 / 删除节点,触发浏览器的重排。
computed 和 watch 区分使用
computed 计算属性依赖其内已被依赖的属性,结果会被缓存,除非依赖的响应式 property 变化才会重新计算。注意,如果某个依赖 (比如非响应式 property) 在该实例范畴之外,则计算属性是 不会 被更新的。多用于进行数据计算、vuex 数据等。
watch 当数据发生改变时会执行监听回调。多用于监听改变后执行对应操作。注意数据相互依赖且没有写好终止条件则会死循环。
隔绝观察
当复杂数据被依赖改变时,深拷贝数据进行隔绝观察,不让每次的改变都进行 UI 的重新渲染。下面是个例子:
<script>
export default {data() {
return {
data: [{ a: 1, b: 2, c: 3},
{a: 1, b: 2, c: 3},
{a: 1, b: 2, c: 3},
]
}
},
methods: {asnyc handle() {for (let i = 0; i < this.data.length; i++) {await this.modifyItem(this.data[i]); // 这里异步改动数据
}
},
modifyItem(row) {return new Promise((resolve, reject) => {setTimeout(() => {row.a += 1;}, 30);
});
}
}
}
</script>
上面代码每次异步改变数据后 UI 被渲染,但其实这里并不想渲染。可以将代码改成下面这样:
...
async handle() {const newData = JSON.parse(JSON.stringify(this.data));
for (let i = 0; i < newData.length; i++) {await this.modifyItem(newData[i]); // 这里异步改动数据
}
// 等待数据全部修改完后再进行赋值
this.data = newData;
}
...
大数组优化 1
当前组件如果只是为纯展示组件时,拿到数据后使用 Object.freeze()
将数据冻结,这样数据就无法进行响应变化。
注意:冻结后无法解冻。
大数组优化 2
当组件处于非常长的列表时,数据过多导致 DOM 元素同样多,导致卡顿。
方法 1:如果是 Select
组件的话可以使用滚动加载配合搜索,可以看看我这篇文章的处理方式 el-select 数据过多懒加载
方法 2:使用业界常用手段 虚拟滚动
,只渲染可以看到的窗口的区域 DOM。参看开源项目 vue-virtual-scroll-list
事件销毁
Vue 组件销毁时,实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。单独添加的监听事件是不会移除的,需要手动移除事件的监听,以免造成内存泄漏。
created() {document.addEventListener('scroll', this.onScroll, false);
},
beforeDestory() {document.removeEventListener('scroll', this.onScroll, false);
}
路由懒加载
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效。
const Foo = () => import('./Foo.vue');
const router = new VueRouter({
routes: [{ path: '/foo', component: Foo}
]
});
Webpack 配置优化
图片压缩
Webpack 配置中 url-loader 中设置 limit
来对小于 limit
的图片转化为 base64 格式,其余不作处理,对于这些剩余的大图在资源加载的时候会很慢,使用 image-webpack-loader
压缩图片。
yarn add image-webpack-loader --dev
在 webpack.config.js
中:
rules: [{test: /\.(gif|png|jpe?g|svg)$/i,
use: [
'file-loader',
{
loader: 'image-webpack-loader',
options: {
bypassOnDebug: true, // webpack@1.x
disable: true, // webpack@2.x and newer
},
},
],
}]
按组件分割代码
有时候我们想把某个路由下的所有组件都打包在同个异步块 (chunk) 中。只需要使用 命名 chunk,一个特殊的注释语法来提供 chunk name (需要 Webpack > 2.4)。多个小组件代码合并在一起减少请求次数。
const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')
减少 Babel 编译后的冗余代码
Babel 插件会在代码装换生成 ES5 代码时注入一些辅助函数,例如下面的代码:
class Component1 extends CompoentBase {}
转换成浏览器可以正常执行的 ES5 代码时需要下面两个辅助函数:
babel-runtime/helpers/createClass // 实现 class 语法
babel-runtime/helpers/interits // 实现 extends 语法
默认情况下,Babel 将在每个输出的文件中加入这些依赖的辅助函数代码,如果多个源代码文件都依赖这些辅助函数,那么这些辅助函数会出现多次,造成冗余。
避免这种冗余我们可以在依赖它们时通过 require('babel-runtime/helpers/createClass')
的方式,这样它们就只出现一次。
babel-plugin-transform-runtime
插件就是实现这个作用的,将相关辅助函数替换成导入语句,解决 Babel 编译装换后的冗余。
yarn add babel-plugin-transform-runtime --dev
在 .babelrc
中:
"plugins": ["transform-runtime"]
提取公共代码
每个页面都有第三方库和公共模块,会存在下面问题:
- 相同资源重复加载,浪费资源
- 每个页面打开都要加载这些资源,加载时间变长
所以将公共模块代码抽离成单独的文件,Webpack 提供了相应的功能 optimization.splitChunks
,在webpack.config.js
中:
optimization: {
splitChunks: {
cacheGroups: {
default: {
name: 'common',
chunks: 'initial',
minChunks: 2 // 模块被引用 2 次以上的才抽离
}
}
}
}
- name: 提取出来的公共模块将会以这个来命名,可以不配置,如果不配置,就会生成默认的文件名,大致格式是
index~a.js
这样的 - chunks: 指定哪些类型的 chunk 参与拆分,值可以是 string 可以是函数。如果是 string,可以是这个三个值之一:
all
,async
,initial
,all
代表所有模块,async
代表只管异步加载的,initial
代表初始化时就能获取的模块。如果是函数,则可以根据 chunk 参数的 name 等属性进行更细致的筛选 - minChunks: 模块被引用多少次以上的才抽离
提取第三方库
第三方库使用 CDN 预加载 +externals
+HtmlWebpackPlugin
处理,有这么几点好处:
- 不需要每个页面都去加载
- 加快项目打包速度
- 加快项目热更新速度
在
vue-cli
中的配置,Webpack 配置基本一样只是写法不同,vue-cli 也是封装了 Webpack。文件vue.config.js
中:
...
// 防止将某些 import 的包 (package) 打包到 bundle 中,而是在运行时 (runtime) 再去从外部获取这些扩展依赖(external dependencies)
const externals = {
vue: 'Vue',
'vue-router': 'VueRouter',
vuex: 'Vuex',
// 其它自己项目中的第三方库
}
// 区分不同环境,
// 根据自己项目不同的配置
const cdnDict = {
dev: {
css: ['https://cdn.bootcss.com/nprogress/0.2.0/nprogress.min.css',],
js: ['https://cdn.bootcss.com/tinymce/4.9.2/tinymce.min.js',]
},
build: {
css: ['//cdn.kuguanwang.com/jxc/public/css/nprogress.min.css'],
js: [
'//cdn.kuguanwang.com/jxc/public/js/vue.min.js',
'//cdn.kuguanwang.com/jxc/public/js/vue-router.min.js',
'//cdn.kuguanwang.com/jxc/public/js/vuex.min.js',
]
}
}
...
chainWebpack: config => {
// 添加 cdn 参数到 HtmlWebpackPlugin 中,在 public/index.html 中使用
config.plugin('html').tap(args => {if (process.env.NODE_ENV === 'production') {args[0].cdn = cdn.build;
} else if (process.env.NODE_ENV === 'development') {args[0].cdn = cdn.dev;
}
return args;
});
}
...
configureWebpack: config => {if (['production', 'development'].includes(process.env.NODE_ENV)) {config.externals = externals;}
return config;
}
public/index.html
中加入:
<head>
...
<!-- 使用 CDN 加速的 CSS 文件,配置在 vue.config.js 下 -->
<% for (var i in htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.css) { %>
<link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="preload" as="style">
<link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="stylesheet">
<% } %>
<!-- 使用 CDN 加速的 JS 文件,配置在 vue.config.js 下 -->
<% for (var i in htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.js) { %>
<link href="<%= htmlWebpackPlugin.options.cdn.js[i] %>" rel="preload" as="script">
<% } %>
...
</head>
提取单文件组件的 CSS
在使用单文件组件时,组件内的 CSS 会议 style 标签的形式通过 JavaScript 动态注入。
这里会有一些小的运行时的开销,如果使用服务端渲染,会导致 文档样式短暂失效 简称为 FOUC。将所有组件的 CSS 提取到同一个文件可以避免该问题,也可以更好的进行压缩和缓存。
yarn add extract-text-webpack-plugin --dev
在 webpack.config.js
中:
var ExtractTextPlugin = require('extract-text-webpack-plugin');
module.export = {
...
module: {
rules: [{
test: /\.vue$/,
loader: 'vue-loader',
options: {extractCSS: true,}
}]
},
plugin: [new ExtractTextPlugin('style.css')
]
}
优化 SourceMap
由于打包后的文件经过了压缩、合并、混淆、babel 编译后的代码不利于定位分析 bug。
查看文档 devtool 选择合适自己的可选值,不同的值会明显影响到构建 (build) 和重新构建 (rebuild) 的速度。
如果要加速打包速度可以选择生产环境时不生成 SourceMap
输出文件分析
使用 webpack-bundle-analyzer
可以将我们打包后的文件进行图形化的方式展示,便于分析问题。
- 打包后的文件中都有什么文件
- 每个文件在总大小的占比,过大的话可以针对做优化
- 模块之间包含关系
- 是否有重复的依赖项
- 每个文件的大小(包含 gzip 等)
编译优化
上面其实已经包含了编译优化的几个内容,再补充一些其它:
优化 Babel 转换
babel-loader 转换文件很耗时,我们只需要让它转换必须的部分:
- 优化正则匹配
- 开启缓存 cacheDirectory
- 减少包含文件 include、exclude
优化前:
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test')]
}
优化后
{
// 只有 js 文件则不要写成 /\.jsx?$/,提升正则表达式性能
test: /\.js$/,
// 开启 babel-loader 的缓存转换
loader: 'babel-loader?cacheDirectory',
// 只对 src 文件夹下文件进行转换
include: [resolve('src')]
}
优化 resolve.alias 配置
创建 import
或 require
的别名,来确保模块引入变得更简单。webpack.config.js
:
module.exports = {
...
resolve: {
alias: {'@': resolve('src'),
}
}
};
引入模块:
import Button from '../../../components/Button.tsx'; // 无别名
import Button from '@/components/Button.tsx'; // 使用别名
优化 resolve.modules 配置
resolve.modules 用于配置 Webpack 去哪些目录寻找第三方模块。
默认值是[‘node_modules’], 告诉 webpack 解析模块时应该搜索的目录。
绝对路径和相对路径都能使用,但是要知道它们之间有一点差异。
通过查看当前目录以及祖先路径(即 ./node_modules
, ../node_modules
等等),相对路径将类似于 Node 查找 ‘node_modules’ 的方式进行查找。
这里使用绝对路径,在给定目录中搜索,减少搜索步骤
mudule.export = {
...
resolve: {modules: [path.resolve(__dirname, 'node_modules')]
}
}
优化 resolve.extensions 配置
自动解析确定的扩展。导入文件没有文件类型后轴时,会自动带上后缀查询文件是否存在。
默认为['.wasm', '.mjs', '.js', '.json']
,如果文件没有后缀则会依次找不同类型文件,到最后还没有则报错。列表越长,尝试次数越多。
所以 resolve.extensions
的配置也会影响性能
优化 module.noParse 配置
防止 webpack 解析那些任何与给定正则表达式相匹配的文件。忽略的文件中 不应该含有 import
, require
, define
的调用,或任何其他导入机制。忽略大型的 library 可以提高构建性能。
webpack.config.js
:
module.export = {
...
module: {
noParse: /jquery|lodash/, // 写法 1 正则
noParse: (content) => /jquery|lodash/.test(content), // 写法 2 函数 返回值为 Boolean
}
}
开启多进程
当项目代码体积越来越大之后,编译打包的时候也会越来越久,开启多进程将任务分解给多个子进程并发执行,子进程执行完后将结果再返回主进程
JS 可以使用 HappyPack 或者 thread-loader
CSS 开启 ’uglifyjs-webpack-plugin’ 的 parallel
参数
使用不同环境变量和模式
vue-cli
构建的(Webpack 需要自己改动)项目根目录中的下列文件来指定环境变量:
.env # 在所有的环境中被载入
.env.local # 在所有的环境中被载入,但会被 git 忽略
.env.[mode] # 只在指定的模式中被载入
.env.[mode].local # 只在指定的模式中被载入,但会被 git 忽略
一个环境文件只包含环境变量的“键 = 值”对:
FOO=bar
VUE_APP_SECRET=secret
例如:项目中有不同的打包环境,预发布、发布,会有不同的代码配置
根目录新增文件env.uat
NODE_ENV = production
VUE_APP_NAME = uat
package.json
中
...
"build": "vue-cli-server build",
"build:uat": "vue-cli-server build --mode uat",
业务代码中:
if (process.env.VUE_APP_NAME === 'uat') {// 执行特定处理}
开启 Gzip
Gzip是一种压缩文件格式并且也是一个在类 Unix 上的一种文件解压缩的软件,通常指 GNU 计划的实现,此处的 gzip 代表 GNU zip。也经常用来表示 gzip 这种文件格式。主流浏览器和常用 Web 服务器都支持。
yarn add compression-webpack-plugin --dev
vue.config.js
中
var CompressionWebpackPlugin = require('compression-webpack-plugin');
...
configureWebpack: config => {
config.plugins.push(
new CompressionWebpackPlugin({test: new RegExp('\\.(js|css)$'),
threshold: 8192,
minRatio: 0.8
})
)
nginx.conf
中
#开启和关闭 gzip 模式
gzip on|off;
#gizp 压缩起点,文件大于 1k 才进行压缩
gzip_min_length 1k;
# gzip 压缩级别,1-9,数字越大压缩的越好,也越占用 CPU 时间
gzip_comp_level 1;
# 进行压缩的文件类型。gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript ;
# nginx 对于静态文件的处理模块,开启后会寻找以.gz 结尾的文件,直接返回,不会占用 cpu 进行压缩,如果找不到则不进行压缩
gzip_static on|off
# 是否在 http header 中添加 Vary: Accept-Encoding,建议开启
gzip_vary on;
# 设置压缩所需要的缓冲区大小,以 4k 为单位,如果文件为 7k 则申请 2 *4k 的缓冲区
gzip_buffers 2 4k;
# 设置 gzip 压缩针对的 HTTP 协议版本
gzip_http_version 1.1;
重启 Nginx 后,可以看到 Network 响应头中Content-Encoding: gzip
静态资源 CDN
内容分发网络(英语:Content Delivery Network 或Content Distribution Network,缩写:CDN)是指一种透过互联网互相连接的电脑网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、影片、应用程序及其他文件发送给用户,来提供高性能、可扩展性及低成本的网络内容传递给用户。
打包后的资源要上传到 CDN 服务中,在 vue.config.js
中:
function getPublicPath() {
// 根据不同环境变量处理
if (process.env.VUE_APP_NAME === 'uat') {return '//cdn.example.com/';} else {return './';}
}
module.exports = {
...
publicPath: getPublicPath(), // 部署应用包时的基本 URL
其它优化
- iconfont 代替图片
- 浏览缓存
- 开启 Http2
- 服务端渲染
- 加入骨架屏
- 资源预加载
rel="prefetch"
- 避免重绘、重排
- 使用 GPU
参考文献
- webpack 优化之玩转代码分割和公共代码提取
- Vue 项目性能优化 — 实践指南(网上最全 / 详细)