# 浅记需求开发中一次代码优化过程

# 需求简介

从两个不同维度指标对不同行业进行监测,通过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绘制散点图
1
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++){
      // 去重检查,数据结构转换等
      // 更新缓存
    }
  }
}
// ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

但优化后的效果不明显,仍旧表现卡顿。

# 二、采用时间分片

此时想到react解决卡顿问题的方案——时间分片。

对上述代码使用requestIdleCallback进行包裹,保证浏览器优先处理交互事件,再在每一帧空闲时段处理散点图的绘制。

requestIdleCallback(()=>{
  // 数据处理部分的耗时操作
})
1
2
3

调整后,交互不卡顿了,但是出现散点图延迟渲染问题,体验不好。

# 三、添加定时器,行吗

通过分析发现,当用户点击了一个指标,再点击另一个指标,会导致浏览器需要等待上次点击的数据处理任务完成,才能处理下次点击的任务。很明显,这不是我们想要的,为此,我们需要一个能取消上一任务,执行当前任务的处理机制。

第一想到的是使用定时器。伪代码如下,

if(timer){
  // 停止上一任务
  clearTimeout(timer)
  timer = null
}

timer = setTimeout(()=>{
  // 执行耗时任务
})
1
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()
  })
})
1
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)
}
1
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

# 小结

优化过程不是一蹴而就,优化后可能会出现新的问题,这时会发现原来的方案并不是最佳的解决方式。

想起在知乎中看到的一句话,任何的优化都绕不过时间换空间,空间换时间,真是万变不离其宗啊!