阅读 341

web-worker优化实践

开篇记

这是我的第一篇文章,也是我工作一年后的新征程。作者是2019年刚刚毕业的,出身贫寒(普通二本)。亲眼目睹校招神仙打架,不幸流落凡尘(我不配)。现在以外包的形式,在一家金融公司工作。

场景

前端项目为vue技术栈, 业务中遇到这样一个情景,有一个输入框,可以键入或者复制粘贴进一大段带有某种格式的文本,根据格式符号对文本进行分割处理(例如根据‘;’分割对象,根据‘,’分割属性),最终将他们处理成某种格式的对象集合,同时生成预览。 效果大概是这个样子 代码如下

// index.vue
 
import { sectionSplice, contentSplice } from '@/utils/handleInput';
...
onInput() {
      this.loading = true;
      const temp = sectionSplice(this.text);
      this.cardList = contentSplice(temp).data;
      this.loading = false;
    },
// @/utils/handleInput
export function sectionSplice(val) {
  const breakSymbol = '\n';
  let cards = val.split(breakSymbol);
  return cards.filter((item) => item != '');
}
export function contentSplice(dataArr, cardId) {
  const splitSymbol = ',';
  const length = dataArr.length;
  const result = {
    data: [],
    cardId,
  };
  let item = null;
  let time = new Date().getTime();
  function maxLength(text) {
    if (text && text.length > 1000) return text.substring(0, 1000);
    return text;
  }
  for (let i = 0; i < length; i++) {
    item = dataArr[i].split(splitSymbol);
    if (item != '') {
      result.data.push({
        title: maxLength(item[0]),
        desc: maxLength(item.slice(1).join(splitSymbol)),
        key: time + i,
        keydef: time + i + 'keydef',
      });
    }
  }
  return result;
}
复制代码

性能瓶颈

但随着输入内容的增多,以及操作的频繁,很快会遇到性能问题,导致页面卡死。 这是一段2082080字数键入后执行的情况 这是当输入内容比较多是的执行情况,因为再多就卡死了,可以看到整个input回调执行相当耗时,造成性能低下,同时频繁触发vue 更新让原本就就已经低效的性能雪上加霜。

如何优化

引入web-worker

既然input回调高耗时,阻塞后续事件的执行,那我们就引用web-worker开辟新的线程,来执行这部分耗时操作就好了。在这个过程中,因为web-worker的加载方式使得在webpack工程化的项目中造成了困难。我尝试使用worker-loader等方式,但是太多坑了。最终使用了vue-worker,之所以使用this.$worker.run()方法是因为这种方式执行完成后worker会自行销毁。这里附带上

// main.js
import VueWorker from 'vue-worker';
Vue.use(VueWorker);
// index.js
onInput() {
      this.loading = true;
      const option = [this.text];
      this.workerInput = this.$worker
        .run(sectionSplice, option)
        .then((res) => {
          this.handleCards(res);
        })
        .catch((e) => console.log(e));
    },
 handleCards(data) {
      this.workerCards = this.$worker
        .run(contentSplice, [data])
        .then((res) => {
          this.cardList = res.data;
          this.loading = false;
        })
        .catch((e) => console.log(e));
    },
复制代码

一个线程不够用

但是现实非常残酷的开辟1个新线程之后,这一套处理过程还是非常繁重,只不过阻塞的位置从页面渲染线程换到了新线程。 与是我想到了React Fiber的理念,我也去搞个分片吧。于是将原有的逻辑拆分成两步。

  1. 开辟1个线程,将整体文本分割成数组
  2. 将分割好的数组按50的长度分片,为每一个分片开辟线程执行,并将返回结果汇总

一切大功告成之后,又遇到了新的问题,由于分片过程异步,执行中不可终止(vue-worker没有终止功能),分片返回结果时,就可能是过时的内容了。

使用代理

想了一下我想起了代理模式 设计一个Cards类,有4个属性

  1. SL记录此次任务的分片个数
  2. count当前已经完成的分片个数
  3. CardId当前操作id
  4. list合并后的结果

每次更新操作时,实例一个cards,并传入自增的操作id。当分片任务完成时,调用addCards方法,比对分片id与当前cards实例的CardId如果相同,数组合并,count自增当所有分片全部完成,返回最终结果list。 这样我们解决了不同步的问题。

export default class Cards {
  constructor(id, length) {
    this.SL = length;
    this.count = 0;
    this.CardId = id;
  }
  list = [];
  addCards(sid, section) {
    if (this.CardId == sid) {
      this.count++;
      this.list = this.list.concat(section);
    }
    if (this.count == this.SL) {
      return this.list;
    } else {
      return [];
    }
  }
  empty() {
    this.list = [];
  }
  get() {
    return this.list;
  }
}
复制代码

web-worker这么好,可以无限开新线程么?

这个问题非常重要,但是我并不是科班出身,我百度了好久都没有找到相关说明的文章,只能试着说明了。
这就设计到计算机基础了,最早cpu只有一个核心,一个线程,同时只能同时完成一件事情,一心不可二用。但是随着技术的发展,现在的消费级cpu都有16核32线程了,可以理解为三头六臂,同时可以做很多事情。
但是并非有多少线程,就只能开多少线程。以今年热销的英特尔i5 10400为例,这颗cup是6核12线程,12线程指的是最大并行执行的线程数量。其实是可以开辟多余12的线程数,这时cpu就有一个类似js eventloop的调度机制,用于切换任务在空闲线程执行。在这个过程中要消耗物理资源的,如果线程过多,在线程间来回切换的损耗会非常巨大。因此线程开辟,不超过cpu线程数为宜。 并且为什使用了vue-worker就可以绕过那么多在vue环境下使用web worker的坑呢?于是我去看了一下vue-worker的源码。

// https://github.com/israelss/vue-worker/blob/master/index.js
import SimpleWebWorker from 'simple-web-worker'
export default {
  install: function(Vue, name) {
    name = name || '$worker'
    Object.defineProperty(Vue.prototype, name, { value: SimpleWebWorker })
  }
}
复制代码

这。。。。竟然只是把SimpleWebWorker注册成vue插件,好吧,看来vue-worker也大可不必了。于是我基于SimpleWebWorker写了一个worker的执行队列,通过 window.navigator.hardwareConcurrency获取cpu线程信息限制开放线程数不超过cpu线程数,如果获取不到就默认上线是4个线程,毕竟现在都2020年了,在老的机器也都是2核4线程以上的配置了。 但是这种线程的限制方式并不严谨,因为是还有很多其他应用程序在占用线程,但是相对合理不会多度开辟新线程.

import SimpleWebWorker from "simple-web-worker";
export default class WorkerQueue {
  constructor() {
    try {
      this.hardwareConcurrency = window.navigator.hardwareConcurrency;
    } catch (error) {
      console.log(
        "Set 4 Concurrency,because can`t get your hardwareConcurrency."
      );
      this.concurrency = 4;
    }
    this.concurrency = 4;
    this._worker = SimpleWebWorker;
    this.workerCont = 0;
    this.queue = [];
  }
  push(fn, callback, ...args) {
    this.queue.push({ fn, callback, args });
    this.run();
  }
  run() {
    while (this.queue.length && this.concurrency > this.workerCont) {
      this.workerCont++;
      const { fn, callback, args } = this.queue.shift();
      this._worker
        .run(fn, args)
        .then((res) => {
          callback(res);
          this.workerCont--;
          this.run();
        })
        .catch((e) => {
          throw e;
        });
    }
  }
}
复制代码

防抖

虽然引入了worker开辟线程,一定程度上减轻耗阻塞的问题,但是频繁触发Input回调,以及频繁的vue更新还是会影响性能,因此这里引入防抖控制回调执行的频率。给cup一点喘息的时间,让他能够一直跑起来。

if (this.timer) {
    clearTimeout(this.timer);
    this.timer = setTimeout(() => {
       clearTimeout(this.timer);
       this.timer = null;
     }, 2000);
    return
 }
复制代码

最终效果

极端情况这是一次性键入的1278531字数的内容,当一次性输入这么多内容时,即便是浏览器的textInput都吃不消了,反而成为了最耗时的事件,而我们的处理过程并未造成卡顿。也就是说理论上当内容足够多,浏览器都吃不消时,我们的事件处理也不会造成卡顿,已经能够满足我们的需求了。

正常大数据量情况,还是使用开头2082080字数文字键入后的执行情况,与优化前进行对比。

优化前

优化后

结尾

附上demo地址github.com/liubon/vue-…

第一次尝试写文章,不足之处请见谅,存在问题欢迎指正

文章分类
前端
文章标签