Vue项目能进行哪些性能优化?

ops/2025/2/12 7:26:45/

一、前言

在新公司开发项目的时候,我发现总结了一个问题就是,当我开发完某个需求。但是当测试完开发完产品验收的时候可能会从性能方面验收的时候会出现一些你意想不到的结果。为此我整理一下项目中优化可以从哪些方面来入手。首先可以从三个方面来进行,一是代码层面的优化二是项目打包的优化,三是项目部署的优化。如果有错误,有不足的,请指教,一起进步,祝你找到更好的工作。如果觉得的本文对您有帮助,点个赞支持一下。

二、代码层面的优化

1、利用v-if和v-show减少初始化渲染和切换渲染的性能开销

在页面加载时,利用v-if来控制组件仅在首次使用时渲染减少初始化渲染,随后用v-show来控制组件显示隐藏减少切换渲染的性能开销。

也许上面描述不够清楚,下面用一个实例来加深印象。

iview弹窗组件大家很经常用吧,弹窗组件是用v-show来控制其显示和隐藏,那么在页面加载时,弹窗组件(包括里面的内容)是会被初始化渲染的,如果一个页面中只有一个弹窗组件,不会对性能造成影响,但是假如一个页面中有几十个弹窗组件,会不会影响到性能,大家可以做一个右键菜单去试一下。

注意: ,element弹窗组件初次渲染时,弹窗body里面的内容不会被渲染的。

下面用代码来实现一下。

javascript"><template><div><Button type="primary" @click.native="add">添加</Button><add v-model="add.show" v-bind="add"></add></div></template> 
<script>export default{ data()
{return{ add:{ show:false, init:false } } },components:{ add:() =>import('./add.vue') },methods:{ add(){ this.add.show=true; this.add.init=true } } 
} 
</script>
javascript"><template><div v-if="init"> <Modal v-model="show" title="添加" @on-cancel="handleClose"></Modal></div> 
</template> 
<script> 
export default{ 
props:{ value:{ type:Boolean, default:false },init:{ type:Boolean, default:false } }, 
data(){ return{ show:false, } },watch:{ value(val){ if(val){ this.show = val; } } },methods:{ handleClose(val) 
{ this.$emit('input', val); }, }} 
</script>

原理:

v-if绑定值为false时,初始渲染时,不会渲染其条件块。

v-if绑定值,在true和false之间切换时,销毁和重新渲染其条件块。

v-show绑定值不管为true还是为false,初始渲染时,总是渲染其条件块。

v-show绑定值,在true和false之间切换时,不会销毁和重新渲染其条件块,只是用display:none样式来控制其显示隐藏。

2、computed、watch、methods区分使用场景

对于有些需求,computed、watch、methods都可以实现,但是还是要区分一下使用场景。用错场景虽然功能实现了但是影响了性能。

  • computed
    • 一个数据受多个数据影响的。
    • 该数据要经过性能开销比较大的计算,如它需要遍历一个巨大的数组并做大量的计算才能得到,这时就可以利用computed的缓存特性,只有它计算时依赖的数据发现变化时才会重新计算,否则直接返回缓存值。
  • watch
    • 一个数据影响多个数据的。
    • 当数据变化时,需要执行异步或开销较大的操作时。如果数据变化时请求一个接口。
  • methods
    • 希望数据是实时更新,不需要缓存。
3、提前处理好数据解决v-if和v-for必须同级的问题

因为当Vue处理指令时,v-forv-if具有更高的优先级,意味着v-if 将分别重复运行于每个v-for循环中。

可以在computed中提前把要v-for的数据中v-if的数据项给过滤处理了。

javascript"><template> 
<div> 
<div v-for="item in userList" :key="item.id" v-if="item.age > 18">{{ item.name }}</div> 
</div> 
</template>

​​​​​​​

javascript"><template> 
<div> 
<div v-for="item in userComputedList" :key="item.id">{{ item.name }}</div> 
</div>
</template>export default { 
computed:{ userComputedList:function(){return this.userList.filter(function (item) { return item.age > 18 }) }} }

也许会问为什么v-forv-if具有更高的优先级?这个问题已经涉及到原理层次。下篇在讲

上面说到 “v-if将分别重复运行于每个v-for循环中”,这个过程只有在渲染页面时才有,而Vue最终是通过render函数来渲染页面的,先把组件编译生成的render打印出来。

javascript">console.log(userList.render) </script>var render = function() {var _vm = this var _h = _vm.$createElement var _c = _vm._self._c || _h return _c( "div", _vm._l(_vm.userList, function(item) { return item.age > 18 ? _c("div", { key: item.id }, [_vm._v(_vm._s(item.name))]) : _vm._e() }), 0 ) } 
var staticRenderFns = []render._withStripped = true export { render, staticRenderFns }

其中_l方法是v-for指令通过genFor函数生成的renderList方法,item.age > 18? v-if指令通过genIf函数生成的三元运算符的代码,_v方法是createTextVNode方法用来创建文本节点,_e方式是createEmptyVNode方法用来创建空节点。到这里是不是已经很清楚,v-if运行在每个v-for中。

归根到底还是在生成render函数中,导致v-forv-if具有更高的优先级

4、给v-for循环项加上key提高diff计算速度
  • 为什么加key会提高diff计算速度。

    经过旧头新头、旧尾新尾、旧头新尾、旧尾新头四次交叉比对后,都没有匹配到值得比对的节点,这时如果新节点有key的话。可以通过map直接获得值得对比的旧节点的下标,如果没有key的话,就要通过循环旧节点数组用sameVnode方法判断新节点和该旧节点是否值得比较,值得就返回该旧节点的下标。显然通过map比通过循环数组的计算速度来的快。

  • 什么是diff计算。

    对于渲染watcher触发时会执行vm._update(vm._render(), hydrating),在vm._undata方法中会调用vm.__patch__,而vm.__patch__指向patch方法,diff计算是指在调用patch方法开始,用sameVnode方法判断节点是否值得比较,若不值得直接新节点替换旧节点后结束。值得对比进入patchVnode方法,分别处理一下几种情况,若新旧节点都有文本节点,新节点下的文本节点直接替换旧节点下的文本节点,如果新节点有子节点,旧节点没有子节点,那么直接把新节点查到旧节点的父级中,如果新节点没有子节点,旧节点有子节点,那么旧节点的父级下的子节点都删了。如果新旧节点都有子节点,进入updateChildren方法,通过旧头新头、旧尾新尾、旧头新尾、旧尾新头四次交叉比对,如果值得对比再进入patchVnode方法,如果都不值得对比,有key用map获得值得对比的旧节点,没有key通过循环旧节点获得值得对比的旧节点。当新节点都对比完,旧节点还没对比完,将还没对比完的旧节点删掉。当旧节点都对比完,新节点还没对比完,将新节点添加到最后一个对比过的新节点后面,完成diff计算。

首先介绍一下什么diff计算,diff计算就是对比新旧虚拟DOM(virtual DOM),virtual DOM是将真实的DOM的数据抽取出来,以对象的形式模拟树形结构,再说简白一点,diff计算就是对两个对象进行对比。

在采取diff算法比较新旧节点的时候,比较只会在同层级进行, 不会跨层级比较。 

 先上步骤图,可以先看图,再看文字介绍 


​​​​​​​

 每次对比的逻辑大概如下所示

  • 1、在patch方法内,用sameVnode判断新旧节点是否值得比较。
  • 2、如果不值得比较,直接在旧节点的父级中添加新节点,然后删除旧节点,退出对比。
  • 3、如果值得比较,调用patchVnode方法。
  • 4、如果新旧节点是否完全相等,如果是,退出对比。
  • 5、如果不是,找到对应的真实DOM,记为el。
  • 6、如果新旧节点都有文本节点并且不相等,那么将el的文本节点设置为新节点的文本节点,退出对比。
  • 7、如果新节点有子节点,旧节点没有子节点,则将新节点的子节点生成真实DOM后添加到el中,退出对比。
  • 8、如果新节点没有子节点,旧节点有子节点,则删除el的子节点,退出对比。
  • 9、如果新节点和旧节点都有子节点,则开始对比它们的子节点,用的是updateChildren方法。
  • 10、将旧节点的子节点记为oldCh是个数组,其头部用oldCh[oldStartIdx]获取记为oldStartVnodeoldStartIdx初始为0。其尾部用oldCh[oldEndIdx]获取记为oldEndVnodeoldEndIdx初始为oldCh.length - 1
  • 11、将旧节点的子节点记为newCh是个数组,其头部用newCh[newStartIdx]获取记为newStartVnodenewStartIdx初始为0。其尾部用newCh[newEndIdx]获取记为newEndVnodenewEndIdx初始为newCh.length - 1
  • 12、将旧子节点的头部和新子节点的头部,简称旧头和新头用sameVnode判断是否值得比较。
  • 13、如果值得比较,调用patchVnode方法,重新执行第3步。同时用oldCh[++oldStartIdx]重新获取旧子节点头部,用newCh[++newStartIdx]重新获取新子节点头部。
  • 14、如果不值得比较,将旧子节点的尾部和新子节点的尾部,简称旧尾和新尾用sameVnode判断是否值得比较。
  • 15、如果值得比较,调用patchVnode方法,重新执行第3步。同时用oldCh[--oldEndIdx]重新获取旧子节点尾部,重新用newCh[--newEndIdx]获取新子节点尾部。
  • 16、如果不值得比较,将旧子节点的头部和新子节点的尾部,简称旧头和新尾用sameVnode判断是否值得比较。
  • 17、如果值得比较,调用patchVnode方法,重新执行第3步。同时将旧子节点的头部oldStartVnode对应的真实DOM移动到旧子节点的尾部oldEndVnode对应的真实DOM后面。同时用oldCh[++oldStartIdx]重新获取旧子节点头部,用newCh[--newEndIdx]重新获取新子节点尾部。
  • 18、如果不值得比较,将旧子节点的尾部和新子节点的头部,简称旧尾和新头用sameVnode判断是否值得比较。
  • 19、如果值得比较,调用patchVnode方法,重新执行第3步。同时将旧子节点的尾部oldEndVnode对应的真实DOM移动到旧子节点的头部oldStartVnode对应的真实DOM后面。同时用oldCh[--oldEndIdx]重新获取旧子节点尾部,用newCh[++newStartIdx]重新获取新子节点头部。
  • 20、如果不值得比较,如果旧子节点有key,可以用createKeyToOldIdx方法获得以旧子节点的key为健,其下标为值的map结构,记为oldKeyToIdx
  • 21、如果新子节点的头部newStartVnode有key属性,直接通过oldKeyToIdx[newStartVnode.key]获取对应的下标idxInOld
  • 22、如果新子节点的头部newStartVnode没有key属性,要用过findIdxInOld方法,找到值得对比的旧子节点对应的下标idxInOld
  • 23、经过查找。如果idxInOld不存在。则调用createElm方法直接生成newStartVnode对应的真实DOM插入oldStartVnode对应真实DOM前面。
  • 24、如果idxInOld存在,则把用通过oldCh[idxInOld]获取到Vnode记为vnodeToMovenewStartVnode用sameVnode判断是否值得比较。
  • 25、如果值得比较,调用patchVnode方法,重新执行第3步。同时执行oldCh[idxInOld] = undefined,免得被重复比较。同时将vnodeToMove对应的真实DOM移动到旧子节点的头部oldStartVnode对应的真实DOM前面。
  • 26、如果不值得比较,则调用createElm方法直接生成newStartVnode对应的真实DOM插入oldStartVnode对应真实DOM前面。
  • 27、用newCh[++newStartIdx]重新获取新子节点头部
  • 28、如果满足oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx继续执行步骤9。
  • 29、如果oldStartIdx > oldEndIdx,说明所有旧子节点已经都比对完了,还剩下未比对的新子节点都调用createElm方法生成对应的真实DOM,插到newCh[newEndIdx + 1]对应的真实DOM后面。
  • 30、如果newStartIdx > newEndIdx,说明所有新子节点都比较完,那么还剩下的旧子节点都删除掉。
5、利用v-once处理只会渲染一次的元素或组件

只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。

例如某个页面是合同范文,里面大部分内容从服务端获取且是固定不变,只有姓名、产品、金额等内容会变动。这时就可以把v-once添加到那些包裹固定内容的元素上,当生成新的合同可以跳过那些固定内容,只重新渲染姓名、产品、金额等内容即可。

v-if一起使用时,v-once不生效。在v-for循环内的元素或组件上使用,必须加上key。

6、利用Object.freeze()冻结不需要响应式变化的数据

Vue初始化过程中,会把data传入observe函数中进行数据劫持,把data中的数据都转换成响应式的。

在observe函数内部调用defineReactive函数处理数据,配置getter/setter属性,转成响应式,如果使用Object.freeze()将data中某些数据冻结了,也就是将其configurable属性(可配置)设置为false。

defineReactive函数中有段代码,检测数据上某个key对应的值的configurable属性是否为false,若是就直接返回,若不是继续配置getter/setter属性。

javascript">export function defineReactive(obj,key,val,customSetter,shallow){ //... const property = Object.getOwnPropertyDescriptor(obj, key)//获取obj[key]的属性 if (property && property.configurable === false) { return } //... }

在项目中如果遇到不需要响应式变化的数据,可以用Object.freeze()把该数据冻结了,可以跳过初始化时数据劫持的步骤,大大提高初次渲染速度。

7、提前过滤掉非必须数据,优化data选项中的数据结构

Vue初始化时,会将选项data传入observe函数中进行数据劫持,

javascript">initData(vm){ let data = vm.$options.data //... observe(data, true) }

在observe函数会调用

javascript">observe(value,asRootData){ //... ob = new Observer(value); }

在Observer原型中defineReactive函数处理数据,配置getter/setter属性,转成响应式

javascript">walk (obj) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } }

defineReactive函数中,会将数据的值再次传入observe函数中

javascript">export function defineReactive(obj,key,val,customSetter,shallow){ //... if (arguments.length === 2) { val = obj[key] } let childOb = observe(val); //... }

observe函数中有段代码,将数据传入,Observer类中。

javascript">export function observe(value,asRootData){ //... ob = new Observer(value) //... return ob }

以上构成了一个递归调用。

接收服务端传来的数据,都会有一些渲染页面时用不到的数据。服务端的惯例,宁可多传也不会少传。

所以要先把服务端传来的数据中那些渲染页面用不到的数据先过滤掉。然后再赋值到data选项中。可以避免去劫持那些非渲染页面需要的数据,减少循环和递归调用,从而提高渲染速度。

8、避免在v-for循环中读取data中数组类型的数据
javascript">export function defineReactive(obj,key,val,customSetter,shallow){ const dep = new Dep() const property = Object.getOwnPropertyDescriptor(obj, key) const getter = property && property.get; const setter = property && property.set; if ((!getter || setter) && arguments.length === 2) { val = obj[key] } let childOb = !shallow && observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value } }) } function dependArray (value: Array<any>) { for (let e, i = 0, l = value.length; i < l; i++) { e = value[i] e && e.__ob__ && e.__ob__.dep.depend() if (Array.isArray(e)) { dependArray(e) } } } export function observe (value, asRootData){ if (!isObject(value) || value instanceof VNode) { return } //... }

为什么要避免在v-for循环中读取data中数组类型的数据,因为在数据劫持中会调用defineReactive函数中。由于 getter是函数,并且引用了 depchildOb,形成了闭包,所以 depchildOb 一直存在于内存(每个数据的getter函数)中,dep是每个数据的依赖收集容器,childOb是经过响应式处理后的数据。

在渲染视图、使用watch监听、使用计算属性过程中,读取数据,都会对Dep.target进行赋值,其值为Watcher(依赖),例如在渲染视图过程中读取数据时,Dep.target为renderWatcher。

接着先调用dep.depend()给自身收集依赖,如果val(自身的值)不是对象,则childOb为false。如果val(自身的值)是对象,用childOb.dep.depend()收集依赖,若value(自身的值)是数组用dependArray(value)递归每一项来收集依赖。

为什么要避免在v-for循环中读取data中数组类型的数据,其原因就是若value(自身的值)是数组用dependArray(value)递归每一项来收集依赖

举个简单的栗子,表格中每行有两个输入框,分别可以输入驾驶员和电话,代码这么实现。

javascript"><template><div class="g-table-content"> <el-table :data="tableData"> <el-table-column prop="carno" label="车牌号"></el-table-column> 
<el-table-column prop="cartype" label="车型"></el-table-column> 
<el-table-column label="驾驶员"> <template slot-scope="{row,column,$index}"> <el-input v-model="driverList[$index].name"></el-input> </template></el-table-column><el-table-column label="电话"> <template slot-scope="{row,column,$index}"> <el-input v-model="driverList[$index].phone"></el-input> </template> 
</el-table-column> </el-table> </div> 
</template>

假设表格有500条数据,那么读取driverList共500次,每次都读取driverList都会进入dependArray(value)中,总共要循环500*500=25万次,若有分页,每次切换页码,都会至少循环25万次。

如果我们在从服务获取到数据后,做了如下预处理,在赋值给this.tableData,会是怎么样?

res.data.forEach(item =>{ item.name=''; item.phone=''; })

模板这样实现

javascript"><template> 
<div class="g-table-content"> <el-table :data="tableData"> 
<el-table-column prop="carno" label="车牌号"></el-table-column> <el-table-column prop="cartype" label="车型">
</el-table-column><el-table-column label="驾驶员"><template slot-scope="{row}"> <el-input v-model="row.name">
</el-input> </template></el-table-column><el-table-column label="电话"> <template slot-scope="{row,column,$index}"> <el-input v-model="row.phone"></el-input> </template> </el-table-column> </el-table> </div> 
</template>

也可以实现需求,渲染过程中求值时也不会进入dependArray(value)中,也不会造成25万次的不必要的循环。大大提高了性能。

9、防抖和节流

防抖和节流是针对用户操作的优化。首先来了解一下防抖和节流的概念。

  • 防抖:触发事件后规定时间内事件只会执行一次。简单来说就是防止手抖,短时间操作了好多次。
  • 节流:事件在规定时间内只执行一次。
  • 应用场景: 节流不管事件有没有触发还是频繁触发,在规定时间内一定会只执行一次事件,而防抖是在规定时间内事件被触发,且是最后一次被触发才执行一次事件。假如事件需要定时执行,但是其他操作也会让事件执行,这种场景可以用节流。假如事件不需要定时执行,需被触发才执行,且短时间内不能执行多次,这种场景可以用防抖。

在用Vue Cli脚手架搭建的Vue项目中,可以通过引用Lodash工具库里面的debounce防抖函数和throttle节流函数。

javascript">import debounce from 'lodash/debounce'; import throttle from 'lodash/throttle'; export default{ methods:{ a: debounce(function (){ //... },200,{ 'leading': false, 'trailing': true }), b: throttle(function (){ //... },200,{ 'leading': false, 'trailing': true }) } }
  • debounce(func, [wait=0], [options={}]) 创建一个防抖函数,该函数会从上一次被调用后,延迟 wait 毫秒后调用 func 方法。返回一个防抖函数debounceFn,debounce.cancel取消防抖,debounce.flush 立即调用该func。

    • options.leading为true时,func在延迟开始前调用。
    • options.trailing为true时,func在延迟开始结束后调用。
    • options.maxWait设置func 允许被延迟的最大值。
  • throttle(func, [wait=0], [options={}]) 创建一个节流函数,在 wait 秒内最多执行 func 一次的函数。 返回一个节流函数throttleFn,throttleFn.cancel取消节流,throttleFn.flush 立即调用该func。

    • options.leading为true时,func在节流开始前调用。
    • options.trailing为true时,func在节流结束后调用。
    • leadingtrailing都为true,func在wait期间多次调用。
10、图片大小优化和懒加载

关于图片大小的优化,可以用image-webpack-loader进行压缩图片,在webpack插件中配置,具体可以看本文中这点。

关于图片懒加载,可以用vue-lazyload插件实现。

执行命令npm install vue-lazyload --save安装vue-lazyload插件。在main.js中引入配置

javascript">import VueLazyload from 'vue-lazyload'; Vue.use(VueLazyload, { preLoad: 1.3,//预载高度比例 error: 'dist/error.png',//加载失败显示图片 loading: 'dist/loading.gif',//加载过程中显示图片 attempt: 1,//尝试次数 })

在项目中使用

javascript"><img v-lazy="/static/img/1.png">
11、利用挂载节点会被替换的特性优化白屏问题
javascript">import Vue from 'vue' import App from './App.vue' new Vue({ render: h => h(App) }).$mount('#app')

Vue 选项中的 render 函数若存在,则 Vue 构造函数不会从 template 选项或通过 el 选项指定的挂载元素中提取出的 HTML 模板编译渲染函数。

也就是说渲染时,会直接用render渲染出来的内容替换<div id="app"></div>

Vue项目有个缺点,首次渲染会有一段时间的白屏原因是首次渲染时需要加载一堆资源,如js、css、图片。很多优化策略,最终目的是提高这些资源的加载速度。但是如果遇上网络慢的情况,无论优化到极致还是需要一定加载时间,这时就会出现白屏现象。

首先加载是index.html页面,其是没有内容,就会出现白屏。如果<div id="app"></div>里面有内容,就不会出现白屏。所以我们可以在<div id="app"></div>里添加首屏的静态页面。等真正的首屏加载出来后就会把<div id="app"></div>这块结构都替换掉,给人一种视觉上的误差,就不会产生白屏。

11、组件库的按需引入

组件库按需引入的方法,一般文档都会介绍。

如element UI库,用babel-plugin-component插件实现按需引入。

执行命令npm install babel-plugin-component --save-dev,安装插件。

在根目录下.babelrc.js文件中按如下配置

javascript">{ "presets": [["es2015", { "modules": false }]], "plugins": [ [ "component", { "libraryName": "element-ui", "styleLibraryName": "theme-chalk" } ] ] }

其中libraryName为组件库的名称,styleLibraryName为组件库打包后样式存放的文件夹名称。 在main.js中就可以按需引入。

javascript">import Vue from 'vue'; import { Button, Select } from 'element-ui'; Vue.use(Button) Vue.use(Select)

其实babel-plugin-component插件是element用babel-plugin-import插件改造后特定给element UI使用。一般的组件库还是babel-plugin-import插件实现按需引入。

执行命令npm install babel-plugin-import --save-dev,安装插件。

在根目录下.babelrc.js文件中按如下配置

javascript">{ "plugins": [ ["import", { "libraryName": "vant", "libraryDirectory": "es", "style": true }] ] }

其中libraryName为组件库的名称,libraryDirectory表示从库的package.json的main入口文件或者module入口文件所在文件夹名称,否则默认为lib。

三、项目打包的优化

在说这个之前,先要明确什么是打包。通俗来说,就是把一个项目打包成一个个js文件、css文件等资源,最后在index.html文件中引入,大家可以看一下项目中dist文件夹中的index.html。

如下图,红框中就是一个项目通过打包出来的资源。其实说优化,就是优化这些资源。那么要怎么优化这些资源呢? 

 在早期没有Webpack时,这些资源都是开发者按照团队规范来处理和引入。并通过优化来实现最快的、最合理从服务器下载这些资源。这时期的优化主要体现在:

  • js、css代码按需引入。
  • js、css代码公用代码提取。
  • js、css代码的最小化压缩。
  • 图片资源的压缩。

现在项目是用Webpack打包的,可以通过配置Webpack来优化。

如果你的Vue项目是用Vue Cli3搭建起来,可以在根目录新建一个vue.config.js文件,在这个文件中配置Webpack来优化这些资源。

优化还是提现在上面四点。下面总结了5个优化手段,其中两个手段虽然在生产环境已经是默认优化的,但是还是要了解一下。

优化自然要前后对比,先安装插件webpack-bundle-analyzer,可以帮助你可视化的分析打包后的各个资源的大小。

javascript">npm install webpack-bundle-analyzer --save-dev

vue.config.js中引入这插件

javascript">const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; module.exports={ configureWebpack:config =>{ return { plugins:[ new BundleAnalyzerPlugin() ] } } }

执行命令npm run build,会在浏览器打开一份打包分析图 

1、利用import()异步引入组件实现按需引入

说起import(),我们自然会想到路由懒加载,所谓的懒加载就是用import()异步引入组件。

在网上随便一搜,懒加载还可以通过resolve =>require(['需要加载组件的地址'],resolve)来实现。

javascript">component: () =>import('views/home.vue'), component: resolve =>require(['views/home.vue'],resolve)

但是用resolve =>require(['需要加载组件的地址'],resolve)来异步引入组件,通过Webpack4打包后,发现所有组件的代码被打包成一个js文件这和预期的不符,预期应该是每个组件的代码都被打包成对应的js文件,加载组件时会对应加载js文件,这才是懒加载。

import()来异步引入组件后,执行命令npm run build后,看一下打包分析图。

对比后发现,原来一个1.42MB的js文件不见了,被拆分成许多如32.55KB、31.69KB的js小文件。这些js小文件只有在对应的组件加载时才会加载,这才是懒加载。

javascript">function load(component) { return () => import(/* webpackChunkName: "[request]" */ `views/${component}`) }

执行命令npm run build,看一下打包分析图。

 如图中红框的js文件是views/flow_card_manage/flow_card_list/index.vue这个组件打包出来的。

在浏览器上打开项目,用F12抓包看一下,搜一下flow_card_manage-flow_card_list.67de1ef8.js这个文件。在首页时,还没加载到这个路由组件时。这个js文件是有被加载,只是预取(prefetch)一下,没有返回内容的。目的是告诉浏览器,空闲的时候给我加载这个js文件。 

 直到真正加载这个路由组件时,这个js文件再次被加载,如果浏览器已经加载好了直接返回内容,如果浏览器还没加载好,就去服务器请求这个js文件,再返回内容。这样就是懒加载,按需加载。

2、利用externals提取第三方依赖并用CDN引入

从打包分析图中可以发现chunk-vendors.js和chunk-80f6b79a.js这两个文件还是很大。这两个文件内有element-ui、jquery、xlsx等第三方依赖的资源。 

在Webpack中的externals配置选项,可避免将第三方依赖打包,而是在项目运行时从外部获取第三方依赖。

执行命令npm run build,看一下打包分析图。 

 chunk-vendors.js和chunk-80f6b79a.js的文件大小和之前相比,有大幅度的减小。

用externals提取第三方依赖时,需切记中庸之道。虽然我们的最终目的是减少http请求资源大小,但是过犹不及,提取的过细将会增加http请求数量。

3、利用SplitChunks插件提取公共js代码和分割js代码

用Webpack打包后,还是有很多资源被重复打包到各个js文件,可以用SplitChunks插件进一步优化,减少打包生成文件的总体大小。

另外CDN是第三方,具有不稳定性,万一CDN突然挂了,系统也就崩了,有一定的风险。也可以用SplitChunks插件实现externals配置的效果,第三方依赖还是在自己服务器上,减少风险。

4、利用MiniCssExtractPlugin插件提取css样式

在用Vue Cli3搭建的Vue项目中是用css.extract来控制MiniCssExtractPlugin插件是否启用,虽然在生产环境中,css.extract默认是为true,也就是说MiniCssExtractPlugin插件是启用的。但是还是要熟悉一下MiniCssExtractPlugin插件的用法,以防面试官细问。

5、利用OptimizeCssnanoPlugin插件压缩和去重css样式文件

在用Vue Cli3搭建的Vue项目中默认是使用OptimizeCssnanoPlugin插件来压缩和去重css样式文件,

这里来讲一下怎么使用这款插件。 先安装OptimizeCssnanoPlugin插件

javascript">cnpm install --save-dev @intervolga/optimize-cssnano-plugin

vue.config.js中这么配置

javascript">const OptimizeCssnanoPlugin = require('@intervolga/optimize-cssnano-plugin'); module.exports={ configureWebpack:config =>{ return { plugins:[ new OptimizeCssnanoPlugin({ sourceMap: false, cssnanoOptions: { preset: [ 'default', { mergeLonghand: false, cssDeclarationSorter: false } ] }, }), ] } } }

其中cssnanoOptions的配置可以看​​​​​​​这里

mergeLonghand:false,表示关闭如margin,padding和border类似css样式属性合并。

javascript">.box { margin-top: 10px; margin-right: 20px; margin-bottom: 10px; margin-left: 20px; } //压缩后 .box { margin: 10px 20px; }

cssDeclarationSorter:false,表示关闭根据CSS的属性名称对CSS进行排序。

javascript">body { animation: none; color: #C55; border: 0; } //压缩后 body { animation: none; border: 0; color: #C55; }
6、开启optimization.minimize来压缩js代码

optimization.minimize选项有两个值truefalse,为true开启压缩js代码,为false关闭压缩js代码。

在生产环境中默认为true,在开发环境中默认为false

如果你在开发环境不需要用debug调试代码,可以也设置为true来压缩js代码,提高页面加载速度。

vue.config.js中这么配置

javascript">module.exports={ configureWebpack:config =>{ return { optimization:{ minimize: true } } } }

在Vue Cli3中默认用TerserPlugin插件来压缩js代码,其中配置已经是最优了。

如果想用其它插件来压缩js代码,可以在optimization.minimizer选项中添加,其值为数组。

用chainWebpack来添加,其中WebpackPlugin为插件名称,args为插件参数。

javascript">const WebpackPlugin = require(插件名称) module.exports = { chainWebpack: config =>{ config.optimization .minimizer(name) .use(WebpackPlugin, args) }, }
四、项目部署的优化

这里只讲一个比较常见和简单的优化手段,gzip压缩。其实还有其他优化手段,涉及到服务端,如果面试官深究会其反作用。

1、识别gzip压缩是否开启

这个很简单,只要看响应头部(Response headers)中 有没有Content-Encoding: gzip这个属性即可,有代表有开启gzip压缩。

2、在Nginx上开启gzip压缩

在nginx/conf/nginx.conf中配置

javascript">http { gzip on; gzip_min_length 1k; gzip_comp_level 5; gzip_types application/javascript image/png image/gif image/jpeg text/css text/plain; gzip_buffers 4 4k; gzip_http_version 1.1; gzip_vary on; }
  • gzip:on | off ,默认为off,on为开启gzip,off为关闭gzip。
  • gzip_min_length:number,压缩起点,文件大于多少才进行压缩,单位默认为字节,也可用k表示千字节。
  • gzip_comp_level:压缩级别,1-9,数字越大,压缩后的大小越小,也越占用CPU,花费时间越长。
  • gzip_types:需要进行压缩的文件类型。类型去Response headers中看Content-Type属性。
  • gzip_buffers:number size,设置系统获取几个单位的缓存用于存储gzip的压缩结果数据流。 例如 4 4k代表以4k为单位,按照原始数据大小以4k为单位的4倍申请内存。如原始数据大小为17K,则申请 (17/4)*4 = 17k内存。
  • gzip_http_version:设置gzip压缩针对的HTTP协议版本以上。
  • gzip_vary:on | off,是否在http header中添加Vary:Accept-Encoding,on表示添加。Vary:Accept-Encoding告诉代理服务器缓存两种版本的资源:压缩和非压缩,避免一个浏览器不支持压缩资源,而先请求了服务器,服务器缓存了非压缩的资源,然后一个浏览器支持压缩资源,再去请求了服务器,结果得到非压缩资源,但是又去解压它,结果会出错。所以建议设置为on。​​​​​​​。
3、在Webpack上开启gzip压缩

利用CompressionWebpack插件来实现gzip压缩。

首先安装CompressionWebpack插件

javascript">npm install compression-webpack-plugin --save-dev

然后在vue.config.js中这么配置

javascript">const CompressionPlugin = require('compression-webpack-plugin'); module.exports = { configureWebpack: config =>{ return { plugins: [ new CompressionPlugin() ], } } }

执行npm run build命令后,打开dist文件,会发现多出很多名字相同的文件,只是其中一个文件后缀为.gz,这就是用gzip压缩后的文件。 

4、Nginx和Webpack压缩的区别
  • 不管Nginx还是Webpack压缩,在Nginx中都要开启gzip压缩,不然浏览器加载还是未压缩的资源。

    还可以在Nginx加上gzip_static on;的配置。gzip_static启用后, 浏览器请求资源时,Nginx会先检查是否存该资源名称且后缀为.gz的文件,如果有则直接返回该gz文件内容,可以避免Nginx对该资源再进行gzip压缩,浪费服务器的CPU。

  • 用Nginx压缩会占用服务器的CPU,浏览器每次请求资源,Nginx会对该资源实时压缩,压缩完毕后才会返回该资源,如果资源很大的话,还是压缩级别设置很高,都会导致返回资源的时间过长,造成不好的用户体验。

  • 用Webpack会使打包时间变长。但是用CompressionPlugin插件压缩,会有缓存,可以相对减少打包时间。

  • 建议Nginx和Webpack压缩都开启压缩,且在Nginx加上gzip_static on;的配置,减少服务器的CPU的使用,当然还是要根据项目的情况实际选择。


http://www.ppmy.cn/ops/157719.html

相关文章

C++ 顺序表

顺序表的操作有以下&#xff1a; 1 顺序表的元素插入 给定一个索引和元素&#xff0c;这个位置往后的元素位置都要往后移动一次&#xff0c;元素插入的步骤有以下几步 &#xff08;1&#xff09;判断插入的位置是否合法&#xff0c;如果不合法则抛出异常 &#xff08;2&…

SVN 版本回退

SVN 版本回退 引言 Subversion&#xff08;简称SVN&#xff09;是一款广泛使用的版本控制系统&#xff0c;它允许开发团队在项目开发过程中跟踪源代码的修改历史。在软件开发过程中&#xff0c;版本回退是一项重要的操作&#xff0c;它可以帮助开发人员恢复到某个特定的代码版…

如何查看Ubuntu24.04系统,显卡是什么型号,适配的驱动是什么?

在Ubuntu 24.04系统中&#xff0c;查看显卡型号和适配的驱动程序可以通过以下步骤完成&#xff1a; 查看显卡型号 要确定您的计算机上安装了什么显卡&#xff0c;您可以使用几种不同的命令。最常用的命令之一是lspci&#xff0c;它能够列出所有PCI设备的信息。为了专门找到NV…

环信EaseCallKit信令详解

EaseCallKit 是一套基于环信 IM 和声网音视频结合开发的音视频 UI 库&#xff0c;实现了一对一语音和视频通话以及多人音视频通话的功能。基于 EaseCallKit 可以快速实现通用音视频功能。 下面讲解用IM做信令的过程和效果示意 &#xff08;一&#xff09;演示样例&#xff08…

大语言模型多代理协作(MACNET)

大语言模型多代理协作(MACNET) Scaling Large-Language-Model-based Multi-Agent Collaboration 提出多智能体协作网络(MACNET),以探究多智能体协作中增加智能体数量是否存在类似神经缩放定律的规律。研究发现了小世界协作现象和协作缩放定律,为LLM系统资源预测和优化…

9.混合推荐系统的高级应用

接下来我们将学习混合推荐系统的高级应用。混合推荐系统&#xff08;Hybrid Recommender System&#xff09;通过结合多种推荐算法&#xff0c;可以利用不同算法的优势&#xff0c;提升推荐效果和用户满意度。在这一课中&#xff0c;我们将介绍以下内容&#xff1a; 混合推荐系…

微服务中如何使用openfeign上传文件

继上一篇文章讲了springboot上传文件的各种基本操作&#xff0c;但我们在实际的开发中可能会单独的开发一个文件服务来管理我们的真实的文件&#xff0c;然后将我们的文件信息和业务的数据存放在一起。那么在微服务中如何使用openfeign上传文件呢&#xff1f; 首先需要搭建一套…

图像锐化(QT)

如果不使用OpenCV&#xff0c;我们可以直接使用Qt的QImage类对图像进行像素级操作来实现锐化。锐化算法的核心是通过卷积核&#xff08;如拉普拉斯核&#xff09;对图像进行处理&#xff0c;增强图像的边缘和细节。 以下是一个完整的Qt应用程序示例&#xff0c;展示如何使用Qt…