web-worker 优化惨案纪实

前端Q

共 4835字,需浏览 10分钟

 · 2020-12-06


欢迎大佬们点开阅读原文,点个 star~

开篇记

这是我的第一篇文章,也是我工作一年后的新征程。作者是 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 > 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 字数键入后执行的情况这是当输入内容比较多的执行情况,因为再多就卡死了,可以看到整个 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 {
  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 都吃不消了,反而成为了最耗时的事件,而我们的处理过程并未造成卡顿。也就是说理论上当内容足够多,浏览器都吃不消时,我们的事件处理也不会造成卡顿,已经能够满足我们的需求了。

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

优化前

优化后

结尾

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

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



最后


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

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

点个在看支持我吧
浏览 16
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报