乐趣区

关于前端:12-个-Vue-开发中的性能优化小技巧看你用过几个

性能优化,是每一个开发者都会遇到的问题,特地是当初越来越器重体验,以及竞争越来越强烈的环境下,对于咱们开发者来说,只实现迭代,把性能做好是远远不够的,最重要的是把产品做好,让更多人违心应用,让用户用得更爽,这不也是咱们开发者价值与能力的体现吗。器重性能问题,优化产品的体验,比起改几个无关痛痒的 bug 要有价值得多

本文记录了我在 Vue 我的项目日常开发中的一些小技巧,废话不多说,咱们开始吧

1. 长列表性能优化

1. 不做响应式

比方会员列表、商品列表之类的,只是纯正的数据展现,不会有任何动静扭转的场景下,就不须要对数据做响应化解决,能够大大晋升渲染速度

比方应用 Object.freeze() 解冻一个对象,MDN 的形容是 该办法解冻的对象不能被批改;即不能向这个对象增加新属性,不能删除已有属性,不能批改该对象已有属性的可枚举性、可配置性、可写性,以及不能批改已有属性的值,以及该对象的原型也不能被批改

export default {data: () => ({userList: []
  }),
  async created() {const users = await axios.get("/api/users");
    this.userList = Object.freeze(users);
  }
};

Vue2 的响应式源码地址:src/core/observer/index.js - 144 行 是这样的

export function defineReactive (...){const property = Object.getOwnPropertyDescriptor(obj, key)
    if (property && property.configurable === false) {return}
    ...
}

能够看到一开始就判断 configurablefalse 的间接返回不做响应式解决

configurablefalse 示意这个属性是不能被批改的,而解冻的对象的 configurable 就是为 false

Vue3 里则是增加了响应式flag,用于标记指标对象类型,Vue3 的响应式详情能够看我另一篇文章

2. 虚构滚动

如果是大数据很长的列表,全副渲染的话一次性创立太多 DOM 就会十分卡,这时就能够用虚构滚动,只渲染少部分 (含可视区域) 区域的内容,而后滚动的时候,一直替换可视区域的内容,模拟出滚动的成果

<recycle-scroller
  class="items"
  :items="items"
  :item-size="24"
>
  <template v-slot="{item}">
    <FetchItemView
      :item="item"
      @vote="voteItem(item)"
    />
  </template>
</recycle-scroller>

参考 vue-virtual-scroller、vue-virtual-scroll-list

原理是监听滚动事件,动静更新须要显示的 DOM,并计算出在视图中的位移,这也意味着在滚动过程须要实时计算,有肯定老本,所以如果数据量不是很大的状况下,用一般的滚动就行

2. v-for 遍历防止同时应用 v-if

为什么要防止同时应用 v-forv-if

在 Vue2 中 v-for 优先级更高,所以编译过程中会把列表元素全副遍历生成虚构 DOM,再来通过 v-if 判断符合条件的才渲染,就会造成性能的节约,因为咱们心愿的是不符合条件的虚构 DOM 都不要生成

在 Vue3 中 v-if 的优先级更高,就意味着当判断条件是 v-for 遍历的列表中的属性的话,v-if 是拿不到的

所以在一些须要同时用到的场景,就能够通过计算属性来过滤一下列表,如下

<template>
    <ul>
      <li v-for="item in activeList" :key="item.id">
        {{item.title}}
      </li>
    </ul>
</template>
<script>
// Vue2.x
export default {
    computed: {activeList() {
        return this.list.filter( item => {return item.isActive})
      }
    }
}

// Vue3
import {computed} from "vue";
const activeList = computed(() => {
  return list.filter( item => {return item.isActive})
})
</script>

3. 列表应用惟一 key

比方有一个列表,咱们须要在两头插入一个元素,在不应用 key 或者应用 index 作为 key 会产生什么变动呢?先看个图

如图的 li1li2 不会从新渲染,这个没有争议的。而 li3、li4、li5 都会从新渲染

因为在不应用 key 或者列表的 index 作为 key 的时候,每个元素对应的地位关系都是 index,上图中的后果间接导致咱们插入的元素到前面的全副元素,对应的地位关系都产生了变更,所以在 patch 过程中会将它们全都执行更新操作,再从新渲染。这可不是咱们想要的,咱们心愿的是渲染增加的那一个元素,其余四个元素不做任何变更,也就不要从新渲染

而在应用惟一 key 的状况下,每个元素对应的地位关系就是 key,来看一下应用惟一 key 值的状况下

这样如图中的 li3li4 就不会从新渲染,因为元素内容没产生扭转,对应的地位关系也没有产生扭转。

这也是为什么 v-for 必须要写 key,而且不倡议开发中应用数组的 index 作为 key 的起因

4. 应用 v-show 复用 DOM

v-show:是渲染组件,而后扭转组件的 display 为 block 或 none
v-if:是渲染或不渲染组件

所以对于能够频繁扭转条件的场景,就应用 v-show 节俭性能,特地是 DOM 构造越简单收益越大

不过它也有劣势,就是 v-show 在一开始的时候,所有分支外部的组件都会渲染,对应的生命周期钩子函数都会执行,而 v-if 只会加载判断条件命中的组件,所以须要依据不同场景应用适合的指令

比方上面的用 v-show 复用 DOM,比 v-if/v-else 成果好

<template>
  <div>
    <div v-show="status" class="on">
      <my-components />
    </div>
    <section v-show="!status" class="off">
      <my-components >
    </section>
  </div>
</template>

原理就是应用 v-if 当条件变动的时候,触发 diff 更新,发现新旧 vnode 不统一,就会移除整个旧的 vnode,再从新创立新的 vnode,而后创立新的 my-components 组件,又会经验组件本身初始化,renderpatch 等过程,而 v-show 在条件变动的时候,新旧 vnode 是统一的,就不会执行移除创立等一系列流程

5. 无状态的组件用函数式组件

对于一些纯展现,没有响应式数据,没有状态治理,也不必生命周期钩子函数的组件,咱们就能够设置成函数式组件,进步渲染性能,因为会把它当成一个函数来解决,所以开销很低

原理是在 patch 过程中对于函数式组件的 render 生成的虚构 DOM,不会有递归子组件初始化的过程,所以渲染开销会低很多

它能够承受 props,然而因为不会创立实例,所以外部不能应用 this.xx 获取组件属性,写法如下

<template functional>
  <div>
    <div class="content">{{value}}</div>
  </div>
</template>
<script>
export default {props: ['value']
}
</script>

// 或者
Vue.component('my-component', {
  functional: true, // 示意该组件为函数式组件
  props: {...}, // 可选
  // 第二个参数为上下文,没有 this
  render: function (createElement, context) {// ...}
})

6. 子组件宰割

先看个例子

<template>
  <div :style="{opacity: number / 100}">
    <div>{{someThing() }}</div>
  </div>
</template>
<script>
export default {props:['number'],
  methods: {someThing () {/* 耗时工作 */}
  }
}
</script>

下面这样的代码中,每次父组件传过来的 number 发生变化时,每次都会从新渲染,并且从新执行 someThing 这个耗时工作

所以优化的话一个是用计算属性,因为计算属性本身有缓存计算结果的个性

第二个是拆分成子组件,因为 Vue 的更新是组件粒度的,尽管第次数据变动都会导致父组件的从新渲染,然而子组件却不会从新渲染,因为它的外部没有任何变动,耗时工作天然也就不会从新执行,因而性能更好,优化代码如下

<template>
  <div>
    <my-child />
  </div>
</template>
<script>
export default {
  components: {
    MyChild: {
      methods: {someThing () {/* 耗时工作 */}
      },
      render (h) {return h('div', this.someThing())
      }
    }
  }
}
</script>

7. 变量本地化

简略说就是把会屡次援用的变量保存起来,因为每次拜访 this.xx 的时候,因为是响应式对象,所以每次都会触发 getter,而后执行依赖收集的相干代码,如果应用变量次数越多,性能天然就越差

从需要上说在一个函数里一个变量执行一次依赖收集就够了,可是很多人习惯性的在我的项目中大量写 this.xx,而疏忽了 this.xx 背地做的事,就会导致性能问题了

比方上面例子

<template>
  <div :style="{opacity: number / 100}"> {{result}}</div>
</template>
<script>
import {someThing} from '@/utils'
export default {props: ['number'],
  computed: {base () {return 100},
    result () {
      let base = this.base, number = this.number // 保存起来
      for (let i = 0; i < 1000; i++) {number += someThing(base) // 防止频繁援用 this.xx
      }
      return number
    }
  }
}
</script>

8. 第三方插件按需引入

比方 Element-UI 这样的第三方组件库能够按需引入防止体积太大,特地是我的项目不大的状况下,更没有必要残缺引入组件库

// main.js
import Element3 from "plugins/element3";
Vue.use(Element3)

// element3.js
// 残缺引入
import element3 from "element3";
import "element3/lib/theme-chalk/index.css";

// 按需引入
// import "element3/lib/theme-chalk/button.css";
// ...
// import {
  // ElButton,
  // ElRow,
  // ElCol,
  // ElMain,
  // .....
// } from "element3";

export default function (app) {
  // 残缺引入
  app.use(element3)
  
  // 按需引入
  // app.use(ElButton);
}

9. 路由懒加载

咱们晓得 Vue 是单页利用,所以如果没有用懒加载,就会导致进入首页时须要加载的内容过多,工夫过长,就会呈现长时间的白屏,很不利于用户体验,SEO 也不敌对

所以能够去用懒加载将页面进行划分,须要的时候才加载对应的页面,以分担首页的加载压力,缩小首页加载工夫

没有用路由懒加载:

import Home from '@/components/Home'
const router = new VueRouter({
  routes: [{ path: '/home', component: Home}
  ]
})

用了路由懒加载:

const router = new VueRouter({
  routes: [{ path: '/home', component: () => import('@/components/Home') },
    {path: '/login', component: require('@/components/Home').default }
  ]
})

在进入这个路由的时候才会走对应的 component,而后运行 import 编译加载组件,能够了解为 Promiseresolve 机制

  • import:Es6 语法标准、编译时调用、是解构过程、不反对变量函数等
  • require:AMD 标准、运行时调用、是赋值过程,反对变量计算函数等

更多无关前端模块化的内容能够看我另一篇文章 前端模块化标准具体总结

10. keep-alive 缓存页面

比方在表单输出页面进入下一步后,再返回上一步到表单页时要保留表单输出的内容、比方在 列表页 > 详情页 > 列表页,这样来回跳转的场景等

咱们都能够通过内置组件 <keep-alive></keep-alive> 来把组件缓存起来,在组件切换的时候不进行卸载,这样当再次返回的时候,就能从缓存中疾速渲染,而不是从新渲染,以节俭性能

只须要包裹想要缓存的组件即可

<template>
  <div id="app">
    <keep-alive>
      <router-view/>
    </keep-alive>
  </div>
</template>
  • 也能够用 include/exclude 来 缓存 / 不缓存 指定组件
  • 可通过两个生命周期 activated/deactivated 来获取以后组件状态

11. 事件的销毁

Vue 组件销毁时,会主动解绑它的全副指令及事件监听器,然而仅限于组件自身的事件

而对于 定时器
addEventListener 注册的监听器等,就须要在组件销毁的生命周期钩子中手动销毁或解绑,以防止内存泄露

<script>
export default {created() {this.timer = setInterval(this.refresh, 2000)
      addEventListener('touchmove', this.touchmove, false)
    },
    beforeDestroy() {clearInterval(this.timer)
      this.timer = null
      removeEventListener('touchmove', this.touchmove, false)
    }
}
</script>

12. 图片懒加载

图片懒加载就是对于有很多图片的页面,为了进步页面加载速度,只加载可视区域内的图片,可视区域外的等到滚动到可视区域后再去加载

这个性能一些 UI 框架都有自带的,如果没有呢?

举荐一个第三方插件 vue-lazyload

npm i vue-lazyload -S

// main.js
import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload)

// 接着就能够在页面中应用 v-lazy 懒加载图片了
<img v-lazy="/static/images/1.png">

或者本人造轮子,手动封装一个自定义指令,这里封装好了一个兼容各浏览器的版本的,次要是判断浏览器支不反对 IntersectionObserver API,反对就用它实现懒加载,不反对就用监听 scroll 事件 + 节流的形式实现

const LazyLoad = {
  // install 办法
  install(Vue, options) {
    const defaultSrc = options.default
    Vue.directive('lazy', {bind(el, binding) {LazyLoad.init(el, binding.value, defaultSrc)
      },
      inserted(el) {if (IntersectionObserver) {LazyLoad.observe(el)
        } else {LazyLoad.listenerScroll(el)
        }
      },
    })
  },
  // 初始化
  init(el, val, def) {el.setAttribute('data-src', val)
    el.setAttribute('src', def)
  },
  // 利用 IntersectionObserver 监听 el
  observe(el) {var io = new IntersectionObserver((entries) => {
      const realSrc = el.dataset.src
      if (entries[0].isIntersecting) {if (realSrc) {
          el.src = realSrc
          el.removeAttribute('data-src')
        }
      }
    })
    io.observe(el)
  },
  // 监听 scroll 事件
  listenerScroll(el) {const handler = LazyLoad.throttle(LazyLoad.load, 300)
    LazyLoad.load(el)
    window.addEventListener('scroll', () => {handler(el)
    })
  },
  // 加载实在图片
  load(el) {
    const windowHeight = document.documentElement.clientHeight
    const elTop = el.getBoundingClientRect().top
    const elBtm = el.getBoundingClientRect().bottom
    const realSrc = el.dataset.src
    if (elTop - windowHeight < 0 && elBtm > 0) {if (realSrc) {
        el.src = realSrc
        el.removeAttribute('data-src')
      }
    }
  },
  // 节流
  throttle(fn, delay) {
    let timer
    let prevTime
    return function (...args) {const currTime = Date.now()
      const context = this
      if (!prevTime) prevTime = currTime
      clearTimeout(timer)
 
      if (currTime - prevTime > delay) {
        prevTime = currTime
        fn.apply(context, args)
        clearTimeout(timer)
        return
      }
 
      timer = setTimeout(function () {prevTime = Date.now()
        timer = null
        fn.apply(context, args)
      }, delay)
    }
  },
}
export default LazyLoad

应用上是这样的,用 v-LazyLoad 代替 src

<img v-LazyLoad="xxx.jpg" />

13. SSR

这一点我在我的项目中也没有实际过,就不班门弄斧了,对于 SSR 的优化能够看这篇文章:Vue-SSR 优化计划具体总结,这里记录一下,就不搬过去了

往期精彩

  • 深入浅出虚构 DOM 和 Diff 算法,及 Vue2 与 Vue3 中的区别
  • Vue3 的 7 种和 Vue2 的 12 种组件通信,值得珍藏
  • 最新的 Vue3.2 都更新了些什么理解一下
  • JavaScript 进阶知识点
  • 前端异样监控和容灾
  • 20 分钟助你拿下 HTTP 和 HTTPS,坚固你的 HTTP 常识体系

结语

如果本文对你有一丁点帮忙,点个赞反对一下吧,感激感激

退出移动版