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

相關文章
相關標籤/搜索