web-worker 优化惨案纪实

开篇记

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

场景

前端项目为 vue 技术栈, 业务中遇到这样一个情景,有一个输入框,能够键入或者复制粘贴进一大段带有某种格式的文本,根据格式符号对文本进行分割处理(例如根据‘;’分割对象,根据‘,’分割属性),最终将他们处理成某种格式的对象集合,同时生成预览。效果大概是这个样子image.png代码以下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 > 1000return text.substring(01000);
    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 字数键入后执行的状况image.png这是当输入内容比较多的执行状况,由于再多就卡死了,能够看到整个 input 回调执行至关耗时,形成性能低下,同时频繁触发 vue 更新让本来就就已经低效的性能雪上加霜。webpack

如何优化

引入 web-worker

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

// 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 的理念,我也去搞个分片吧。因而将原有的逻辑拆分红两步。github

  1. 开辟 1 个线程,将总体文本分割成数组
  2. 将分割好的数组按 50 的长度分片,为每个分片开辟线程执行,并将返回结果汇总 一切大功告成以后,又遇到了新的问题,因为分片过程异步,执行中不可终止(vue-worker 没有终止功能),分片返回结果时,就多是过期的内容了。

使用代理

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

  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 {
  installfunction (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 都吃不消了,反而成为了最耗时的事件,而咱们的处理过程并未形成卡顿。也就是说理论上当内容足够多,浏览器都吃不消时,咱们的事件处理也不会形成卡顿,已经可以知足咱们的需求了。image.png异步

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

优化前

image.png

优化后

image.png

结尾

附上 demo 地址https://github.com/liubon/vue-worker-demo)

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



最后

  • 欢迎加我微信(winty230),拉你进技术群,长期交流学习...

  • 欢迎关注「前端Q」,认真学前端,作个专业的技术人...

image.png

相关文章
相关标签/搜索