赞
踩
使用key的注意事项: 不要使用可能重复的或者可能变化 key 值(控制台也会给出提醒) 如果数组中的数据有状态需要维持时(例如输入框),不要使用数组的 index 作为 key 值,因为如果在数组中插入或者移除一个元素时,其后面的元素 index 将会变化,这回让vue进行原地复用时错误的绑定状态。 如果数组中没有唯一的 key值可用,且数组更新时不是全量更新而是采用类似push,splice来插入或者移除数据时,可以考虑对其添加一个 key 字段,值为 Symbol() 即可保证唯一。 // Symbol()为key let user = { name: 'lucy', type: Symbol() } let name = { name: 'lucy', type: Symbol() } let obj = { [user.type]: { Math: 94, Chinese: 88 }, [name.type]: { Math: 99, Chinese: 80 } } console.log(obj) console.log(obj[user.type]) console.log(obj[name.type])
vue 中的 原地复用 (大概就是 虚拟dom 变化时,两个 虚拟dom节点 的 key 如果一样就不会重新创建节点,而是修改原来的节点)
当我们渲染的数据不需要保持状态时,例如常见的单纯的表格分页渲染(不包含输入,只是展示)、下拉加载更多等场景,那么使用 index 作为 key 再好不过,因为进入下一页或者上一页时就会原地复用之前的节点,而不是重新创建,如果使用唯一的 id 作为 key 反而会重新创建dom,性能相对较低。
此外使用 index 作为 key 我还应该要尽量避免对数组的中间进行 增加/删除 等会影响后面元素key变化的操作。这会让 vue 认为后面所有元素都发生了变化,导致多余的对比和原地复用。
1.数据没有独立的状态
2.数据不会进行 增加/删除 等会影响后面元素key变化的操作
对于大多数数据的 id 都是唯一的,这无疑的一个 key 的优选答案。对于任何大多数情况使用 id 作为 key 都不会出现上面 bug。但是如果你需要考虑性能问题,那就就要思考是否应该使用原地复用了。
同样是上面的分页数据展示,如果使用 id 作为 key ,可想而知每一页的每一条数据 id 都是不一样的,所以当换页时两颗 虚拟DOM树 的节点的 key 完全不一致,vue 就会移除原来的节点然后创建新的节点。可想而知效率会更加低下。但是他也有它的优点。唯一的 key 可以帮助 diff 更加精确的为我们绑定状态,这尤其适合数据有独立的状态的场景,例如带输入框或者单选框的列表数据。
所以何时使用 id 作为 key?只有一点:
无法使用 index 作为 key 的时候
原因:默认情况下,Vue 会尽可能高效的更新 DOM。这意味着其在相同类型的元素之间切换时,会修补已存在的元素,而不是将旧的元素移除然后在同一位置添加一个新元素。如果本不相同的元素被识别为相同,则会出现意料之外的副作用。
有 v-if ,并且有 v-else 或者 v-if-else,就有必要加 key 了。v-if/v-else-if/v-else 中的 key 相对简单,我们可以直接写入固定的字符串或者数组即可。
以下例子对 button 添加了 过渡效果, 但是如果不添加 key 切换时是无法触发过渡的
<transition> <button v-if="isEditing" key='false' v-on:click="isEditing = false" > Save </button> <button v-else key='true' v-on:click="isEditing = true" > Edit </button> </transition> .v-enter-active, .v-leave-active { transition: all 1s; } .v-enter, .v-leave-to { opacity: 0; transform: translateY(30px); } .v-leave-active { position: absolute; }
此优化技巧仅限于Vue2,Vue3 中对 v-for 和 v-if 的优先级做了调整
vue2中 v-for 的优先级高于 v-if,所以当它们使用再同一个标签上是,每一个渲染都会先循环再进行条件判断. Vue3 中 v-if 优先级高于 v-for,所以当 v-for 和 v-if 一起使用时效果类似于 Vue2 中把 v-if 上提的效果。
1.使用 computed 来对遍历对象进行过滤
// let usersActive = computed(()=>users.filter(user => user.active))
2.尽量将 v-if 移动到上级
// 如果不想让循环的内容多出一个无需有的上级容器,可以选择使用 template 来作为其父元素,template 不会被浏览器渲染为 DOM 节点
v-if 指令在编译阶段就会编译成一个三元运算符,条件渲染。当条件 props.value 的值变化的时候,会触发对应的组件更新,对于 v-if 渲染的节点,由于新旧节点 vnode 不一致,在核心 diff 算法比对过程中,会移除旧的 vnode 节点,创建新的 vnode 节点,那么就会创建新的 Heavy 组件,又会经历 Heavy 组件自身初始化、渲染 vnode、patch 等过程。
v-show 当条件 props.value 的值变化的时候,不会重新创建节点,只更新节点,由于新旧 vnode 一致,它们只需要一直 patchVnode 即可。
那么它又是怎么让 DOM 节点显示和隐藏的呢?
原来在 patchVnode 过程中,内部会对执行 v-show 指令对应的钩子函数 update,然后它会根据 v-show 指令绑定的值来设置它作用的 DOM 元素的 style.display 的值控制显隐。
因此相比于 v-if 不断删除和创建函数新的 DOM,v-show 仅仅是在更新现有 DOM 的显隐值,所以 v-show 的开销要比 v-if 小的多,当其内部 DOM 结构越复杂,性能的差异就会越大。
但是 v-show 相比于 v-if 的性能优势是在组件的更新阶段,如果仅仅是在初始化阶段,v-if 性能还要高于 v-show,原因是在于它仅仅会渲染一个分支,而 v-show 把两个分支都渲染了,通过 style.display 来控制对应 DOM 的显隐。
在使用 v-show 的时候,所有分支内部的组件都会渲染,对应的生命周期钩子函数都会执行,而使用 v-if 的时候,没有命中的分支内部的组件是不会渲染的,对应的生命周期钩子函数都不会执行。
v-if 和 v-show 的区别相比大家都非常熟悉了;v-if 通过直接操作 DOM 的删除和添加来控制元素的显示和隐藏;v-show 是通过控制 DOM 的 display CSS熟悉来控制元素的显示和隐藏
由于对 DOM 的 添加/删除 操作性能远远低于操作 DOM 的 CSS 属性
所以当元素需要频繁的 显示/隐藏 变化时,我们使用 v-show 来提高性能。
当元素不需要频繁的 显示/隐藏 变化时,我们通过 v-if 来移除 DOM 可以节约掉浏览器渲染这个的一部分DOM需要的资源。
1.应该把复杂计算属性分割为尽可能多的更简单的 property。他体现的是一种空间换时间的优化思想
computed会在其表达式中依赖的响应式数据发送变化时重新计算。如果我们在一个计算属性中书写了比较复杂的表达式,那么其依赖的响应式数据也任意变得更多。当其中任何一个依赖项变化时整个表达式都需要重新计算。
// 计算金额
let price = computed(()=>{
let basePrice = manufactureCost / (1 - profitMargin)
return (
basePrice -
basePrice * (discountPercent || 0)
)
})
以上写法,manufactureCost、profitMargin、discountPercent 中任何一个变化时都会重新计算整个 price。
// 优化
let basePrice = computed(() => manufactureCost / (1 - profitMargin))
let discount = computed(() => basePrice * (discountPercent || 0))
let finalPrice = computed(() => basePrice - discount)
如果当 discountPercent 变化时,只会 重新计算 discount 和 finalPrice,由于 computed 的缓存特性,不会重新计算 basePrice
注意,这仅仅在 Vue2 中被作为一种优化手段,在 3.x 中,有状态组件和函数式组件之间的性能差异已经大大减少,并且在大多数用例中是微不足道的。因此,在 SFCs 上使用 functional 的开发人员的迁移路径是删除该 attribute,并将 props 的所有引用重命名为 $props,将 attrs 重命名为 $attrs。
// 纯展示组件,无this,无相应数据 // 优化前 <template> <div class="cell"> <div v-if="value" class="on"></div> <section v-else class="off"></section> </div> </template> <script> export default { props: ['value'], } </script> // 函数式组件 // 优化后-性能提升 <template functional> <div class="cell"> <div v-if="props.value" class="on"></div> <section v-else class="off"></section> </div> </template> <script> export default { props: ['value'], } </script>
JS 引擎是单线程的运行机制,JS 线程会阻塞 UI 线程,所以当脚本执行时间过长,就会阻塞渲染,导致页面卡顿。而优化后的 script 执行时间短,所以它的性能更好。
那么,为什么用函数式组件 JS 的执行时间就变短了呢?这要从函数式组件的实现原理说起了,你可以把它理解成一个函数,它可以根据你传递的上下文数据渲染生成一片 DOM。
函数式组件和普通的对象类型的组件不同,它不会被看作成一个真正的组件,我们知道在 patch 过程中,如果遇到一个节点是组件 vnode,会递归执行子组件的初始化过程;而函数式组件的 render 生成的是普通的 vnode,不会有递归子组件的过程,因此渲染开销会低很多。
因此,函数式组件也不会有状态,不会有响应式数据,生命周期钩子函数这些东西。你可以把它当成把普通组件模板中的一部分 DOM 剥离出来,通过函数的方式渲染出来,是一种在 DOM 层面的复用。
1.尽可能的遵循单一功能原则
2.合理的拆分组件不仅仅可以优化性能,还能够让代码更清晰可读。
函数在页面渲染的话,建议使用计划属性优化,这里的heavy()函数是模仿耗时任务的渲染
// 优化前的组件代码如下 <template> <div :style="{ opacity: number / 300 }"> <div>{{ heavy() }}</div> </div> </template> <script> export default { props: ['number'], methods: { heavy () { const n = 100000 let result = 0 for (let i = 0; i < n; i++) { result += Math.sqrt(Math.cos(Math.sin(42))) } return result } } } </script>
由于 Vue 的更新是组件粒度的(以组件为基础),虽然每一帧都通过数据修改导致了父组件的重新渲染,但是 ChildComp 却不会重新渲染,因为它的内部也没有任何响应式数据的变化。所以优化后的组件不会在每次渲染都执行耗时任务,自然执行的 JavaScript 时间就变少了。
<template> <div :style="{ opacity: number / 300 }"> <ChildComp/> </div> </template> <script> export default { components: { ChildComp: { methods: { heavy () { const n = 100000 let result = 0 for (let i = 0; i < n; i++) { result += Math.sqrt(Math.cos(Math.sin(42))) } return result }, }, render (h) { return h('div', this.heavy()) } } }, props: ['number'] } </script>
解决页面卡顿,或者长时间白屏问题
<template>
<div class="deferred-off">
<VueIcon icon="fitness_center" class="gigantic"/>
<h2>I'm an heavy page</h2>
<Heavy v-for="n in 8" :key="n"/>
<Heavy class="super-heavy" :n="9999999"/>
</div>
</template>
<template> <div class="deferred-on"> <VueIcon icon="fitness_center" class="gigantic"/> <h2>I'm an heavy page</h2> <template v-if="defer(2)"> <Heavy v-for="n in 8" :key="n"/> </template> <Heavy v-if="defer(3)" class="super-heavy" :n="9999999"/> </div> </template> <script> import Defer from '@/mixins/Defer' export default { mixins: [ Defer(), ], } </script>
export default function (count = 10) { return { data () { return { displayPriority: 0 } }, mounted () { this.runDisplayPriority() }, methods: { runDisplayPriority () { const step = () => { requestAnimationFrame(() => { this.displayPriority++ if (this.displayPriority < count) { step() } }) } step() }, defer (priority) { return this.displayPriority >= priority } } } }
Defer 的主要思想就是把一个组件的一次渲染拆成多次,它内部维护了 displayPriority 变量,然后在通过 requestAnimationFrame 在每一帧渲染的时候自增,最多加到 count。然后使用 Defer mixin 的组件内部就可以通过 v-if=“defer(xxx)” 的方式来控制在 displayPriority 增加到 xxx 的时候渲染某些区块了。
当你有渲染耗时的组件,使用 Deferred 做渐进式渲染是不错的注意,它能避免一次 render 由于 JS 执行时间过长导致渲染卡住的现象。
时间切片的核心思想是:如果任务不能在50毫秒内执行完,那么为了不阻塞主线程,这个任务应该让出主线程的控制权,使浏览器可以处理其他任务。让出控制权意味着停止执行当前任务,让浏览器去执行其他任务,随后再回来继续执行没有执行完的任务。
所以时间切片的目的是不阻塞主线程,而实现目的的技术手段是将一个长任务拆分成很多个不超过50ms的小任务分散在宏任务队列中执行。
使用时间切片的缺点是,任务运行的总时间变长了,这是因为它每处理完一个小任务后,主线程会空闲出来,并且在下一个小任务开始处理之前有一小段延迟。
fetchItems ({ commit }, { items }) {
commit('clearItems')
commit('addItems', items)
}
也可以使用生成器Generator函数来实现时间切片
fetchItems ({ commit }, { items, splitCount }) {
commit('clearItems')
const queue = new JobQueue()
splitArray(items, splitCount).forEach(
chunk => queue.addJob(done => {
// 分时间片提交数据
requestAnimationFrame(() => {
commit('addItems', chunk)
done()
})
})
)
await queue.start()
}
优化前总的 script 执行时间要比优化后的还要少一些,但是从实际的观感上看,优化前点击提交按钮,页面会卡死 1.2 秒左右,在优化后,页面不会完全卡死,但仍然会有渲染卡顿的感觉。
那么为什么在优化前页面会卡死呢?
因为一次性提交的数据过多,内部 JS 执行时间过长,阻塞了 UI 线程,导致页面卡死。
优化后,页面仍有卡顿,是因为我们拆分数据的粒度是 1000 条,这种情况下,重新渲染组件仍然有压力,我们观察 fps 只有十几,会有卡顿感。通常只要让页面的 fps 达到 60,页面就会非常流畅,如果我们把数据拆分粒度变成 100 条,基本上 fps 能达到 50 以上,虽然页面渲染变流畅了,但是完成 10000 条数据总的提交时间还是变长了。
使用 Time slicing 技术可以避免页面卡死,通常我们在这种耗时任务处理的时候会加一个 loading 效果,在这个示例中,我们可以开启 loading animation,然后提交数据。对比发现,优化前由于一次性提交数据过多,JS 一直长时间运行,阻塞 UI 线程,这个 loading 动画是不会展示的,而优化后,由于我们拆成多个时间片去提交数据,单次 JS 运行时间变短了,这样 loading 动画就有机会展示了。
注意⚠️
这里要注意的一点,虽然我们拆时间片使用了 requestAnimationFrame API,但是使用 requestAnimationFrame 本身是不能保证满帧运行的,requestAnimationFrame 保证的是在浏览器每一次重绘后会执行对应传入的回调函数,想要保证满帧,只能让 JS 在一个 Tick 内的运行时间不超过 17ms。
vue中响应式数据需要额外的对其绑定get、set处理函数,如果你的某些数据不会发生变化或者你不希望它的变化会导致任何副作用(更新视图或者其他)。一般我会这样定义他。
export default {
data() {
this.version = '10'; // 不会被做响应式处理
return {
/* ... */
}
}
}
Tips: 其实这种方式并不是最好的,因为这会将数据直接绑定到vue实例上,而vue更希望数据能够统一在data中,然后通过代理到vue实例上的方式来访问。所以更好的方式应该是对data中的数据进行冻结。
在Vue3中无法通过以上方式来解决,因为Proxy代理的粒度是整个对象而不是某一个属性。
const data = items.map(
item => ({
id: uid++,
data: item,
vote: 0
})
)
const data = items.map( item => optimizeItem(item) ) function optimizeItem (item) { const itemData = { id: uid++, vote: 0 } Object.defineProperty(itemData, 'data', { // Mark as non-reactive configurable: false, value: item }) return itemData }
优化后执行 script 的时间要明显少于优化前的,因此性能体验更好。
之所以有这种差异,是因为内部提交的数据的时候,会默认把新提交的数据也定义成响应式,如果数据的子属性是对象形式,还会递归让子属性也变成响应式,因此当提交数据很多的时候,这个过程就变成了一个耗时过程。
而优化后我们把新提交的数据中的对象属性 data 手动变成了 configurable 为 false,这样内部在 walk 时通过 Object.keys(obj) 获取对象属性数组会忽略 data,也就不会为 data 这个属性 defineReactive,由于 data 指向的是一个对象,这样也就会减少递归响应式的逻辑,相当于减少了这部分的性能损耗。数据量越大,这种优化的效果就会更明显。
其实类似这种优化的方式还有很多,比如我们在组件中定义的一些数据,也不一定都要在 data 中定义。有些数据我们并不是用在模板中,也不需要监听它的变化,只是想在组件的上下文中共享这个数据,这个时候我们可以仅仅把这个数据挂载到组件实例 this 上,例如:
export default {
created() {
// 直接在created中挂载到实例上
this.scroll = null
},
mounted() {
this.scroll = new BScroll(this.$el)
}
}
// 可以在组件上下文中共享 scroll 对象了,但是它不是一个响应式对象。
只渲染视口内的 DOM,这样总共渲染的 DOM 数量就很少了,自然性能就会好很多。
在一些渲染成本比较高的组件需要被经常切换时,可以使用 keep-alive 来缓存这个组件。
而在使用 keep-alive 后,被 keep-alive 包裹的组件在经过第一次渲染后,的 vnode 以及 DOM 都会被缓存起来,然后再下一次再次渲染该组件的时候,直接从缓存中拿到对应的 vnode 和 DOM,然后渲染,并不需要再走一次组件初始化,render 和 patch 等一系列流程,减少了 script 的执行时间,性能更好。
注意: 滥用 keep-alive 只会让你的应用变得更加卡顿,因为他会长期占用较大的内存
// 优化前
<template>
<div id="app">
<router-view/>
</div>
</template>
// 优化后
<template>
<div id="app">
<keep-alive>
<router-view/>
</keep-alive>
</div>
</template>
当一个组件被销毁时,我们应该清除组件中添加的 全局事件 和 定时器 等来防止内存泄漏
Vue3 的 HOOK 可以让我们将事件的声明和销毁写在一起,更加可读
function scrollFun(){ /* ... */}
document.addEventListener("scroll", scrollFun)
onBeforeUnmount(()=>{
document.removeEventListener("scroll", scrollFun)
})
// 推荐写法一: function scrollFun(){ /* ... */} document.addEventListener("scroll", scrollFun) this.$once('hook:beforeDestroy', ()=>{ document.removeEventListener("scroll", scrollFun) }) // 写法二 function scrollFun(){ /* ... */} export default { created() { document.addEventListener("scroll", scrollFun) }, beforeDestroy(){ document.removeEventListener("scroll", scrollFun) } }
图片懒加载:适用于页面上有较多图片且并不是所有图片都在一屏中展示的情况,vue-lazyload 插件给我们提供了一个很方便的图片懒加载指令 v-lazy
但是并不是所有图片都适合使用懒加载,例如 banner、相册等 更加推荐使用图片预加载技术,将当前展示图片的前一张和后一张优先下载。
使用webp格式:这个没什么好说的,大家都知道WebP的优势体现在它具有更优的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量;同时具备了无损和有损的压缩模式、Alpha 透明以及动画的特性,在 JPEG 和 PNG 上的转化效果都相当优秀、稳定和统一。
还有一种优化用户体验的方式,就是使用交错GIF或者是渐进(Progressive Encoding)JPEG的图片。渐进JPEG文件首先是模糊的,然后渐渐清晰起来。
JPEG文件格式有两种保存方式。他们是Baseline JPEG和Progressive JPEG。
两种格式有相同尺寸以及图像数据,他们的扩展名也是相同的,唯一的区别是二者显示的方式不同。
用户体验 一个以progressive方式编码的jpeg文件,在浏览器上的渲染方式是由模糊到清晰的。用户能在渐变的图像当中获得所需信息的反馈。如果内容不是用户所期待的,用户就能提前前往新的页面。
文件大小 有实验证明,在JPEG文件小于10KB的时候,使用标准型编码(Huffman表已经被优化)的JPEG文件要小于使用渐变式编码的JPEG文件(发生概率为75%)。当文件大于10KB时,渐变式编码的JPEG文件有94%的概率拥有比标准编码的文件更小的体积。
例如一个将数组转化为多级结构的方法
** * 数组转树形结构,时间复杂度O(n) * @param list 数组 * @param idKey 元素id键 * @param parIdKey 元素父id键 * @param parId 第一级根节点的父id值 * @return {[]} */ function listToTree (list,idKey,parIdKey,parId) { let map = {}; let result = []; let len = list.length; // 构建map for (let i = 0; i < len; i++) { //将数组中数据转为键值对结构 (这里的数组和obj会相互引用,这是算法实现的重点) map[list[i][idKey]] = list[i]; } // 构建树形数组 for(let i=0; i < len; i++) { let itemParId = list[i][parIdKey]; // 顶级节点 if(itemParId === parId) { result.push(list[i]); continue; } // 孤儿节点,舍弃(不存在其父节点) if(!map[itemParId]){ continue; } // 将当前节点插入到父节点的children中(由于是引用数据类型,obj中对于节点变化,result中对应节点会跟着变化) if(map[itemParId].children) { map[itemParId].children.push(list[i]); } else { map[itemParId].children = [list[i]]; } } return result; }
我在项目中关于首屏优化主要有以下几个优化方向
体积
代码分割
网络
webpack 和 vite 的生产环境打包默认就会压缩你的代码,这个一般不需要特殊处理,webpack 也可以通过对应的压缩插件手动实现
可以查看你的打包产物中是否有 .map 文件,如果有你可以将 source-map 的值设置为false或者空来关闭代码映射(这个占用的体积是真的大)
这个需要服务器也开启允许 gizp 传输,不然启用了也没啥用( webpack 有对应的 gzip 压缩插件,不同版本的 webpack 压缩插件可能不同,建议先到官网查询)
将打包产物分割为一个一个的小产物,其依赖 esModule。所以当你使用 import() 函数来导入一个文件或者依赖,那么这个文件或者依赖就会被单独打包为一个小产物。路由懒加载 和 异步组件 都是使用这个原理。
路由懒加载
异步组件
对于 UI库我一般不会使用按需加载组件,而是比较喜欢 CDN 引入的方式来优化。
CDN: 首先就是上面的说的 CDN 引入把,开发阶段使用本地库,通过配置 外部扩展(Externals) 打包时来排除这些依赖。然后在 html 文件中通过 CDN 的方式来引入它们
Server Push: HTTP2已经相对成熟了;经过上面的 CDN 引入,我们可以对网站使用 HTTP2 的 Server Push 功能来让浏览器提前加载 这些 CDN 和 其他文件。
开启 gzip: 这个上面已经说过了,其原理就是当客户端和服务端都支持 gzip 传输时,服务端会优先发送经过 gzip 压缩过的文件,然后客户端接收到在进行解压。
开启缓存: 一般我使用的是协商缓存,但是这并不适用于所有情况,例如对于使用了 Server Push 的文件,就不能随意的修改其文件名。所以我一般还会将生产的主要文件固定文件名
可以在核心文件加载完成之前,通过展示loading或者骨架屏等方式来提升用户体验。即可缩短白屏时间。
但是需要注意的是,页面刚开始加载时有许多资源需要加载,如果将loading相关的资源放到dom后的话,有可能会导致loading的资源被其他资源阻塞。
所以推荐loading相关的css或者js代码最好是内联到html中的头部,这样即可保证展示loading时对应的css和js已经加载完成。并且不推荐loading中使用高性能或者高网络消化的逻辑,这样会延长后面其他资源的解析或者加载时间。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。