點擊上方前端Q,關注公眾號
回復加群,加入前端Q技術交流群
頁面中有十萬條數據,對其進行複雜運算,需要多久呢?
表格4000行,25列,總十萬條數據
運算包括:總和、算術平均、加權平均、最大、最小、計數、樣本標準差、樣本方差、中位數、總體標準差、總體方差
答案是:35s 左右註:具體時間根據電腦配置會有所不同
並且這個時間段內,頁面一直處於假死狀態,對頁面做任何操作都沒有反應(原地爆炸)

boom0.png
什麼是假死?
瀏覽器有GUI渲染線程與JS引擎線程,這兩個線程是互斥的關係。當js有大量計算時,會造成 UI 阻塞,出現界面渲染卡頓、掉幀等情況,嚴重時會出現頁面卡死的情況,俗稱假死
致命bug
強行送測吧
測試小姐姐:你的頁面又死了!!我:還沒有死,在ICU…… ,過一會就好了測試小姐姐:已經等了好一會了,還不行啊,是個致命bug我:……

絕望.jpg
闖蕩前端數十載,竟被提了個致命bug,顏面何在!
Performance分析假死期間的性能表現
如下圖所示: 此次計算總用時為35.45s
重點從以下三個方面分析:
1、FPS:FPS: 表示每秒傳輸幀數,是分析動畫的一個主要性能指標,綠色的長度越長,用戶體驗越好;反之紅色越長,說明卡頓嚴重
從圖中看到FPS中有一條持續了35s的紅線,說明這期間卡頓嚴重
2、火焰圖MainMain: 表示主線程運行狀況,包括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-loadernpm 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.jsimport Worker from "worker-loader!./worker"
4、最終代碼
<template><div><button@click="makeWorker">開始線程</button><!--在計算時往input輸入值時沒有發生卡頓--><p><inputtype="text"></p></div></template><script>importWorkerfrom"worker-loader!./worker";exportdefault{methods:{makeWorker(){//獲取計算開始的時間letstart=performance.now();//新建一個線程letworker=newWorker();//線程之間通過postMessage進行通信worker.postMessage(0);//監聽message事件worker.addEventListener("message",(e)=>{//關閉線程worker.terminate();//獲取計算結束的時間letend=performance.now();//得到總的計算時間letdurationTime=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><inputtype="text"></p></div></template><script>importWorkerfrom"worker-loader!./worker";exportdefault{data(){//模擬數據letarr=newArray(100000).fill(1).map(()=>Math.random()*10000);letweightedList=newArray(100000).fill(1).map(()=>Math.random()*10000);letcalcList=[{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=>{letworkerName=`worker${this.workerList.length}`;letworker=newWorker();letstart=performance.now();worker.postMessage({arr:this.arr,type:item.type,weightedList:this.weightedList});worker.addEventListener("message",(e)=>{worker.terminate();lettastName='';this.calcList.forEach(item=>{if(item.type===e.data.type){item.value=e.data.value;tastName=item.name;}})letend=performance.now();letduration=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.pngsrc=http___img.soogif.com_n7sySW0OULhVlH5j7OrXHpbqEiM9hDsr.gif&refer=http___img.soogif.gif
最終的效果

table.gif十萬條太low了,百萬條數據玩一玩//修改上文的模擬數據letarr=newArray(1000000).fill(1).map(()=>Math.random()*10000);letweightedList=newArray(1000000).fill(1).map(()=>Math.random()*10000);複製代碼
時間明顯上來了,最長要50多s了,沒事玩一玩,開心就好

time3.pngweb worker 提高Canvas運行速度
web worker除了單純進行計算外還可以結合離屏canvas進行繪圖,提升繪圖的渲染性能和使用體驗
案例:
<template><div><button@click="makeWorker">開始繪圖</button><canvasid="myCanvas"width="300"height="150"></canvas></div></template><script>importWorkerfrom"worker-loader!./worker";exportdefault{methods:{makeWorker(){letworker=newWorker();lethtmlCanvas=document.getElementById("myCanvas");//使用canvas的transferControlToOffscreen函數獲取一個OffscreenCanvas對象letoffscreen=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阻塞的情況
並且更大程度地利用了終端硬件的性能

R-C (3).gif參考鏈接
JavaScript 中的多線程 \-- Web Worker[1]OffscreenCanvas-離屏canvas使用說明[2]

往期推薦



歡迎加我微信,拉你進技術群,長期交流學習...
歡迎關注「前端Q」,認真學前端,做個專業的技術人...

