这篇文章上次修改于 297 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

一文彻底了解Web Worker,十万条数据都是弟弟
海阔_天空 程序员成长指北
2025年01月14日 08:45 北京
点击上方 程序员成长指北,关注公众号

回复1,加入高级Node交流群

今天为大家分享一篇关于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: 表示各指标时间占用统计报表

Loading: 加载时间
Scripting: js计算时间
Rendering: 渲染时间
Painting: 绘制时间
Other: 其他时间
Idle: 浏览器闲置时间
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、最终代码

计算过程中,在input框输入值,页面一直未发生卡顿

图片
total.png
对比试验
如果直接把下面这段代码直接丢到主线程中,计算过程中页面一直处于假死状态,input框无法输入

let sum = 0;
for (let i = 0; i < 200000; i++) {

for (let i = 0; i < 10000; i++) {
  sum += Math.random()
}

}

前戏差不多了,上硬菜
图片
开启多线程,并行计算

回到要解决的问题,执行多种运算时,给每种运算开启单独的线程,线程计算完成后要及时关闭

多线程代码

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}