使用 key
对于通过循环生成的列表,应给每个列表项一个稳定且唯一的 key ,这有利于在列表变动时,尽量少的删除、新增、改动元素;
使用冻结对象
冻结的对象不会被响应化;
-
App.vue
<template> <div id="app"> <button @click="loadNormalDatas">load normal datas</button> <button @click="loadFrozenDatas">load frozen datas</button> <h1>normal datas count: {{ normalDatas.length }}</h1> <h1>freeze datas count: {{ freezeDatas.length }}</h1> </div> </template> <script> export default { data() { return { normalDatas: [], freezeDatas: [], }; }, methods: { loadNormalDatas() { this.normalDatas = this.getDatas(); console.log('normalDatas', this.normalDatas); }, loadFrozenDatas() { // 使用冻结对象 this.freezeDatas = Object.freeze(this.getDatas()); console.log('freezeDatas', this.freezeDatas); }, getDatas() { const result = []; for (var i = 0; i < 1000000; i++) { result.push({ id: i, name: `name${i}`, address: { city: `city${i}`, province: `province${i}`, }, }); } return result; }, }, }; </script> <style> #app { text-align: center; } </style>
-
使用灯塔监控性能
使用函数式组件
参见函数式组件
-
App.vue
<template> <div id="app"> <button @click="normalCount = 10000">生成10000个普通组件</button> <button @click="functionalCount = 10000">生成10000个函数组件</button> <div class="container"> <div class="item"> <NormalComp v-for="n in normalCount" :key="n" :count="n"></NormalComp> </div> <div class="item"> <FunctionalComp v-for="n in functionalCount" :key="n" :count="n"></FunctionalComp> </div> </div> </div> </template> <script> import NormalComp from './components/NormalComp'; import FunctionalComp from './components/FunctionalComp'; export default { components: { NormalComp, FunctionalComp, }, data() { return { functionalCount: 0, normalCount: 0, }; }, mounted() { window.vm = this; }, }; </script> <style> #app { text-align: center; } .container { width: 90%; display: flex; margin: 0 auto; } .item { padding: 30px; border: 1px solid #ccc; margin: 1em; flex: 1 1 auto; } </style>
-
普通组件
<template> <h1>NormalComp: {{ count }}</h1> </template> <script> export default { props: { count: Number, }, }; </script> <style></style>
-
函数式组件
<template functional> <h1>NormalComp: {{ props.count }}</h1> </template> <script> export default { functional: true, props: { count: Number, }, }; </script> <style></style>
-
灯塔测试函数式组件的性能
使用计算属性
如果模板中某个数据会使用多次,并且该数据是通过计算得到的,使用计算属性以缓存它们;
保持对象引用稳定
-
在绝大部分情况下,vue 触发 rerender 的时机是其依赖的数据发生 变化,若数据没有发生变化,哪怕给数据重新赋值了, vue 也是不会做出任何处理的;
-
下面是 vue 判断数据 没有变化 的源码:
// value 为旧值, newVal 为新值 if (newVal === value || (newVal !== newVal && value !== value)) { return }
-
因此,如果需要,只要能保证组件的依赖数据不发生变化,组件就不会重新渲染;
- 对于原始数据类型,保持其值不变即可;
- 对于对象类型,保持其引用不变即可;
-
从另一方面来说,由于可以通过保持属性引用稳定来避免子组件的重渲染,那么应该细分组件来尽量避免多余的渲染;
延迟装载
首页白屏
首页白屏时间主要受到两个因素的影响:
- 打包体积过大(本节不予讨论):巨型包需要消耗大量的传输时间,导致 JS 传输完成前页面只有一个 <div> ,没有可显示的内容;
- 需要立即渲染的内容太多:JS 传输完成后,浏览器开始执行 JS 构造页面,但可能一开始要渲染的组件太多,不仅 JS 执行的时间很长,而且执行完后浏览器要渲染的元素过多,从而导致页面白屏;
延迟装载
一个可行的办法就是 延迟装载组件 ,让组件按照指定的先后顺序依次一个一个渲染出来;
延迟装载是一个思路,本质上就是利用 requestAnimationFrame 事件分批渲染内容,它的具体实现多种多样;
-
app.vue
<template> <div class="container"> <div class="block" v-for="n in 21" v-if="defer(n)"> <heavy-comp></heavy-comp> </div> </div> </template> <script> import HeavyComp from './components/HeavyComp.vue'; import defer from './mixin/defer'; export default { mixins: [defer(21)], components: { HeavyComp }, }; </script> <style scoped> .container { display: grid; grid-template-columns: repeat(3, 1fr); grid-gap: 1em; } .block { border: 3px solid #f40; } </style>
-
defer.js
/*** * 在 maxFrameCount 个渲染帧之内把页面渲染出来 * @param {*} maxFrameCount 最大渲染帧 * @returns */ export default function (maxFrameCount) { return { data() { return { frameCount: 0, }; }, mounted() { const refreshFrameCount = () => { requestAnimationFrame(() => { this.frameCount++; if (this.frameCount < maxFrameCount) { refreshFrameCount(); } }); }; refreshFrameCount(); }, methods: { // 希望这个组件超过第 showInFrameCount 帧的时候渲染 defer(showInFrameCount) { return this.frameCount >= showInFrameCount; }, }, }; }
使用 keep-alive
长列表优化
-
RecycleScroller.vue
<template> <div class="recycle-scroller-container" @scroll="setPool" ref="container"> <div class="recycle-scroller-wrapper" :style="{ height: `${totalSize}px` }"> <div class="recycle-scroller-item" v-for="poolItem in pool" :key="poolItem.item[keyField]" :style="{ transform: `translateY(${poolItem.position}px)`, }" > <slot :item="poolItem.item"></slot> </div> </div> </div> </template> <script> var prev = 10, next = 10; export default { props: { // 数据的数组 items: { type: Array, default: () => [], }, // 每条数据的高度 itemSize: { type: Number, default: 0, }, keyField: { // 给我的items数组中,每个对象哪个属性代表唯一且稳定的编号 type: String, default: 'id', }, }, data() { return { pool: [ // { item: 原始数据, position: 该数据对应的偏移位置 } ], // 渲染池,保存当前需要渲染的数据 }; }, mounted() { this.setPool(); window.vm = this; }, computed: { totalSize() { return this.items.length * this.itemSize; // 总高度 }, }, methods: { setPool() { const scrollTop = this.$refs.container.scrollTop; const height = this.$refs.container.clientHeight; // 开始和结束索引 let startIndex = Math.floor(scrollTop / this.itemSize); let endIndex = Math.ceil((scrollTop + height) / this.itemSize); // 前面多渲染 prev 个节点 // 后面多渲染 next 个节点 startIndex -= prev; if (startIndex < 0) { startIndex = 0; } endIndex += next; // 偏移量 const startPos = startIndex * this.itemSize; this.pool = this.items.slice(startIndex, endIndex).map((it, i) => ({ item: it, position: startPos + i * this.itemSize, })); }, }, }; </script> <style> .recycle-scroller-container { overflow: auto; } .recycle-scroller-wrapper { position: relative; } .recycle-scroller-item { position: absolute; width: 100%; left: 0; top: 0; } </style>
-
App.vue
<template> <div id="app"> <RecycleScroller :items="items" :itemSize="54" class="scroller" v-slot="{ item }"> <ListItem :item="item" /> </RecycleScroller> </div> </template> <script> import ListItem from './components/ListItem.vue'; import RecycleScroller from './components/RecycleScroller'; // import { RecycleScroller } from 'vue-virtual-scroller'; // import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'; var items = []; for (var i = 0; i < 10000; i++) { items.push({ id: i + 1, count: i + 1 }); } export default { components: { ListItem, RecycleScroller }, data() { return { items, }; }, }; </script> <style> #app { width: 100%; margin: 0 auto; } .scroller { width: 500px; margin: 0 auto; height: 500px; } </style>
vue2.x✍️ 组件间通信
上一篇