JS中的多线程——Web Worker

众所周知,JavaScript 是单线程运行的(至于为什么是单线程可以看一下这篇文章——事件循环机制),当浏览器主线程被大量计算任务阻塞时,页面就会出现明显的卡顿现象。Web Worker 提供了在独立线程中运行 JavaScript 的能力,通过消息传递或共享内存(SharedArrayBuffer)与主线程协作。下面通过这篇文章向大家介绍webworker是什么以及如何使用,以及具体开发中的使用场景


一、概念与由来

1. 什么是 Web Worker?

Web Worker 是浏览器暴露的一个后台执行环境——它在独立线程中运行 JavaScript,上下文没有 window、无法直接访问 DOM。主线程与 Worker 通过 postMessage/onmessage 通信。默认通信使用结构化克隆(复制数据),也支持 Transferable(零拷贝的所有权转移)和 SharedArrayBuffer(共享内存 + Atomics 同步)。

2. 为什么要它?它能解决什么问题?

浏览器的主线程(通常也称为 UI 线程)是一个极其繁忙的“单线程序员”,它肩负着脚本执行、样式计算、布局(重排)、绘制(重绘)以及处理用户事件(如点击、滚动)等多重重任。任何长时间运行的 JavaScript 任务都会阻塞这个线程,导致页面无法及时更新和响应用户交互,从而造成令人不快的“卡顿”现象(如按钮点击无反应、动画掉帧)。

要理解问题的根源,我们可以从计算机任务的类型划分入手:

I/O 密集型(I/O-bound):指任务大部分时间在等待输入/输出操作完成,例如网络请求(AJAX/Fetch)或文件读取。这类任务通常通过异步回调(如 Promise, async/await)来处理,浏览器在等待期间可以腾出主线程做其他事情。

CPU 密集型(CPU-bound):指任务需要进行大量复杂的计算,持续占用中央处理器(CPU)资源直到计算完成。例如,大规模数据的排序或筛选、复杂的数学计算(如加密解密、图像/视频处理、物理模拟)、语法高亮或代码编译等。异步编程模型无法解决CPU密集型任务带来的阻塞问题,因为计算本身就在主线程上,必须算完才能继续。

Web Worker 的核心目标,正是为了攻克 CPU 密集型任务 带来的阻塞难题。它允许开发者创建一个独立于主线程的后台线程,将那些“重活累活”(CPU密集型任务)丢进去执行。两个线程并行不悖,主线程因此得以保持流畅,及时响应用户交互和更新UI,从而从根本上提升了前端应用的性能和用户体验。

3. 核心设计取舍(对工程意味着什么)

  • 线程隔离(安全):避免竞态条件和复杂的 DOM 线程安全问题,但代价是:Worker 不能直接操控 DOM。
  • 消息传递优先(简单):默认复制数据简单安全,但会产生拷贝开销;为此浏览器提供 Transferable/SharedArrayBuffer。
  • 创建成本(有限资源):线程不是免费的——创建、销毁、内存占用都要考虑,因此生产环境通常要复用(池化)Worker。

理解这些取舍能帮助你判断什么时候应该用 Worker、怎么设计任务拆分、以及如何在工程中平衡性能与复杂度。


二、基础使用

下面给出通过一个基础的示例来看webworker的基础用法。

1. 简单示例(文件式 Worker)

worker.js

// worker.js
self.onmessage = (e) => {const n = e.data;// 计算密集型:斐波那契(示例,真实项目请替换为合适算法)function fib(x){ return x <= 1 ? x : fib(x-1) + fib(x-2); }const r = fib(n);self.postMessage(r);
};

main.js

const w = new Worker('worker.js'); // 可加 { type: 'module' } 使用 ES module
w.onmessage = (e) => console.log('结果:', e.data);
w.postMessage(40);

对一些关键 API的解释

  • new Worker(url, options):创建 Worker。options.type = 'module' 支持 import/export
  • worker.postMessage(value, transferables?):发送消息。第二个参数可传 Transferable(如 ArrayBuffer)做零拷贝。
  • worker.onmessage / self.onmessage:接收消息。
  • worker.terminate() / self.close():销毁 Worker(释放线程与资源)。
  • SharedArrayBuffer + Atomics:共享内存与同步(复杂场景用),需满足安全头。

3. Transferable(零拷贝)示例

当你传输大数组或位图时,复制开销很昂贵。使用 Transferable 把所有权转移给 Worker:

const ab = new ArrayBuffer(1024*1024);
worker.postMessage(ab, [ab]); // 发送后 main 端 ab 变为 neutered(不可访问)

4. 简易 Promise 化调用(一次性任务)

function runTask(script, payload) {return new Promise((resolve, reject) => {const w = new Worker(script);w.onmessage = e => { resolve(e.data); w.terminate(); };w.onerror = e => { reject(e); w.terminate(); };w.postMessage(payload);});
}

三、进阶实战

Web Worker 的强大不言而喻,但在大型工程中,粗暴地创建和使用 Worker 反而会带来管理混乱和性能问题。本章节将深入探讨如何在工程中优雅、高效、安全地使用 Worker。

1. 何时使用 Worker(冷静判断)

  • 适合:任务明显耗时,且会影响帧率或用户交互(图像处理、大数据解析、音视频编/解码、加密/压缩、机器学习推理),这些操作都有一个前提,那就是不涉及DOM操作,因为Web Worker不可以操作DOM。
  • 不适合:非常短小且频繁的任务(Worker 创建/消息开销可能高于任务本身)、纯 DOM 操作(Worker 无法访问 DOM)。

简单来说,不与DOM打交道、计算量大、耗时长的任务,就是Worker的完美候选者。

2. Worker 池(生产级必备)

池化的必要性: 线程的创建和销毁会产生性能开销,同时考虑到设备CPU核心数有限的实际情况。线程池通过复用现有线程、控制并发数量以及支持超时/重试等策略来优化资源使用。

Worker池的实现原理: 维护一组预先初始化的空闲Worker实例。任务到达时,从池中分配可用Worker执行任务,任务完成后Worker自动回归线程池。这种机制有效避免了频繁实例化带来的资源消耗。

下面是一段代码示例,让我们来看看具体的操作方式,看他是如何实现Worker池的。

// WorkerPool.js
class WorkerPool {constructor(workerScript, poolSize = navigator.hardwareConcurrency || 4) {this.workerScript = workerScript;this.poolSize = poolSize;this.workers = []; // 空闲Worker队列this.queue = []; // 任务等待队列 { resolve, reject, message, transfer }// 初始化Worker池for (let i = 0; i < poolSize; i++) {this._createWorker();}}_createWorker() {const worker = new Worker(this.workerScript);worker.onmessage = (e) => {// 当前Worker完成任务,从队列头取一个任务给它const nextTask = this.queue.shift();if (nextTask) {const { message, transfer, resolve, reject } = nextTask;worker.postMessage(message, transfer);// 将resolve和reject重新挂载到worker对象上,用于下一次onmessageworker._resolve = resolve;worker._reject = reject;} else {// 没有任务了,将此Worker放入空闲队列this.workers.push(worker);}// 外部Promise的resolveworker._resolve(e.data);};worker.onerror = (e) => {if (worker._reject) {worker._reject(e);worker._resolve = null;worker._reject = null;}// Worker出错,可能需要销毁并创建一个新的替换this._replaceWorker(worker);};// 初始创建后是空闲的this.workers.push(worker);}postMessage(message, transfer = []) {// 如果有空闲Worker,直接使用if (this.workers.length > 0) {const worker = this.workers.pop();// 返回一个Promise,将resolve/reject方法暂存在worker对象上return new Promise((resolve, reject) => {worker._resolve = resolve;worker._reject = reject;worker.postMessage(message, transfer);});} else {// 没有空闲Worker,将任务加入队列等待return new Promise((resolve, reject) => {this.queue.push({ resolve, reject, message, transfer });});}}_replaceWorker(badWorker) {// 从池中移除坏的Workerconst index = this.workers.indexOf(badWorker);if (index !== -1) this.workers.splice(index, 1);badWorker.terminate();// 创建一个新的Worker补充池子this._createWorker();}terminateAll() {this.workers.forEach(worker => worker.terminate());this.workers = [];this.queue = [];}
}// 使用示例
const myWorkerPool = new WorkerPool('worker-script.js', 4);// 异步提交任务并等待结果
async function processData(data) {try {const result = await myWorkerPool.postMessage(data);console.log('Result from worker:', result);} catch (error) {console.error('Worker error:', error);}
}

简单总结一下,该 WorkerPool 通过维护固定数量的 Worker 实例,实现了任务的高效调度:空闲 Worker 直接处理任务,繁忙时任务排队等待,Worker 出错时自动替换,最终达到减少 Worker 创建开销、提高并发处理效率的目的。外部通过 Promise 接口即可异步获取任务结果,使用简单且不阻塞主线程。

建议从以下方面进行优化:超时控制、健康检查、优先级队列以及任务取消机制(可考虑采用 token 方案)。这些内容不在本文讨论范围内,就先不展开说明了,后面有机会再详细介绍。

3. 图像处理与Canvas(OffscreenCanvas)

传统痛点: 在Worker中处理图像数据,需要将 ImageData 来回传递,仍然可能阻塞主线程(虽然后台计算时UI不卡,但传输和最终绘制可能卡)。

现代解决方案: OffscreenCanvas。它允许你将一个Canvas的控制权完全转移给Worker,从计算到渲染全过程都在后台进行,主线程几乎零开销。

下面通过代码来看如何具体使用

在主线程中:

const offscreenCanvas = document.querySelector('canvas').transferControlToOffscreen();
worker.postMessage({ canvas: offscreenCanvas }, [offscreenCanvas]);
// 注意:第二个参数必须传输Transferable对象

在Worker中:

onmessage = function(e) {const canvas = e.data.canvas;const ctx = canvas.getContext('2d');// 现在你可以在Worker中直接进行绘图操作!ctx.beginPath();ctx.moveTo(10, 10);ctx.lineTo(100, 100);ctx.stroke();// 或者处理图像const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);// ... 对imageData进行复杂的像素操作 ...ctx.putImageData(imageData, 0, 0);
};

注意: 浏览器兼容性是主要考量,但现代浏览器已广泛支持。

4. SharedArrayBuffer 场景(并发协作,需注意安全)

SharedArrayBuffer 允许多线程共享内存并配合 Atomics 做同步,适合低延迟、多 Worker 协作(例如分块矩阵乘法、并行 FFT)。但必须满足安全要求(浏览器会在没有 COOP/COEP 的情况下禁用它)。

必需的 HTTP 响应头(服务器上设置):

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

示例(主/Worker 端简化)

// 主线程
const sab = new SharedArrayBuffer(4); // 4 bytes
const arr = new Int32Array(sab);
arr[0] = 0;
worker.postMessage({ sab });// Worker
self.onmessage = (e) => {const arr = new Int32Array(e.data.sab);// 等待通知Atomics.wait(arr, 0, 0);// 继续工作...
};

注意:使用 SharedArrayBuffer 时要小心死锁、复杂同步与性能调优(频繁 Atomics 操作也会带来开销)。

5. 错误处理、生命周期管理与降级策略

  • 错误上报worker.onerror + Worker 内捕获异常并 postMessage 反馈结构化错误,便于上报与诊断。
  • 超时与终止:对每个任务设置超时;超时后 terminate() 并重试或降级回主线程实现。
  • 销毁时机:组件卸载、页面隐藏、beforeunload 时清理 Worker;池实现中做“空闲销毁”(空闲超过某时长自动销毁部分 Worker)。
  • 降级处理:若环境不支持某特性(SharedArrayBuffer、OffscreenCanvas、module worker),提供主线程回退实现或分片处理以保证功能可用但性能降级可控。

6. 性能测量(判断是否值得起 Worker)

别凭感觉决定:量化!

  1. 在主线程测量原始任务耗时(performance.now())。
  2. 在 Worker 方案中测量序列化、传输、执行与回传时间(在主/Worker 端分别打点)。
  3. 在目标设备(PC/中低端手机)上比对用户可感知延迟(例如输入响应时间、动画掉帧)与资源占用(CPU/内存)。
    只有当总体体验有明显改善时,才长期采用。

7. 兼容性与部署注意

  • type: 'module' Worker 在现代浏览器支持良好,但旧浏览器可能不支持,需做降级。常见的浏览器对webworker的支持情况如下图所示:
    web worker支持情况

  • OffscreenCanvasImageBitmapSharedArrayBuffer 的支持度差异更大,使用前要 feature-detect 并提供回退。下面贴出来常见的浏览中这三者的支持情况供大家参考。
    OffscreenCanvas
    ImageBitmap
    ![SharedArrayBuffer![](https://i-blog.csdnimg.cn/direct/8a89fae121c442e592dc6154da60d98e.png)

  • 如果要在生产环境启用 SharedArrayBuffer,务必在服务器端配置 COOP/COEP,并测试第三方嵌入对策略的影响(某些第三方资源可能需要 crossorigin / require-corp 的处理)。


四、实战清单

  • 先测量:确认任务确实会卡住主线程。
  • 优化算法:先优化算法/批处理,再考虑 Worker。
  • 池化优先:为短任务或频繁任务实现 Worker 池,控制并发与复用。
  • 使用 Transferable:传二进制或 ImageBitmap 时优先 Transferable。
  • OffscreenCanvas:图像/Canvas 绘制在 Worker 内做(若浏览器支持)。
  • SharedArrayBuffer:只在确需低延迟协作且能配置 COOP/COEP 时使用。
  • 错误/超时/回退:实现报错上报、超时终止与主线程回退策略。
  • 在真实目标设备上做对比测试:PC、Android、iOS 都测一次。

结语

Web Worker 是移动/桌面 Web 中提升用户体验的重要武器,但它不是万金油:先优化、再并行、再复用。将任务合理拆分、用池化与 Transferable/OffscreenCanvas/SharedArrayBuffer 等技术配合,你就能把耗时任务交给“后台工人”,让主线程专注做界面,整体体验稳而顺。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/96460.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/96460.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【SQL注入】延时盲注

sleep(n)​​: 核心延时函数。使数据库程序暂停 n秒。​​if(condition, true_expr, false_expr)​​: 条件判断函数。如果 condition为真&#xff0c;执行 true_expr&#xff0c;否则执行 false_expr。​​用于将延时与判断条件绑定​​。​​mid(a, b, c)​​: 字符串截取函数…

IntelliJ IDEA 2025.1 Java Stream Debugger 快速使用指南

1. 功能概览 Java Stream Debugger 提供 Trace Current Stream Chain 功能&#xff0c;用来在调试时分析和可视化 Stream 操作链。 主要用途&#xff1a; 在运行时查看流操作链的每一步输出找出 map/filter 等操作的问题避免手动加 peek() 打印调试2. 使用入口 在 IDEA 2025.1 …

ARM-指令集全解析:从基础到高阶应用

一、ARM 指令集体系结构版本ARM 公司定义了多个指令集版本&#xff1a;ARMv1&#xff1a;原型机 ARM1&#xff0c;没有用于商业产品。ARMv2&#xff1a;扩展 V1&#xff0c;包含 32 位乘法指令和协处理器指令。ARMv3&#xff1a;第一个微处理器 ARM6 核心&#xff0c;支持 Cach…

第3讲 机器学习入门指南

近年来&#xff0c;随着企业和个人生成的数据量呈指数级增长&#xff0c;机器学习已成为日益重要的技术领域。从自动驾驶汽车到流媒体平台的个性化推荐&#xff0c;机器学习算法已广泛应用于各个场景。让我们深入解析机器学习的核心要义。3.1 机器学习定义机器学习是人工智能的…

深入理解跳表:多层索引加速查找的经典实现

跳表&#xff08;Skip List&#xff09;是一种多层有序链表结构&#xff0c;通过引入多级索引加速查找&#xff0c;其核心设计类似于“立体高速公路系统”&#xff0c;底层是原始链表&#xff0c;上面有各种高度的"高架桥"。 高层道路跨度大&#xff0c;连接远方节点…

Flutter 视频播放器——flick_video_player 介绍与使用

在移动端应用中&#xff0c;视频播放是一个常见的功能场景&#xff0c;例如短视频、直播、课程、广告展示等。 Flutter 本身并没有直接提供视频播放器组件&#xff0c;而是依赖第三方库来实现。 今天要介绍的库是 flick_video_player&#xff0c;它基于 video_player 封装&…

编写cmakelists文件常用语句

cmake_minimum_required (VERSION 3.10) 指定最小版本project(XXXX) 指定项目名字 ---------------set(MAIN_EXEC_NAME dwarf_parser) 定义变量${ MAIN_EXEC_NAME } 变量取值set(CMAKE_CXX_STANDARD 14) 指定c14标准&#xff0c;还有11、17、20等标准…

麒麟桌面系统找不到mbr启动,并重新安装grub

根据你提供的情况,“麒麟桌面系统找不到MBR启动”,这通常是由于GRUB引导损坏、MBR记录丢失或分区表异常导致的。你可以按照以下步骤重新安装GRUB并修复MBR启动: ✅ 步骤一:准备工具 使用银河麒麟LiveCD或U盘启动盘(可用Ventoy制作); 启动电脑,选择从U盘或光盘进入Live环…

【音频字幕】构建一个离线视频字幕生成系统:使用 WhisperX 和 Faster-Whisper 的 Python 实现

一、背景介绍 对于一端没有字幕外国视频、字幕&#xff0c;在不懂外语的情况下&#xff0c;怎么获取相关内容&#xff1f;作为技术宅&#xff0c;怎么自建搭建一个语音转文字的环境当前AI技术这么发达&#xff1f; 试试 二、系统设计 音频提取(仅仅是视频需要该逻辑、本身就是音…

Linux ALSA架构:PCM_OPEN流程 (二)

一 应用端源码路径: external\tinyalsa\pcm.c external\tinyalsa\pcm_hw.cstruct pcm *pcm_open(unsigned int card, unsigned int device,unsigned int flags, struct pcm_config *config) {...pcm->ops &hw_ops;pcm->fd pcm->ops->open(card, device,…

tp5的tbmember表闭包查询 openid=‘abc‘ 并且(wx_unionid=null或者wx_unionid=‘‘)

闭包查询 tbmember表闭包查询查询 openid‘abc并且islose0并且islogout0并且&#xff08;wx_unionidnull或者wx_unionid’&#xff09; Db::table(tbmember)->where([openid>abc,islose>0,islogout>0])->where(function ($query){$query->where(wx_unioni…

邪修实战系列(3)

1、第一阶段邪修实战总览&#xff08;9.1-9.30&#xff09; 把第一阶段&#xff08;基础夯实期&#xff09;的学习计划拆解成极具操作性的每日行动方案。这个计划充分利用我“在职学习”的特殊优势&#xff0c;强调“用输出倒逼输入”&#xff0c;确保每一分钟的学习都直接服务…

【GD32】ROM Bootloader、自定义Bootloader区别

Bootloader是应用程序跑起来之前&#xff0c;用于初始化的一段程序&#xff0c;它分为两种&#xff0c;ROM Bootloader、自定义Bootloader。GD32芯片出厂时预烧录在ROM中的Bootloader&#xff08;以下简称ROM Bootloader&#xff09;和自己编写的Bootloader&#xff08;以下简称…

Linux防火墙-Firewalld

一、 概述 按表现形式划分&#xff1a; 软件防火墙&#xff1a; 集成在系统内部&#xff0c;Linux系统&#xff1a; iptables、firewalld、ufw&#xff1b; windows系统下&#xff1a; windows defender 硬件防火墙&#xff1a; 华为防火墙、思科防火墙、奇安信防火墙、深信服防…

【Qt】PyQt、原生QT、PySide6三者的多方面比较

目录 引言 一、基本定义 二、核心对比维度 1. 编程语言与开发效率 2. 功能与 API 兼容性 3. 性能表现 4. 许可证与商业使用 5. 社区与文档支持 三、迁移与兼容性 四、适用场景推荐 五、总结对比表 总结 引言 PySide6、PyQt&#xff08;通常指 PyQt5/PyQt6&#xf…

JavaWeb站内信系统 - 技术设计文档

1. 系统概述1.1 项目背景本系统旨在为企业或社区平台提供一套完整的站内信解决方案&#xff0c;支持用户之间的消息发送、接收、管理等功能&#xff0c;提升用户间的沟通效率。1.2 设计目标实现用户间消息发送和接收支持一对一和一对多消息发送提供消息状态跟踪&#xff08;已读…

Java基础 9.10

1.System类常见方法和案例exit&#xff1a;退出当前程序arraycopy&#xff1a;复制数组元素&#xff0c;比较适合底层调用&#xff0c;一般使用 Arrays.copyOf 完成复制数组int[] src{1,2,3};int[] dest new int[3]; System.arraycopy(src, 0, dest, 0, 3);currentTimeMilens&…

详解flink性能优化

1. 简介 Apache Flink是一个强大的流处理框架&#xff0c;其性能很大程度上取决于内存的使用效率。在大规模数据处理场景中&#xff0c;合理的内存配置和优化可以显著提升Flink作业的性能和稳定性。本文将深入探讨Flink内存优化的各个方面&#xff0c;包括状态后端选择、内存配…

VueFlow的箭头怎么调整

正好最近用到了VueFlow组件&#xff0c;发现箭头默认样式太小&#xff0c;无法体现流程展示&#xff0c;因此翻阅相关资料得出下列方法&#xff0c;有什么更好的方法&#xff0c;大家可以推荐推荐&#xff0c;谢谢。方法1&#xff1a;通过边&#xff08;Edge&#xff09;的样式…

【Python】S1 基础篇 P9 文件处理与异常处理技术

目录文件读取操作读取文件的全部内容相对路径和绝对路径逐行访问文件内容文件写入操作写入单行内容写入多行内容结构化数据的存储异常处理机制理解异常的工作原理ZeroDivisionError异常示例try-except语句块的使用else语句块的正确使用静默失败的合理应用本文将深入探讨Python中…