前言

先了解一个概念:页面假死
浏览器有GUI渲染线程JS引擎线程,这两个线程是互斥的关系

当js有大量计算时,会造成 UI 阻塞,出现界面卡顿、掉帧等情况,严重时会出现页面卡死的情况,俗称假死

在前端开发中,处理大数据往往会导致主线程阻塞,从而影响页面的响应性能。这种情况下,可以使用 Web Worker,它运行在浏览器主线程之外,用于处理耗时任务,从而保持用户界面的流畅性。

介绍

Web Worker 是一种运行在后台的 JavaScript 脚本,不会影响主线程的性能。Web Worker 不能直接操作 DOM,但可以通过消息传递(postMessage 和 onmessage)与主线程通信。

关键特性
独立线程:运行在独立线程中,不会阻塞主线程。
消息通信:通过消息事件(postMessage 和 onmessage)与主线程交互。

受限环境

1、在 Worker 线程的运行环境中没有 window 全局对象,也无法访问 DOM 对象
2、Worker中只能获取到部分浏览器提供的 API,如定时器、fetch、navigator、location、XMLHttpRequest等。
3、由于可以获取XMLHttpRequest 对象,可以在 Worker 线程中执行ajax请求
4、每个线程运行在完全独立的环境中,需要通过postMessage、 message事件机制来实现的线程之间的通信

适用场景

大数据处理:复杂的计算、排序、筛选等操作。
文件操作:大文件的分片处理、解析。
图像处理:复杂的图像过滤或编辑。
网络请求:异步批量请求处理。

使用

假如有一个大数据数组排序

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Web Worker Example</title>
</head>
<body>
  <h1>Web Worker 大数据排序</h1>
  <button id="sortButton">开始排序</button>
  <div id="result"></div>

  <script src="main.js"></script>
</body>
</html>

1、创建一个独立的 Worker 脚本文件

// worker.js
// 接收主线程的数据
self.onmessage = function (event) {
  const { data } = event;

  if (data.type === "sort") {
    const sortedArray = data.array.sort((a, b) => a - b);
    // 将结果发送回主线程
    self.postMessage({ type: "result", sortedArray });
  }
};

2、创建 Web Worker

// main.js

// 创建 Web Worker 实例
const worker = new Worker("worker.js");

document.getElementById("sortButton").addEventListener("click", () => {
  const largeArray = Array.from({ length: 1000000 }, () =>
    Math.floor(Math.random() * 100000)
  );

  console.log("开始排序...");
  const startTime = performance.now();

  // 向 Worker 发送数据
  worker.postMessage({ type: "sort", array: largeArray });

  // 接收 Worker 返回的结果
  worker.onmessage = function (event) {
    const { data } = event;

    if (data.type === "result") {
      const endTime = performance.now();
      console.log("排序完成:", data.sortedArray);
      document.getElementById(
        "result"
      ).innerText = `排序完成,用时:${(endTime - startTime).toFixed(2)} ms`;
    }
  };
});
示例说明
主线程(main.js):
创建了一个包含 1,000,000 个随机数的数组。
使用 worker.postMessage 将数据传递到 Worker。
接收 worker.onmessage 返回的排序结果。
Worker(worker.js):

接收到数据后进行排序。
排序完成后通过 self.postMessage 将结果返回主线程。
结果展示:

点击按钮后,页面不会因为排序操作卡顿,用户仍然可以正常操作页面。

在这里插入图片描述
注意:

直接通过浏览器打开 index.html 时,浏览器会因同源策略的限制,禁止加载文件协议(file://)下的 Web Worker 文件。
使用 npx 运行一个临时服务器:
打开文件所在终端:npx serve
serve 是一个轻量级静态文件服务器,会默认监听 http://localhost:3000。

在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"
<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框的作用是:我们可以在计算过程中,在input框输入值,页面一直未发生卡顿

对比试验
如果直接把下面这段代码直接丢到主线程中,计算过程中页面一直处于假死状态,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});
}

在这里插入图片描述

web worker 提高Canvas运行速度

web worker除了计算外,还可以结合离屏canvas进行绘图,提升绘图的渲染性能和使用体验

canvas案例

<template>
  <div>
    <canvas ref="canvas" :width="width" :height="height"></canvas>
  </div>
</template>

<script>
import CanvasWorker from '../workers/canvasWorker.js';

export default {
  name: 'CanvasWorker',
  data() {
    return {
      width: 800,
      height: 600,
      worker: null,
    };
  },
  mounted() {
    const canvas = this.$refs.canvas;

    if (canvas.transferControlToOffscreen) {
      const offscreen = canvas.transferControlToOffscreen();
      this.worker = new CanvasWorker();
      this.worker.postMessage(
        { canvas: offscreen, width: this.width, height: this.height },
        [offscreen] // 必须通过 MessagePort 传递
      );
    } else {
      console.error('OffscreenCanvas 不支持,使用普通 canvas 绘图。');
    }
  },
  beforeDestroy() {
    if (this.worker) {
      this.worker.terminate();
    }
  },
};
</script>

<style scoped>
canvas {
  border: 1px solid #ccc;
  display: block;
  margin: 0 auto;
}
</style>

worker.js

self.onmessage = function (e) {
  const { width, height } = e.data;
  const offscreenCanvas = e.data.canvas;
  const ctx = offscreenCanvas.getContext('2d');

  function draw() {
    ctx.clearRect(0, 0, width, height);
    for (let i = 0; i < 500; i++) {
      ctx.beginPath();
      ctx.arc(
        Math.random() * width,
        Math.random() * height,
        Math.random() * 20 + 5,
        0,
        2 * Math.PI
      );
      ctx.fillStyle = `rgba(${Math.random() * 255}, ${Math.random() * 255}, ${
        Math.random() * 255
      }, 0.8)`;
      ctx.fill();
    }
    requestAnimationFrame(draw);
  }

  draw();
};

离屏canvas的优势

1、对于复杂的canvas绘图,可以避免阻塞主线程
2、由于这种解耦,OffscreenCanvas的渲染与DOM完全分离了开来,并且比普通Canvas速度提升了一些

多长时间适合用Web Worker

原则上,运算时间超过50ms会造成页面卡顿,属于Long task,这种情况就可以考虑使用Web Worker,新建一个web worker时, 浏览器会加载对应的worker.js资源,要先考虑worker通信时长的问题,假如一个运算执行时长为100ms, 但是通信时长为300ms, 用了Web Worker可能会更慢
最终标准:
计算的运算时长 - 通信时长 > 50ms,推荐使用Web Worker

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐