Vue 性能优化

412 阅读3分钟

一、按需加载组件

在首屏中存在大量不会显示的组件,比如商品营销弹窗、商品规格弹窗、活动规则弹窗等,虽然没有使用,但是也被一起加载进来,导致 js 文件体积过大加载缓慢,代码利用率低下。

使用 import() 动态导入,配合 Prefetching 食用更香 Webpack Code Splitting

1. Vue 中的异步组件

利用 v-if 的特性,在真正使用 Dialog 的时候才去加载代码,否则初始化渲染的时候就会立即加载没有使用到的组件。

<template>
  <div class="page">
    <!-- bad -->
    <Dialog :visible.sync="visible" />

    <!-- good -->
    <Dialog v-if="canLoad" :visible.sync="visible" />
    
    <button @click="showDialog">点击我显示 Dialog </button>
  </div>
<template>

export default {
  data () {
    return: {
      canLoad: false,
      visible: false
    }
   },
  methods: {
    showDialog () {
      this.canLoad = true
      this.visible = true
   }
  },
  components: {
    Dialog: () => import('./components/Dialog')
  }
}

2. component Vue 内置的动态组件

props: { is: string | ComponentDefinition | ComponentConstructor }
<template>
  <div class="page">
    <component :is="dialog" :visible.sync="visible" />
    <button @click="showDialog">点击我显示 Dialog </button>
  </div>
<template>

export default {
  data () {
    return: {
      dialog: null,
      visible: false
    }
   },
  methods: {
    showDialog () {
      import('./components/Dialog').then(res => {
        this.dialog = res.default
        this.visible = true
        
        // or 
        // this.dialog = Vue.extend(res.default)
      })
   }
  }
}

3. Vue.extend

// dialog.js
import Vue from 'vue'
const Dialog = () => import('./Dialog.vue')

let instance = null

export const showDialog = options => {
  if (instance) {
    instance.visibile = true
    return
  }

  Dialog()
    .then(res => {
      if (!instance) {
        const AgreementConstructor = Vue.extend(res.default)
        instance = new AgreementConstructor({
          el: document.createElement('div')
        })
        instance.close = url => {
          instance.visibile = false
        }
      }
      document.body.appendChild(instance.$el)
      instance.visibile = true
    })
}

二、使用函数式组件

使组件无状态 (没有 data) 和无实例 (没有 this 上下文)。他们用一个简单的 render 函数返回虚拟节点使它们渲染的代价更小。

1. 单文件组件

<template functional>
  <div @click="listeners.hello">
    {{ props.msg }}
  </div>
</template>

2. JSX

export default {
  components: {
    HelloWorld: {
      functional: true,
      props: {
        msg: {
          type: String,
          default: 'Hello'
        }
      },
      render (_, context) {
        return (
          <div onClick={ context.listeners.hello }>
            <p>{ context.props.msg }</p>
          </div>
        )
      }
    }
  }
}

三、减少 this 的访问次数

1. 在 computed 中

export default {
  // ...
  computed: {
  
    // bad
    fullName () {
      return this.firstName + ' ' + this.lastName
    },
    
    // good
    fullName ({ firstName, lastName }) {
      return firstName + ' ' + lastName
    }
  }
}

2. 在 methods 中

export default {
  // ...
  methods: {
  
    // bad
    fullName () {
      return this.firstName + ' ' + this.lastName
    },
    
    // good
    fullName ({ firstName, lastName }) {
      const { firstName, lastName } = this
      return firstName + ' ' + lastName
    }
  }
}

四、合理使用生命周期

1. 解绑事件

export default {
  // bad
  created () {
    window.addEventListener('scroll', this.scroll)
  }
  beforeDestroy () {
    window.removeEventListener('scroll', this.scroll)
  }

  // good
  created () {
    window.addEventListener('scroll', this.scroll)
    this.$on('hook:beforeDestroy', () => {
      window.removeEventListener('scroll', this.scroll)
    })
  }
}

2. 代码拆分

export default {
  created: [
    function () { // task1.. },
    function () { // task2.. },
    function () { // task3.. },
    // ...
  ]
}

五、Data & Watch

1. 尽量减少 data 中数据的嵌套深度

data 中对象嵌套过深,Vue 初始化时需要递归进行响应式数据绑定,影响性能,白屏时间长。

export default {
  data () {
    return {
      // bad
      data: { obj: { obj: { name: '星星' } } },
      
      // good
      data: null
    }
  },
  created () {
    this.ajax()
  },
  methods: {
    ajax () {
      // 发送 ajax
      this.data = { /** ajax 返回值 **/ }  
    }
  }
}

2. 使用 Object.freeze()

Object.freeze() 方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。freeze() 返回和传入的参数相同的对象。

官方优化示例

Vue 中使用 Object.isExtensible() 判断一个对象是否是可扩展的(是否可以在它上面添加新的属性),不可扩展就不会对数据进行响应式绑定(不会遍历数据的每个 key 进行劫持),适合展示类的场景。

export default {
  data () {
    return {
      data: Object.freeze({ name: '星星' })
    }
  },
  methods: {
    setData () {
      // 报错
      this.data.name = 'star'
      
      // 新的响应式对象
      this.data = { name: 'star' }
      
      // 新的冻结对象
      this.data = Object.freeze({ name: 'star' })
    }
  }
}

3. 尽量减少在 watch 中使用 deep

deep 选项开启时,Vue 初始化时需要递归进行依赖收集,影响性能,白屏时间长。

推荐使用键路径。

export default {
  data () {
    return {
      data: { obj: { obj: { name: '星星' } } },
    }
  },
  watch: {
    // bad
    data: {
      deep: true,
      handler (val) {}
    },
    
    // good
    // 键路径
    'data.obj.obj.name' (val) {} // 不会递归
  }
}

六、组件化

父组件状态变化,所有子组件会创建新的 VNode,并且进行 diff 操作,当 this.getProduct 执行时更新了状态,banner 列表会进行无用 diff,造成浪费。

子组件内部状态变化,不会影响父组件。

// bad
<template>
  <div>
    <div class="banner">
      <div v-for="banner in banners">...</div>
    </div>
    <div class="product">
      <div v-for="product in products">...</div>
    </div>
  </div>
</template>

<script>
export default {
  data () {
    return {
      banners: [],
      products: []
    }
  },
  created () {
    this.getBanner()  
    setTimeout(this.getProduct, 1000)
  }
  methods: {
    getBanner () {
      // ajax...
      this.banners = [ ... ]
    },
    getProduct () {
      // ajax...
      this.products = [ ... ]
    }
  }
}
</script>
// good

<template>
  <div>
    <!-- 内部维护状态,互不影响 -->
    <banner></banner>
    <product></product>
  </div>
</template>

<script>
// ...
export default {
  components: {
    Banner,
    Product
  }
}
</script>

总结

本文介绍了几种 Vue 代码层面的优化,我会不断补充其他问题及优化措施,希望对读完本文的你有帮助、有启发,如果有不足之处,欢迎批评指正交流!

参考链接

9 performance secrets revealed