# 浅记需求开发中一次代码优化过程
# 需求简介
从两个不同维度指标对不同行业进行监测,通过eCharts
绘制出散点图,交互场景是改变两个维度的指标数量去实时绘制出相应的散点图,原来每个维度的指标数量是10,而本次需求将指标数量调整到30。由于旧代码未考虑性能问题,每当某维度的指标个数变化时,会经历三层for循环嵌套的数据处理,再用得到的数据渲染散点图,致使出现点击时,改变指标数量出现卡顿问题。
# 优化过程
# 一、缓存处理
使用伪代码进行说明,假如旧代码核心部分如下,
// 1、数据处理
for(let i = 0; i < 100; i++){
for(let j = 0; j < 100; j++){
for(let m = 0; m < 100; m++){
// 去重检查,数据结构转换等
}
}
}
// 2、处理后的数据通过eCharts绘制散点图
2
3
4
5
6
7
8
9
使用缓存,将for循环嵌套降为两层。有了缓存,再通过区分操作是新增还是删除,其中删除操作不必做遍历,新增操作借助点击时确定该维度指标,可以减少这一维度的遍历。
// 外层缓存上次数据
let temp = [];
// ...
// 数据处理部分的调整
if(判断是否是删除指标操作){
// 从缓存找到满足条件的项并将其剔除
} else {
// 这里可以减少一层是因为点击时可确定该维度的指标,无必要再做这一维度的遍历
for(let i = 0; i < 100; i++){
for(let j = 0; j < 100; j++){
// 去重检查,数据结构转换等
// 更新缓存
}
}
}
// ...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
但优化后的效果不明显,仍旧表现卡顿。
# 二、采用时间分片
此时想到react
解决卡顿问题的方案——时间分片。
对上述代码使用requestIdleCallback
进行包裹,保证浏览器优先处理交互事件,再在每一帧空闲时段处理散点图的绘制。
requestIdleCallback(()=>{
// 数据处理部分的耗时操作
})
2
3
调整后,交互不卡顿了,但是出现散点图延迟渲染问题,体验不好。
# 三、添加定时器,行吗
通过分析发现,当用户点击了一个指标,再点击另一个指标,会导致浏览器需要等待上次点击的数据处理任务完成,才能处理下次点击的任务。很明显,这不是我们想要的,为此,我们需要一个能取消上一任务,执行当前任务的处理机制。
第一想到的是使用定时器。伪代码如下,
if(timer){
// 停止上一任务
clearTimeout(timer)
timer = null
}
timer = setTimeout(()=>{
// 执行耗时任务
})
2
3
4
5
6
7
8
9
但是实践发现,清除定时器需要满足任务未执行,若任务已在执行中,是无法终止的。
下面是一段JS测试代码。
let timer;
// 模拟耗时任务
function execTask(){
let i = 1;
while(i <= 1000 * 1000 * 1000 * 100){
if(i % (1000 * 1000 * 1000) === 0){
console.log(i)
// 任务不会停止执行
clearTimeout(timer);
}
i++;
}
}
// 测试定时器清除特性
timer = setTimeout(()=>{
execTask()
}, 5000)
// 任务不会执行
clearTimeout(timer)
// 发现一:requestIdleCallback无法控制定时器任务
requestIdleCallback(()=>{
timer = setTimeout(()=>{
execTask()
})
})
// 发现二:定时器任务里使用requestIdleCallback,无法起到空闲时执行
timer = setTimeout(()=>{
requestIdleCallback(()=>{
execTask()
})
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 四、Worker
后面想到线程,也就是Worker。通过Worker创建后台线程,负责处理耗时任务,而且也支持任务中断,既满足交互时不卡顿,又避免延迟渲染问题。
// index.js
let worker
// ...
if(worker){
// 终止线程,亦即停止上一任务
worker.terminate()
worker = null
}
// 创建线程
worker = new Worker('worker.js', {credentials: 'same-origin'})
// 接收线程处理后的数据
worker.onmessage = function(e){
console.log(e.data)
// 绘制散点图
}
worker.postMessage({
// 传递必要参数
})
// ...
// worker.js
function execTask(...params){
// 耗时操作
}
self.onmessage = function(e){
console.log(e.data)
const result = execTask(/* 从e.data获取参数 */)
self.postMessage(result)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 小结
优化过程不是一蹴而就,优化后可能会出现新的问题,这时会发现原来的方案并不是最佳的解决方式。
想起在知乎中看到的一句话,任何的优化都绕不过时间换空间,空间换时间,真是万变不离其宗啊!
← 富文本编辑器支持小程序跳转 代码片段欣赏 →