今天為大家分享一篇關于web worker的優質文章,讓你了解一下如何通過Web Worker來解決前端處理大量數據運算時頁面假死的問題。
以下是正文:
如何讓前端擁有后端的計算能力,在算力緊缺的年代,擴展前端的業務邊界!
前言 頁面中有十萬條數據,對其進行復雜運算,需要多久呢?
表格4000行,25列,共十萬條數據
運算包括 :總和、算術平均、加權平均、最大、最小、計數、樣本標準差、樣本方差、中位數、總體標準差、總體方差
table.jpg
答案是: 35s 左右
注:具體時間根據電腦配置會有所不同
并且 這個時間段內,頁面一直處于假死狀態,對頁面做任何操作都沒有反應??????
boom.gif
什么是假死?
瀏覽器有GUI渲染線程與JS引擎線程,這兩個線程是互斥的關系
當js有大量計算時,會造成 UI 阻塞,出現界面卡頓、掉幀等情況,嚴重時會出現頁面卡死的情況,俗稱假死
致命bug 強行送測吧
測試小姐姐:你的頁面又死了!! 我:還沒有死,在ICU…… ,過一會就好了 測試小姐姐:已經等了好一會了,還不行啊,是個致命bug ?? 我:……
絕望.jpg
闖蕩前端數十載,竟被提了個致命bug,顏面何在!??
Performance分析假死期間的性能表現 如下圖所示:此次計算總用時為35.45s
重點從以下三個方面分析:
1)FPS
FPS: 表示每秒傳輸幀數,是分析動畫的一個主要性能指標,綠色的長度越長,用戶體驗越好;反之紅色越長,說明卡頓嚴重
從圖中看到FPS中有一條持續了35s的紅線,說明這期間卡頓嚴重
2)火焰圖Main Main: 表示主線程運行狀況,包括js的計算與執行、css樣式計算、Layout布局等等。
展開Main,紅色倒三角的為Long Task ,執行時長50ms就屬于長任務,會阻塞頁面渲染
從圖中看到計算過程的Long Task執行時間為35.45s, 是造成頁面假死的原因
3)Summary 統計匯總面板 Summary: 表示各指標時間占用統計報表
Scripting代碼執行為35.9s
performance8.png
拿什么拯救你,我的頁面
召喚Web Worker,出來吧神龍 R-C (1).gif
神龍,我想讓頁面的計算變快,并且不卡頓
Web Worker了解一下:
在HTML5的新規范中,實現了 Web Worker 來引入 js 的 “多線程” 技術, 可以讓我們在頁面主運行的 js 線程中,加載運行另外單獨的一個或者多個 js 線程
一句話:Web Worker專門處理復雜計算的,從此讓前端擁有后端的計算能力
在Vue中 使用 Web Worker 1、安裝worker-loader
npm install worker-loader
2、編寫worker.js
onmessage = function (e) { // onmessage獲取傳入的初始值 let sum = e.data; for (let i = 0; i < 200000; i++) { for (let i = 0; i < 10000; i++) { sum += Math.random() } } // 將計算的結果傳遞出去 postMessage(sum); }
3、通過行內loader 引入 worker.js
import Worker from "worker-loader!./worker"
4、最終代碼
<template> <div> <button @click="makeWorker" >開始線程</button> <!--在計算時 往input輸入值時 沒有發生卡頓--> <p><input type ="text" ></p> </div> </template> <script> import Worker from "worker-loader!./worker" ; export default { methods: { makeWorker () { // 獲取計算開始的時間 let start = performance.now(); // 新建一個線程 let worker = new Worker(); // 線程之間通過postMessage進行通信 worker.postMessage(0); // 監聽message事件 worker.addEventListener("message" , (e) => { // 關閉線程 worker.terminate(); // 獲取計算結束的時間 let end = performance.now(); // 得到總的計算時間 let durationTime = end - start; console.log('計算結果:' , e.data); console.log(`代碼執行了 ${durationTime} 毫秒`); }); } }, } </script>
計算過程中,在input框輸入值,頁面一直未發生卡頓
total.png
對比試驗 如果直接把下面這段代碼直接丟到主線程中,計算過程中頁面一直處于假死狀態,input框無法輸入
let sum = 0;for (let i = 0; i < 200000; i++) { for (let i = 0; i < 10000; i++) { sum += Math.random() } }
前戲差不多了,上硬菜
開啟多線程,并行計算
回到要解決的問題,執行多種運算時,給每種運算開啟單獨的線程,線程計算完成后要及時關閉
多線程代碼
<template> <div> <button @click="makeWorker" >開始線程</button> <!--在計算時 往input輸入值時 沒有發生卡頓--> <p><input type ="text" ></p> </div> </template> <script> import Worker from "worker-loader!./worker" ; export default { data () { // 模擬數據 let arr = new Array(100000).fill(1).map(() => Math.random()* 10000); let weightedList = new Array(100000).fill(1).map(() => Math.random()* 10000); let calcList = [ {type : 'sum' , name: '總和' }, {type : 'average' , name: '算術平均' }, {type : 'weightedAverage' , name: '加權平均' }, {type : 'max' , name: '最大' }, {type : 'middleNum' , name: '中位數' }, {type : 'min' , name: '最小' }, {type : 'variance' , name: '樣本方差' }, {type : 'popVariance' , name: '總體方差' }, {type : 'stdDeviation' , name: '樣本標準差' }, {type : 'popStandardDeviation' , name: '總體標準差' } ] return { workerList: [], // 用來存儲所有的線程 calcList, // 計算類型 arr, // 數據 weightedList // 加權因子 } }, methods: { makeWorker () { this.calcList.forEach(item => { let workerName = `worker${this.workerList.length} `; let worker = new Worker(); let start = performance.now(); worker.postMessage({arr: this.arr, type : item.type, weightedList: this.weightedList}); worker.addEventListener("message" , (e) => { worker.terminate(); let tastName = '' ; this.calcList.forEach(item => { if (item.type === e.data.type) { item.value = e.data.value; tastName = item.name; } }) let end = performance.now(); let duration = end - start; console.log(`當前任務: ${tastName} , 計算用時: ${duration} 毫秒`); }); this.workerList.push({ [workerName]: worker }); }) }, clearWorker () { if (this.workerList.length > 0) { this.workerList.forEach((item, key) => { item[`worker${key} `].terminate && item[`worker${key} `].terminate(); // 終止所有線程 }); } } }, // 頁面關閉,如果還沒有計算完成,要銷毀對應線程 beforeDestroy () { this.clearWorker(); }, } </script>
worker.js
import { create, all } from 'mathjs' const config = { number: 'BigNumber' , precision: 20 // 精度 } const math = create(all, config); //加 const numberAdd = (arg1,arg2) => { return math.number(math.add(math.bignumber(arg1), math.bignumber(arg2))); } //減 const numberSub = (arg1,arg2) => { return math.number(math.subtract(math.bignumber(arg1), math.bignumber(arg2))); } //乘 const numberMultiply = (arg1, arg2) => { return math.number(math.multiply(math.bignumber(arg1), math.bignumber(arg2))); } //除 const numberDivide = (arg1, arg2) => { return math.number(math.divide(math.bignumber(arg1), math.bignumber(arg2))); } // 數組總體標準差公式 const popVariance = (arr) => { return Math.sqrt(popStandardDeviation(arr)) } // 數組總體方差公式 const popStandardDeviation = (arr) => { let s, ave, sum = 0, sums= 0, len = arr.length; for (let i = 0; i < len; i++) { sum = numberAdd(Number(arr[i]), sum); } ave = numberDivide(sum, len); for (let i = 0; i < len; i++) { sums = numberAdd(sums, numberMultiply(numberSub(Number(arr[i]), ave), numberSub(Number(arr[i]), ave))) } s = numberDivide(sums,len) return s; } // 數組加權公式 const weightedAverage = (arr1, arr2) => { // arr1: 計算列,arr2: 選擇的權重列 let s, sum = 0, // 分子的值 sums= 0, // 分母的值 len = arr1.length; for (let i = 0; i < len; i++) { sum = numberAdd(numberMultiply(Number(arr1[i]), Number(arr2[i])), sum); sums = numberAdd(Number(arr2[i]), sums); } s = numberDivide(sum,sums) return s; } // 數組樣本方差公式 const variance = (arr) => { let s, ave, sum = 0, sums= 0, len = arr.length; for (let i = 0; i < len; i++) { sum = numberAdd(Number(arr[i]), sum); } ave = numberDivide(sum, len); for (let i = 0; i < len; i++) { sums = numberAdd(sums, numberMultiply(numberSub(Number(arr[i]), ave), numberSub(Number(arr[i]), ave))) } s = numberDivide(sums,(len-1)) return s; } // 數組中位數 const middleNum = (arr) => { arr.sort((a,b) => a - b) if (arr.length%2 === 0){ //判斷數字個數是奇數還是偶數 return numberDivide(numberAdd(arr[arr.length/2-1], arr[arr.length/2]),2);//偶數個取中間兩個數的平均數 }else { return arr[(arr.length+1)/2-1];//奇數個取最中間那個數 } } // 數組求和 const sum = (arr) => { let sum = 0, len = arr.length; for (let i = 0; i < len; i++) { sum = numberAdd(Number(arr[i]), sum); } return sum; } // 數組平均值 const average = (arr) => { return numberDivide(sum(arr), arr.length) } // 數組最大值 const max = (arr) => { let max = arr[0] for (let i = 0; i < arr.length; i++) { if (max < arr[i]) { max = arr[i] } } return max } // 數組最小值 const min = (arr) => { let min = arr[0] for (let i = 0; i < arr.length; i++) { if (min > arr[i]) { min = arr[i] } } return min } // 數組有效數據長度 const count = (arr) => { let remove = ['' , ' ' , null , undefined, '-' ]; // 排除無效的數據 return arr.filter(item => !remove.includes(item)).length } // 數組樣本標準差公式 const stdDeviation = (arr) => { return Math.sqrt(variance(arr)) } // 數字三位加逗號,保留兩位小數 const formatNumber = (num, pointNum = 2) => { if ((!num && num !== 0) || num == '-' ) return '--' let arr = (typeof num == 'string' ? parseFloat(num) : num).toFixed(pointNum).split('.' ) let intNum = arr[0].replace(/\d{1,3}(?=(\d{3})+(.\d*)?$)/g,'$&,' ) return arr[1] === undefined ? intNum : `${intNum} .${arr[1]} ` } onmessage = function (e) { let {arr, type , weightedList} = e.data let value = '' ; switch (type ) { case 'sum' : value = formatNumber(sum(arr)); break case 'average' : value = formatNumber(average(arr)); break case 'weightedAverage' : value = formatNumber(weightedAverage(arr, weightedList)); break case 'max' : value = formatNumber(max(arr)); break case 'middleNum' : value = formatNumber(middleNum(arr)); break case 'min' : value = formatNumber(min(arr)); break case 'variance' : value = formatNumber(variance(arr)); break case 'popVariance' : value = formatNumber(popVariance(arr)); break case 'stdDeviation' : value = formatNumber(stdDeviation(arr)); break case 'popStandardDeviation' : value = formatNumber(popStandardDeviation(arr)); break } // 發送數據事件 postMessage({type , value}); }
35s變成6s 從原來的35s變成了最長6s,并且計算過程中全程無卡頓,YYDS
time1.png src=http___img.soogif.com_n7sySW0OULhVlH5j7OrXHpbqEiM9hDsr.gif&refer=http___img.soogif.gif
最終的效果
table.gif
十萬條太low了,百萬條數據玩一玩 // 修改上文的模擬數據let arr = new Array(1000000).fill(1).map(() => Math.random()* 10000);let weightedList = new Array(1000000).fill(1).map(() => Math.random()* 10000);
時間明顯上來了,最長要50多s了,沒事玩一玩,開心就好
time3.png
web worker 提高Canvas運行速度 web worker除了單純進行計算外,還可以結合離屏canvas 進行繪圖,提升繪圖的渲染性能和使用體驗
離屏canvas案例
<template> <div> <button @click="makeWorker" >開始繪圖</button> <canvas id="myCanvas" width="300" height="150" ></canvas> </div> </template> <script> import Worker from "worker-loader!./worker" ; export default { methods: { makeWorker () { let worker = new Worker(); let htmlCanvas = document.getElementById("myCanvas" ); // 使用canvas的transferControlToOffscreen函數獲取一個OffscreenCanvas對象 let offscreen = htmlCanvas.transferControlToOffscreen(); // 注意:第二個參數不能省略 worker.postMessage({canvas: offscreen}, [offscreen]); } } } </script>
worker.js
onmessage = function (e) { // 使用OffscreenCanvas(離屏Canvas) let canvas = e.data.canvas; // 獲取繪圖上下文 let ctx = canvas.getContext('2d' ); // 繪制一個圓弧 ctx.beginPath() // 開啟路徑 ctx.arc(150, 75, 50, 0, Math.PI*2); ctx.fillStyle="#1989fa" ;//設置填充顏色 ctx.fill();//開始填充 ctx.stroke(); }
效果:
cricle.gif
離屏canvas的優勢
1、對于復雜的canvas繪圖,可以避免阻塞主線程
2、由于這種解耦,OffscreenCanvas的渲染與DOM完全分離了開來,并且比普通Canvas速度提升了一些
Web Worker的限制 1、在 Worker 線程的運行環境中沒有 window 全局對象,也無法訪問 DOM 對象
2、Worker中只能獲取到部分瀏覽器提供的 API,如定時器
、navigator
、location
、XMLHttpRequest
等
3、由于可以獲取XMLHttpRequest
對象,可以在 Worker 線程中執行ajax
請求
4、每個線程運行在完全獨立的環境中,需要通過postMessage
、 message
事件機制來實現的線程之間的通信
計算時長 超過多長時間 適合用Web Worker 原則上,運算時間超過50ms會造成頁面卡頓,屬于Long task,這種情況就可以考慮使用Web Worker
但還要先考慮通信時長
的問題
假如一個運算執行時長為100ms, 但是通信時長為300ms, 用了Web Worker可能會更慢
face.jpg
通信時長 新建一個web worker時, 瀏覽器會加載對應的worker.js資源
下圖中的Time是這個 js 資源的加載時長
load.png
最終標準:
計算的運算時長 - 通信時長 > 50ms,推薦使用Web Worker
場景補充說明 遇到大數據,第一反應: 為什么不讓后端去計算呢?
這里比較特殊,表格4000行,25列 1)用戶可以對表格進行靈活操作,比如刪除任何行或列,選擇或剔除任意行 2)用戶可以靈活選擇運算的類型,計算一個或多個
即便是讓后端計算,需要把大量數據傳給后端,計算好再返回,這個時間也不短,還可能出現用戶頻繁操作,接口數據被覆蓋等情況
總結 Web Worker為前端帶來了后端的計算能力,擴大了前端的業務邊界
可以實現主線程與復雜計運算線程的分離,從而減輕了因大量計算而造成UI阻塞的情況
并且更大程度地利用了終端硬件的性能
本文轉自 https://juejin.cn/post/7137728629986820126
如有侵權,請聯系刪除。
該文章在 2025/1/6 10:58:03 編輯過