一点点Vue性能优化方案分享

4,571 阅读9分钟

我们在开发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> 
  • 默认是都没有选中,当我们在页面中选中第一个

image.png

  • 点击下面的移除按钮,再看效果

image.png

  • 调整一下代码
<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

  1. 可能会导致xss攻击
  2. 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了,作为用户的标识也作为登录的状态,我们需要将其储存到localStoragesessionStorage起来每次刷新页面Vuex从localStoragesessionStorage获取状态,不然每次刷新页面用户都需要重新登录,重新获取数据

  • 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. 动态图片使用懒加载,静态图片使用精灵图

  • 动态图片参考图片懒加载插件 (github.com/hilongjw/vu…)
  • 静态图片,将多张图片放到一起,加载的时候节省时间

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开发的角度和部分通过源码的角度去总结的,文章中如果存在错误的地方,或者你认为还有其他更好的方案,请大佬在评论区中指出,作者会及时更正,感谢!