点击关注公众号,“技术干货” 及时达!
前言
最近负责了项目的一个大迭代,然后目前基本的功能都是实现了,也上了生产。但是呢,大佬们可以先看下面这张图,cpu占用率100%,「真的卡了不得了」哈哈哈,用户根本没有一点使用体验。还有就是「首屏加载」,我靠说实话,真的贼夸张,首屏加载要十来秒,打开控制台一看,一个js资源加载就要七八秒。本来呢,我在这个迭代中我应该是负责开发需求的那个「底层苦力码农」,而这种「性能优化」这种活应该是组长架构师来干的,我这种小菜鸡应该是拿个小本本偷偷记笔记的,但是组长离职跳槽了,哥们「摇身一变变成了项目负责人」哈哈哈了。所以就有了这篇文章,和大家分享记录一下,毕业几个月的菜鸡的性能优化思路和手段,也希望大佬们给指点一下。
❝先和大家说一下。这个页面主要有两个问题 「卡顿」 和 「首屏加载」,本来这篇文章是打算把我优化这两个问题的思路和方法都一起分享给大家的,但是我码完「卡顿的思路和方法」后发现写的有点多。所以这篇文章就只介绍我「优化卡顿」的思路和方法,首屏加载我会另外发一篇文章。
❞
卡顿
这个页面卡顿呢,主要是由于这个表格的原因,很多人应该会想着表格为什么会卡顿啊,但是我这个表格是真的牛逼,大家可以看我这篇文章 “不是吧,刚毕业几个月的前端,就写这么复杂的表格??”,顺便给我装一下杯,这篇文章上了「前端热榜第一(还是断层霸榜哦)」(手动狗头)。
言归正传,为了一些盆友们不想看那篇文章,我给大家总结一下(最好看一下嘿嘿嘿),这个表格整体就是由三个表格合成为一个表格的,所以这个页面相当于有三个表格。因为它是一个整体的,所以我就需要去监听这个「三个表格」滚动事件去保证它「表现为一个表格」,其实就是保证他们滚动同步,以及信息栏浮层正确的计算位置,有点啰嗦了哈哈哈。
其实可以看到,很明显的卡顿。而且,这还是「最普通的时候」,为什么说普通呢,因为这个项目是金融方面的,所以这些数据都是需要「实时更新」的,我录制的这个动图是没有进行数据更新的。然后这个表格是一共是有四百来条数据,四百来条实时更新,这也就是为什么「cpu占用率百分百」的主要原因。再加之为了实现三个表格表现为一个表格,不得不给每一个表格都监听滚动事件,去改变剩下两个表格滚动条,然后改变滚动条也会触发滚动事件,也就是说滚动一下,触发了三个函数,啥意思呢,就比如说「我本来只用执行1行代码,现在会执行3行代码」(如果看不明白,去上面那边文章的demo跑一下就知道了)。所以,我们就可以知道主要的卡顿原因了。
卡顿原因
看到这盆友们应该知道为什么卡顿了,如果还不知道,那罚你再重新看一遍咯。其实真可以去看一下那篇文章,那篇文章很好的阐述了这个表格为什么会这么复杂。
❝卡顿原因:
❞
大量数据需要实时更新 三个表格滚动事件让工作代码量变成了三倍
优化效果
不行,得先学资本家给大家画个饼,不然搞得我好像在诈骗一样,可以看下面这两张动态图,我只能说吃了二十盒德芙也没有这么「丝滑」。虽然滚轮滚动速度是有差别,可能会造成误差,但是这两区别也太大,丝滑了不止一点点,肉眼都可以看的出来。
❝优化前
❞
❝优化后
❞
在看数据「实时更新」的前后对比动图,优化前的动图可以看到,cpu占有率基本都是100%,偶尔会跳去99%。但是看优化后的图,虽然也会有飙到100的cpu占有率,但是只是某一个瞬间。这肯定就高下立判了,吾与城北徐公孰美,肯定是吾美啊!
❝优化前
❞
❝优化后
❞
优化思路与方法
如何呢,少侠?是不是还不错!
前面已经说过了两个原因导致卡顿,我们只要解决这两个原因自然就会好起来了,也不是解决,只能说是优化它,因为在网络,数据大量更新,以及用户频繁操作等等其他原因,还是会特别卡。
如何优化三个表格的滚动事件
对于这三个表格,核心是「一次滚动事件会触发三次滚动函数」,而且三个事件函数其实都是大差不差的,都是去改变其余两个表格的上下滚动高度或者左右滚动宽度,换句话说,这个滚动事件的主要目的其实就是「获取当前这个表格滚动了多少」。那我们偷换一下概念,原本的是「滚动事件」去改变其他两个表格的滚动高度,不如把他变成「滚动了多少」去改变其他两个表格的滚动高度。懵了吧,少年哈哈哈哈!看下修改后的代码你就能细评这句话了,代码是vue3写法,而且并不全,大家知道我在干嘛就行。
「修改前的js代码」
const leftO = document.querySelector("#left")
const middleO = document.querySelector("#middle")
const rightO = document.querySelector("#right")
leftO.addEventListener("scroll", (e) => {
const top = e.target.scrollTop
const left = e.target.scrollLeft
middleO.scrollTop = e.target.scrollTop
rightO.scrollTop = e.target.scrollTop
rightO.scrollLeft = left
},true)
middleO.addEventListener("scroll", (e) => {
const top = e.target.scrollTop
leftO.scrollTop = e.target.scrollTop
rightO.scrollTop = e.target.scrollTop
},true)
rightO.addEventListener("scroll", (e) => {
const left = e.target.scrollLeft
const top = e.target.scrollTop
leftO.scrollTop = e.target.scrollTop
middleO.scrollTop = e.target.scrollTop
leftO.scrollLeft = left
},true)
「修改后的js代码」
const leftO = document.querySelector("#left")
const middleO = document.querySelector("#middle")
const rightO = document.querySelector("#right")
const top = ref(0)
const left = ref(0)
// 这个是判断哪个表格进行滚动了
const flag = ref("")
leftO.addEventListener("scroll", (e) => {
// 记录top和left
top.value = e.target.scrollTop
left.value = e.target.scrollLeft
flag.value = 'left'
}, true)
middleO.addEventListener("scroll", (e) => {
// 记录top
top.value = e.target.scrollTop
flag.value = 'middle'
}, true)
rightO.addEventListener("scroll", (e) => {
// 记录top和left
top.value = e.target.scrollTop
left.value = e.target.scrollLeft
flag.value = 'right'
}, true)
// 监听top去进行滚动
watch(() => top.value, (newV) => {
// 当前滚动就不进行设置滚动条了
flag.value!=="left" && (leftO.scrollTop = newV)
flag.value!=="middle" && (middleO.scrollTop = newV)
flag.value!=="right" && (rightO.scrollTop = newV)
})
// 监听left去进行滚动
watch(() => left.value, (newV) => {
// 当前滚动就不进行设置滚动条了
flag.value!=="left" && (leftO.scrollleft = newV)
flag.value!=="right" && (rightO.scrollleft= newV)
})
看完了吧,我简单的「总结」下我都干了啥,其实就是将「三个滚动事件所造成的影响全部作用于变量」,再通过watch
去「监听」变量是否变化再去作用于表格,而「不是直接」作用于表格。换句来说,从之前的监听「三个滚动事件」去滚动表格变成监听「一个滚动高度变量」去滚动表格,自然代码工作量从原来的三倍变回了原来的一倍。其实和「发布订阅」是有异曲同工之妙,三个发布者通知一个订阅者
。如此简单的一个事,为啥我要啰里吧嗦逼逼这么多,其实就是想让大家体会待入一下那种「恍然大悟妙不可言的高潮感」,而「不是坐享其成的麻痹感」。
如何优化大量数据实时更新
前面说过这是一个金融项目的页面,所以他是需要「实时更新」的。但是这个表格大概有四百来条数据,一条数据有二十一列,也就是可能会有「八千多个数据需要更新」。这肯定导致页面很卡,甚至是页面崩溃。那咋办呢,俗话说的好啊,只要「思想不滑坡,办法总比困难多」!
我们不妨想一想,「四百来条数据都要实时更新吗」?对,这并不需要!我们只要实现了类似于「图片懒加载」的效果,啥意思呢?就是比如当前我们屏幕只能看到二十条数据,我们只要实时更新的当前这二十条就行了,在「滚动」的时候屏幕又显示了另外二十条,我们在实时更新这二十条数据。不就洒洒水的优化了好几倍的性能吗。
❝我先和大家先说一下,我这边实现这个实时更新是通过「websocket」去实现的,前端将需要实时更新的「数据id代码」,发送给服务端,服务端就会一直推送相关的更新数据。然后我接下来就用「subscribe」代表去给通知服务端需要更新哪些数据id,「unsubscribe」代表去通知服务的不用继续更新数据,来给大家讲一下整体一个思路。
❞
首先,我们需要去维护好一个数组,什么数组呢。就是「在可视窗口的所有数据的id数组」,有了这个数组我们就可以写出下面的一个逻辑,只要是在可视窗口的数据id数组发生了变化,就把之前的数据推送取消,在重新开启当前这二十条的数据推送
。
// idArr为当前在可视窗口数据id数组
function updateSubscribe(idArr){
// 取消之前二十条的数据推送
unsubscribe()
// 开启当前这二十条的数据推送
subscribe(idArr)
}
所以,现在问题就变成「如何维护好这个数组」了!这个是在用户滚动
的时候会发生变化,所以我们还是要监听滚动事件,虽然我们之前已经做了上面的表格滚动优化操作,我这边还是给大家用滚动事件去演示demo。言归正传,我们要获取到这个数组,就要知道有「哪些数据的dom」是在可视窗口中的!这里我的方法还是比较笨的,我感觉应该是有更好的方法去获取的。大家可以复制下面这个demo跑一下,打开控制台看一下打印的数组。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
* {
padding: 0;
margin: 0;
}
.box {
width: 400px;
height: 600px;
margin: 0 auto;
margin-top: 150px;
border: 1px solid red;
overflow-y: scroll;
overflow-x: hidden;
}
.item {
width: 400px;
height: 100px;
/* background-color: beige; */
border: 1px solid rgb(42, 165, 42);
text-align: center;
}
</style>
</head>
<body>
<div class="box" id="box">
<div class="item" id="1">
1
</div>
<div class="item" id="2">
2
</div>
<div class="item" id="3">
3
</div>
<div class="item" id="4">
4
</div>
<div class="item" id="5">
5
</div>
<div class="item" id="6">
6
</div>
<div class="item" id="7">
7
</div>
<div class="item" id="8">
8
</div>
<div class="item" id="9">
9
</div>
<div class="item" id="10">
10
</div>
<div class="item" id="11">
11
</div>
<div class="item" id="12">
12
</div>
<div class="item" id="13">
13
</div>
<div class="item" id="14">
14
</div>
<div class="item" id="15">
15
</div>
<div class="item" id="16">
16
</div>
<div class="item" id="17">
17
</div>
</div>
</body>
<script>
const oBOX = document.querySelector("#box")
oBOX.addEventListener('scroll', () => {
console.log(findIDArr())
})
const findIDArr = () => {
const domList = document.querySelectorAll(".item")
// 过滤在视口的dom
const visibleDom = Array.prototype.filter.call(domList, dom => isVisible(dom))
const idArr = Array.prototype.map.call(visibleDom, (dom) => dom.id)
return idArr
}
// 是否在可视区域内
const isVisible = element => {
const bounding = element.getBoundingClientRect()
// 判断元素是否在可见视口中
const isVisible =
bounding.top >= 0 && bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight)
return isVisible
}
</script>
</html>
这段代码其实还是很好理解的,我就给大家提两个地方比较难搞的地方。
id的获取方式
我们这里是先在每个div手动的绑定了id,然后在通过是拿到dom的实例对象,进而去获取到它的id。而在我们实际的开发工作中,基本都是使用组件的,然后是数据驱动视图的。就比如el-table,给他绑定好一个数据列表,就可以渲染出一个列表。也就是说,「这一行的dom和这一行绑定的数据是两个东西」,我们「所获取的dom不一定就能拿到id」,所以怎么获取到每一行的id也是一个问题,反正核心就是将「dom和数据id」联系起来,这就需要大家具体问题具体分析解决了。
如何判断是否在可视区域
判断是否在可视区域主要是通过getBoundingClientRect
函数,这个函数是可以获取一个元素的六个属性,分别是上面(下面)的这几个属性,然后就可以根据这些字段去判断是否在可视区域。
❝❞
width: 元素的宽度 height: 元素的高度 x: 元素左上角相对于视口的横坐标 y: 元素左上角相对于视口的纵坐标 top: 元素顶部距离窗口的距离 left: 元素左侧距离窗口左侧的距离 bottom: 元素底部距离窗口顶部的距离 (等于 y + height) right: 元素右侧距离窗口右侧的距离(等于 x + width)
进一步优化
除了上面这些,我还做一个优化,啥优化呢?就是在vue中因为是响应式驱动,只要数据一发生变化就会触发视图更新,但是如果变化的太频繁,也会特别卡,所以我就添加了一个「节流」,让他一秒更新一次,但是这个优化其实是有一丢丢问题的。为什么呢,比如以一秒为一个时间跨度,他本来是在0.5秒更新的,但是我现在把他变成了在1秒更新,在「某种意义上他就并不实时」了。但是做了这个操作,性能肯定是比之前好得多,这就涉及到一个平衡了,毕竟「鱼和熊掌不可兼得」嘛。因为保密协议巴拉巴拉的,我就给大家写了个伪代码。
// 表格绑定的值
const tableData = ref([])
// 表格原始值
const tableRow = toRaw(tableData.value)
// 定时器变量
let timer
// 更新函数
const updateData = (resData) => {
// resData是websocket服务端推送的一个数据更新的数组,我们假设resData这个数据结构是下面这样
// [{
// id: "",
// data: {}
// },
// {
// id: "",
// data: {}
// }]
resData.forEach(item => {
// 更新的id
const Id = item.id
// 先去找tableRow原始值中找到对应的数据
const dataItem = tableRow.findIndex(row => row.id == Id)
// 更新tableRow原始值数据
dataItem[data] = item.data
})
if(!timer){
timer = setTimeout(()=>{
// 这个时候才去更新tableData再去更新视图
tableData.value = [...tableRow]
timer = null
},1000)
}
}
我大概的讲一下这段代码在干嘛。假设我这个表格绑定的值是tableData
,我用vue3的toRaw
方法,将这个拷贝一份形成一个没有响应式的值为tableRow
。这里提一嘴,toRaw
这个方法并不是深拷贝,他只是「丧失了响应式」了,「改变tableRow
的值,tableData
也会发生变化但是不会更新视图」。updateData
大家可以看成封装好的更新方法。传入的参数为服务端推送的数据,它是一个全是对象的数组。这段代码的核心就是「服务端推送的数据先去更新tableRow的值,再利用节流实现一秒更新一次tableData的值」。
toRaW
这里再给大家分享一个知识,大家可以看到我去更新的tableData
的值的时候是新创建了一个数组,然后用...扩展运算符
去浅拷贝。这是因为如果直接用toRaw后的对象去赋值给响应式的的对象,这个对象也会丧失响应式。但是如果只是某一个属性单独赋值是不会丧失响应式的
「单独属性赋值」
import { reactive, toRaw } from 'vue';
const state = reactive({ count: 0 });
const rawState = toRaw(state);
// 将原始对象的属性值赋给响应式对象的属性
state.count = rawState.count;
const increment = () => {
state.count++;
};
increment();
console.log(state.count); // 响应式更新,输出1
「整个对象赋值」
import { reactive, toRaw } from 'vue';
const state = reactive({ count: 0 });
const rawState = toRaw(state);
// 错误地用原始对象替换响应式对象
state = rawState;
// 这会导致错误,因为不能重新赋值响应式对象本身,并且响应式关联被破坏
「并不是深拷贝」
import { reactive, toRaw } from 'vue';
const nestedObj = reactive({
a: 1,
b: {
c: 2
}
});
const rawObj = toRaw(nestedObj);
// 修改原始对象的属性
rawObj.a = 10;
console.log(nestedObj.a); // 输出10,说明不是深拷贝,因为修改原始对象影响了响应式对象
总结
其实整体来看,并没有做一些高大上的操作,但是性能确实好了很多很多。去年面试的时候被问到性能优化总是会很慌张,因为我一直觉得的性能优化特别牛逼,我也确实没有做过什么性能优化的操作,只能背一些八股文,什么防抖节流,图片懒加载,虚拟列表......然后我想表达啥呢,因为我觉得肯定很多人面试的时候很怕被问到性能优化,特别是现在在准备秋招春招啥的,因为我也刚毕业三四个月,我包有体会的。所以我想告诉大家的意思的,性能优化并没有这么高端,只要是能让你的项目变好的,都是性能优化。实在不行,你就好好看哥们的写的东西,你就说这个表格是你写,反正面试不就是糊弄面试官的吗,自信!
点击关注公众号,“技术干货” 及时达!