我们在开发 Vue 项目时候都知道,在 vue 开发中某些问题如果前期忽略掉,当时不会出现明显的效果,但是越向后开发越难做,而且项目做久了就会出现问题,这就是所说的蝴蝶效应,这样后期的维护成本会非常高,并且项目上线后还会影响用户体验,也会出现加载慢等一系列的性能问题,下面举一个简单的例子。
举个简单的例子
如果加载项目的时候加载一张图片需要 0.1s,其实算不了什么可以忽略不计,但是如果我有 20 张图片,这就是 2s 的时间, 2s 的时间不算长一下就过去了,但是这仅仅的只是加载了图片,还有我们的 js,css 都需要加载,那就需要更长的时间,可能是 5s,6s...,比如加载时间是 5s,用户可能等都不会等,直接关闭我们的网站,最后导致我们网站流量很少,流量少就没人用,没人用就没有钱,没有钱就涨不了工资,涨不了工资最后就是跑路了😂。通过上面的例子可以看出性能问题是多么的重要甚至关系到了我们薪资😂,那如何避免这些问题呢?废话不多说,下面分享一下自己在写项目的时用到的一些优化方案以及注意事项。
1.不要将所有的数据都放在 data 中
可以将一些不被视图渲染的数据声明到实例外部然后在内部引用引用,因为 Vue2 初始化数据的时候会将 data 中的所有属性遍历通过Object.definePrototype
重新定义所有属性;Vue3 是通过 Proxy 去对数据包装,内部也会涉及到递归遍历,在属性比较多的情况下很耗费性能
<template>
<button @click="updateValue">{{msg}}</button>
</template>
<script>
let keys=true;
export default {
name:'Vueinput',
data(){
return {
msg:'true'
}
},
created(){
this.text = 'text'
},
methods:{
updateValue(){
keys = !keys
this.msg = keys?'true':'false'
}
}
}
</script>
复制代码
2.watch 尽量不要使用 deep:true 深层遍历
因为 watch 不存在缓存,是指定监听对象,如果 deep:true,并且监听对象类型情况下,会递归处理收集依赖,最后触发更新回调
3. vue 在 v-for 时给每项元素绑定事件需要用事件代理
vue 源码中是通过 addEventLisener 去给 dom 绑定事件的,比如我们使用 v-for 需要渲染 100 条数据并且并为每个节点添加点击事件,如果每个都绑定事件那就存在很多的 addEventLisener,这里不用说性能上肯定不好,那我们就需要使用事件代理处理这个问题
<template>
<ul @click="EventAgent">
<li v-for="(item) in mArr" :key="item.id" :data-set="item">{{item.day}}</li>
</ul>
</template>
<script>
let keys=true;
export default {
name:'Vueinput',
data(){
return {
mArr:[{
day:1,
id:'xx1'
},{
day:2,
id:'xx2'
},{
day:2,
id:'xx2'
},
...
]
}
},
methods:{
EventAgent(e){
// 注意这里 在项目中千万不要写的这么简单,我只是为了方便理解才这么写的
console.log(e.target.getAttribute('data-set'))
}
}
}
</script>
复制代码
4. v-for 尽量不要与 v-if 一同使用
vue 的编译过程是 template->vnode,看下面的例子
// 假设data中存在一个arr数组
<div id="container">
<div v-for="(item,index) in arr" v-if="arr.length" key="item.id">{{item}}</div>
</div>
复制代码
上面的例子有可能大家经常这么做,其实这么做也能达到效果但是在性能上面不是很好,因为 Ast 在转化为 render 函数的时候会将每个遍历生成的对象都会加入 if 判断,最后在渲染的时候每次都每个遍历对象都会判断一次需要不需要渲染,这样就很浪费性能,为了避免这个问题我们把代码稍微改一下
<div id="container" v-if="arr.length">
<div v-for="(item,index) in arr" >{{item}}</div>
</div>
复制代码
这样就只判断一次就能达到渲染效果了,是不是更好一些那
参考 前端进阶面试题详细解答
5. v-for 的 key 进行不要以遍历索引作为 key
<template>
<ul>
<li v-for="(item,index) in mArr" :key="item.id
<input type="checkbox" :value="item.is" />
</li>
<button @click="remove">
移除
</button>
</ul>
</template>
<script>
let keys=true;
export default {
name:'Vueinput',
data(){
return {
mArr:[{is:false,id:1},{is:false,id:2}
]
}
},
methods:{
remove(e){
console.log('asd')
this.mArr.shift()
}
}
}
</script>
复制代码
<template>
<ul>
<li v-for="(item,index) in mArr" :key="index">
<input type="checkbox" :value="item.is" />
</li>
<button @click="remove">
移除
</button>
</ul>
</template>
<script>
let keys=true;
export default {
name:'Vueinput',
data(){
return {
mArr:[{is:false,id:1},{is:false,id:2}
]
}
},
methods:{
remove(e){
console.log('asd')
this.mArr.shift()
}
}
}
</script>
复制代码
还是选中状态,就很神奇,解释一下为什么这么神奇,因为我们选中的是 0 索引,然后点击移除后索引为 1 的就变为 0,Vue 的更新策略是复用 dom,也就是说我索引为 1 的 dom 是用的之前索引为 0 的 dom 并没有更改,当然没有 key 的情况也是如此,所以 key 值必须为唯一标识才会做更改
6. SPA 页面采用 keep-alive 缓存组件
<template>
<div>
<keep-alive>
<router-view></router-view>
</keep-alive>
</div>
</template>
复制代码
使用了 keep-alive 之后我们页面不会卸载而是会缓存起来,keep-alive 底层使用的 LRU 算法(淘汰缓存策略),当我们从其他页面回到初始页面的时候不会重新加载而是从缓存里获取,这样既减少 Http 请求也不会消耗过多的加载时间
7. 避免使用 v-html
可能会导致 xss 攻击
v-html 更新的是元素的 innerHTML 。内容按普通 HTML 插入, 不会作为 Vue 模板进行编译 。
8. 提取公共代码,提取组件的 CSS
将组件中公共的方法和 css 样式分别提取到各自的公共模块下,当我们需要使用的时候在组件中使用就可以,大大减少了代码量
9. 首页白屏-loading
当我们第一次进入 Vue 项目的时候,会出现白屏的情况,为了避免这种尴尬的情况,我们在 Vue 编译之前使用加载动画避免
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>Vue</title>
<style>
</style>
</head>
<body>
<noscript>
<strong>We're sorry but production-line doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app">
<div id="loading">
loading
</div>
</div>
<!-- built files will be auto injected -->
</body>
</html>
复制代码
加 loading 只是解决白屏问题的一种,也可以缩短首屏加载时间,就需要在其他方面做优化,这个可以参考后面的案例
10. 拆分组件
主要目的就是提高复用性、增加代码的可维护性,减少不必要的渲染,对于如何写出高性能的组件这里就不展示了,自己可以多看看那些比较火的 UI 库(Element,Antd)的源码
11. 合理使用 v-if 当值为 false 时内部指令不会执行,具有阻断功能
如果操作不是很频繁可以使用 v-if 替代 v-show,如果很频繁我们可以使用 v-show 来处理
key 保证唯一性 ( 默认 vue 会采用就地复用策略 )
上面的第五条已经讲过了,如果 key 不是唯一的情况下,视图可能不会更新。
12. 获取 dom 使用 ref 代替 document.getElementsByClassName
mounted(){
console.log(document.getElementsByClassName(“app”))
console.log(this.$refs['app'])
}
复制代码
document.getElementsByClassName 获取 dom 节点的作用是一样的,但使用 ref 会减少获取 dom 节点的消耗
13. Object.freeze 冻结数据
首先说一下Object.freeze
的作用
不能添加新属性
不能删除已有属性
不能修改已有属性的值
不能修改原型
不能修改已有属性的可枚举性、可配置性、可写性
data(){
return:{
objs:{
name:'aaa'
}
}
},
mounted(){
this.objs = Object.freeze({name:'bbb'})
}
复制代码
使用 Object.freeze 处理的 data 属性,不会被 getter,setter,减少一部分消耗,但是Object.freeze
也不能滥用,当我们需要一个非常长的字符串的时候推荐使用
14. 合理使用路由懒加载、异步组件
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。然后当路由被访问的时候才加载对应组件,这样就更加高效了
// 未使用懒加载的路由
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "@/views/Home";
import About from "@/views/About";
Vue.use(VueRouter);
const routes = [
{
path: "/",
name: "Home",
component: Home,
},
{
path: "/about",
name: "About",
component: About
}
];
// 使用懒加载
import Vue from "vue";
import VueRouter from "vue-router";
Vue.use(VueRouter);
const Home = () => import('../views/Home')
const About = () => import('../views/About')
const routes = [
{
path: "/",
name: "Home",
component: Home,
},
{
path: "/about",
name: "About",
component: About
}
];
复制代码
15. 数据持久化的问题
数据持久化比较常见的就是 token 了,作为用户的标识也作为登录的状态,我们需要将其储存到localStorage
或sessionStorage
起来每次刷新页面 Vuex 从localStorage
或sessionStorage
获取状态,不然每次刷新页面用户都需要重新登录,重新获取数据
localStorage 需要用户手动移除才能移除,不然永久存在。
sessionStorage 关闭浏览器窗口就失效。
cookie 关闭浏览器窗口就失效,每次请求 Cookie 都会被一同提交给服务器。
16. 防抖、节流
这两个算是老生常谈了,就不演示代码了,下面介绍一个场景,比如我们注册新用户的时候用户输入昵称需要校验昵称的合法性,考虑到用户输入的比较快或者修改频繁,这时候我们需要使用节流,间隔性的去校验,这样就减少了判断的次数达到优化的效果。后面我们还需要需要用户手动点击保存才能注册成功,为了避免用户频繁点击保存并发送请求,我们只监听用户最后一次的点击,这时候就用到了节流操作,这样就能达到优化效果
17. 重绘,回流
触发重绘浏览器重新渲染部分或者全部文档的过程叫回流
频繁操作元素的样式,对于静态页面,修改类名,样式
使用能够触发重绘的属性(background,visibility,width,height,display 等)
触发回流浏览器回将新样式赋予给元素这个过程叫做重绘
添加或者删除节点
页面首页渲染
浏览器的窗口发生变化
内容变换
回流的性能消耗比重绘大,回流一定会触发重绘,重绘不一定会回流;回流会导致渲染树需要重新计算,开销比重绘大,所以我们要尽量避免回流的产生.
18. vue 中的 destroyed
组件销毁时候需要做的事情,比如当页面卸载的时候需要将页面中定时器清除,销毁绑定的监听事件
19. vue3 中的异步组件
异步组件与下面的组件懒加载原理是类似,都是需要使用了再去加载
<template>
<logo-img />
<hello-world msg="Welcome to Your Vue.js App" />
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
import LogoImg from './components/LogoImg.vue'
// 简单用法
const HelloWorld = defineAsyncComponent(() =>
import('./components/HelloWorld.vue'),
)
</script>
复制代码
20. 组件懒加载
<template>
<div id="content">
<div>
<component v-bind:is='page'></component>
</div>
</div>
</template>
<script>
// ---* 1 使用标签属性is和import *---
const FirstComFirst = ()=>import("./FirstComFirst")
const FirstComSecond = ()=>import("./FirstComSecond")
const FirstComThird = ()=>import("./FirstComThird")
export default {
name: 'home',
components: {
},
data: function(){
return{
page: FirstComFirst
}
}
}
</script>
复制代码
原理与路由懒加载一样的,只有需要的时候才会加载组件
21. 动态图片使用懒加载,静态图片使用精灵图
动态图片参考图片懒加载插件
静态图片,将多张图片放到一起,加载的时候节省时间
22. 第三方插件的按需引入
element-ui 采用 babel-plugin-component 插件来实现按需导入
//安装插件
npm install babel-plugin-component -D
// 修改babel文件
module.exports = {
presets: [['@babel/preset-env', { modules: false }], '@vue/cli-plugin-babel/preset'],
plugins: [
'@babel/plugin-proposal-optional-chaining',
'lodash',
[
'component',
{
libraryName: 'element-ui',
styleLibraryName: 'theme-chalk'
},
'element-ui'
],
[
'component',
{
libraryName: '@xxxx',
camel2Dash: false
},
]
]
};
复制代码
23. 第三方库 CDN 加速
//vue.config.js
let cdn = { css: [], js: [] };
//区分环境
const isDev = process.env.NODE_ENV === 'development';
let externals = {};
if (!isDev) {
externals = {
'vue': 'Vue',
'vue-router': 'VueRouter',
'ant-design-vue': 'antd',
}
cdn = {
css: [
'https://cdn.jsdelivr.net/npm/ant-design-vue@1.7.2/dist/antd.min.css', // 提前引入ant design vue样式
], // 放置css文件目录
js: [
'https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.min.js', // vuejs
'https://cdn.jsdelivr.net/npm/vue-router@3.2.0/dist/vue-router.min.js',
'https://cdn.jsdelivr.net/npm/ant-design-vue@1.7.2/dist/antd.min.js'
]
}
}
module.exports = {
configureWebpack: {
// 排除打包的某些选项
externals: externals
},
chainWebpack: config => {
// 注入cdn的变量到index.html中
config.plugin('html').tap((arg) => {
arg[0].cdn = cdn
return arg
})
},
}
//index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
<!-- 引入css-cdn的文件 -->
<% for(var css of htmlWebpackPlugin.options.cdn.css) { %>
<link rel="stylesheet" href="<%=css%>">
<% } %>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<!-- 放置js-cdn文件 -->
<% for(var js of htmlWebpackPlugin.options.cdn.js) { %>
<script src="<%=js%>" ></script>
<% } %>
<div id="app"></div>
</body>
</html>
复制代码
最后
以上的优化方案不紧在代码层面起到优化而且在性能上也起到了优化作用,文章内容主要是从 Vue 开发的角度和部分通过源码的角度去总结的,文章中如果存在错误的地方,或者你认为还有其他更好的方案,请大佬在评论区中指出,作者会及时更正,感谢!
评论