news 2026/9/9 22:39:08

页面卡顿怎么办?Web Workers 如何用 postMessage 把重计算移出主线程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
页面卡顿怎么办?Web Workers 如何用 postMessage 把重计算移出主线程

页面卡顿怎么办?Web Workers 如何用 postMessage 把重计算移出主线程

【免费下载链接】33-js-concepts📜 33 JavaScript concepts every developer should know.项目地址: https://gitcode.com/GitHub_Trending/33/33-js-concepts

当页面上的一段同步计算跑得时间过长时,整个页面会卡住:点不了按钮、滚不动鼠标、动画停住,看起来像挂了。33-js-concepts 仓库的 Web Workers 文档 针对的就是这个问题:把 CPU 密集型的重计算移到一个独立的后台线程(Web Worker)执行,通过postMessage/onmessage与主线程交换数据,让 UI 在计算期间保持流畅。这篇内容面向浏览器端 JS 开发者,前提是运行环境支持 Web Workers API——文档引用 Can I Use 数据称 Web Workers 在现代浏览器中支持率超过 98%;其中推荐的 module worker 语法则需要现代浏览器,文档建议投产前自行检查浏览器支持。

如果之前没读过事件循环相关内容,Event Loop 文档 和 async/await 文档 是理解“为什么异步解决不了卡顿”的前置知识。

先判断卡顿类型:async 解决不了 CPU 密集任务

文档给出的卡死示例是一段忙等 5 秒的同步循环(文档示例):

// This will freeze your entire page for ~5 seconds function heavyCalculation() { const start = Date.now() while (Date.now() - start < 5000) { // Simulating heavy work } return 'Done!' } document.getElementById('btn').addEventListener('click', () => { console.log('Starting...') const result = heavyCalculation() // Page freezes here console.log(result) }) // During those 5 seconds: // - Can't click anything // - Can't scroll // - Animations stop // - The page looks broken

如果你第一反应是“我加个 async 不就行了”,文档明确说这行不通:async JavaScript 仍然是单线程的,只是并发(concurrent)而非并行(parallel)。await只是让出控制权,JavaScript 的实际执行仍然一件事接一件事。下面这段代码里的async关键字对卡顿毫无帮助,因为循环本身是同步的 CPU 计算:

// This async function STILL freezes the page async function processLargeArray(data) { const results = [] // This loop is synchronous JavaScript // The "async" keyword doesn't help here! for (let i = 0; i < data.length; i++) { results.push(expensiveCalculation(data[i])) } return results } // The page freezes during the loop // async/await only helps with WAITING, not COMPUTING

文档给出的判断规则:等待(网络请求、文件读取、定时器)用 async/await;重计算(重计算、数据解析、图像处理)用 Web Workers。如果你的卡顿来自等待 I/O,用 Worker 反而多此一举。

主路径:用 postMessage 把重计算移入 Dedicated Worker

Dedicated Worker 是最常见的类型,只被一个脚本使用,也是文档中绝大多数场景的默认选择。下面用文档里的完整示例:主线程点击按钮后,把“求 max 以内所有素数”的重计算发给 Worker,结果回来再更新页面。

先写 Worker 文件prime-worker.js,它运行在独立线程里:

// prime-worker.js function isPrime(n) { if (n < 2) return false for (let i = 2; i <= Math.sqrt(n); i++) { if (n % i === 0) return false } return true } function findPrimes(max) { const primes = [] for (let i = 2; i <= max; i++) { if (isPrime(i)) primes.push(i) } return primes } self.onmessage = (event) => { const { findPrimesUpTo } = event.data const start = performance.now() const primes = findPrimes(findPrimesUpTo) const timeTaken = performance.now() - start self.postMessage({ primes, timeTaken }) }

注意self指 Worker 的全局作用域(DedicatedWorkerGlobalScope),顶层也可以用this,但文档建议用更清晰的self

再写主线程文件main.js。示例引用了页面上三个元素:id="max"的输入框、id="findPrimes"的按钮、id="status"的状态展示元素,照搬代码时需要保证页面里存在它们:

// main.js const worker = new Worker('prime-worker.js', { type: 'module' }) document.getElementById('findPrimes').addEventListener('click', () => { const max = parseInt(document.getElementById('max').value) document.getElementById('status').textContent = 'Calculating...' document.getElementById('findPrimes').disabled = true worker.postMessage({ findPrimesUpTo: max }) }) worker.onmessage = (event) => { const { primes, timeTaken } = event.data document.getElementById('status').textContent = `Found ${primes.length} primes in ${timeTaken}ms` document.getElementById('findPrimes').disabled = false }

这条链路的工作方式是:

  1. worker.postMessage({ findPrimesUpTo: max })把数据发到 Worker。两条线程不能直接访问对方的变量,一切通信都靠消息。
  2. Worker 端self.onmessage收到消息后执行计算,算完用self.postMessage({ primes, timeTaken })把结果发回主线程。
  3. 主线程的worker.onmessage回调触发,从event.data取出结果并更新 DOM——DOM 只能在主线程改(原因见文末限制一节)。

如何判断做成功了:点击按钮后,worker.onmessage触发,状态元素显示形如Found N primes in Xms的文案(N 和 X 随输入值变化,不是固定数值);同时计算期间页面不再冻结——这是文档定义 Worker 价值的核心:run expensive computations without freezing the UI。

Classic 与 Module 两种创建方式

上面用的是{ type: 'module' }的 module worker,也是文档推荐的方式:支持import/export、默认严格模式、支持顶层 await,与现代构建工具配合更好。经典写法用importScripts()加载依赖,兼容性更广(文档对比表标注为 all browsers)。如果不确定目标浏览器对 module worker 的支持情况,可以先用经典写法,或按文档建议投产前检查浏览器支持。

消息里能传什么:结构化克隆的边界

postMessage传数据不是共享引用,而是用结构化克隆算法(structured clone)复制一份给对端,Worker 里的修改不影响原始对象。能克隆的类型包括:原始值、普通对象和数组、DateRegExpBlob/FileArrayBuffer/TypedArray、Map/Set、标准Error类型、ImageBitmap/ImageData

不能克隆的会直接抛错:函数、DOM 节点、SymbolWeakMap/WeakSet、带原型链的对象、getter/setter、Proxy。文档给出的会报错的例子:

// ❌ These will throw errors worker.postMessage({ fn: () => console.log('hi'), // Functions can't be cloned element: document.body, // DOM nodes can't be cloned sym: Symbol('test') // Symbols can't be cloned })

由此引出一个常见错误:不能把回调函数发给 Worker 让它“算完调用”。正确做法是只发数据,在onmessage里处理结果:

worker.postMessage({ data: [1, 2, 3] }) worker.onmessage = (e) => console.log(e.data) // "Callback" on main thread

另外,标准 Error 类型(ErrorRangeErrorTypeError等)可以克隆,namemessage会被保留,浏览器可能还保留stackcause

大数据传输:用 Transferable 代替拷贝

结构化克隆对大对象是慢的——文档明确警告:传几 MB 的数据时拷贝可能耗时数百毫秒(文档示例中对 100MB 缓冲区的拷贝注释为 “Could take hundreds of milliseconds”)。这时改用 transferable 对象:转移所有权而不是复制,传输几乎是瞬时的,代价是发送方的原始对象变为“detached”、不可再用。

// main.js const hugeBuffer = new ArrayBuffer(100 * 1024 * 1024) const array = new Uint8Array(hugeBuffer) // Fill with data... console.time('transfer') // Second argument is an array of objects to transfer worker.postMessage(hugeBuffer, [hugeBuffer]) console.timeEnd('transfer') // Nearly instant! // WARNING: hugeBuffer is now "detached" (unusable) console.log(hugeBuffer.byteLength) // 0 console.log(array.length) // 0

上面byteLength输出 0 是文档示例的展示,说明转移后原缓冲已不可用。Worker 端拿到完整数据(文档示例中输出104857600,即 100MB),处理完可以反向转移回来:

// worker.js self.onmessage = (event) => { const buffer = event.data console.log(buffer.byteLength) // 104857600 (100MB) // Process the data... const array = new Uint8Array(buffer) // Transfer it back when done self.postMessage(buffer, [buffer]) }

postMessage的第二个参数是要转移的对象数组。常见可转移类型:ArrayBufferMessagePortImageBitmapOffscreenCanvas、各种 Stream(ReadableStream/WritableStream/TransformStream)、WebCodecs 的AudioData/VideoFrame等。文档给出的经验法则:数据量大(> 1MB)且发送方不再需要这份数据时就 transfer;数据小或两端都要用就拷贝。

错误处理与 Worker 生命周期

文档强调两点:Worker 的错误默认会“消失”,不处理就只能干瞪眼;Worker 不终止就会一直占着资源。

挂上错误处理器

// main.js const worker = new Worker('worker.js', { type: 'module' }) // Handle messages worker.onmessage = (event) => { console.log('Result:', event.data) } // Handle errors thrown in the worker worker.onerror = (event) => { console.error('Worker error:', event.message) console.error('File:', event.filename) console.error('Line:', event.lineno) } // Handle message errors (e.g., data can't be cloned) worker.onmessageerror = (event) => { console.error('Message error:', event) }

onerror捕获 Worker 内部抛出的异常(含文件和行号),onmessageerror捕获消息层面的错误(典型场景就是上节说的数据无法克隆)。也可以用addEventListener('message' / 'error')替代onmessage属性写法。

用完要 terminate,或干脆复用同一个 Worker

文档列出的典型错误是在事件里反复创建新 Worker 从不清理:

// ❌ WRONG - Creates a new worker for each click, never cleans up button.addEventListener('click', () => { const worker = new Worker('worker.js') worker.postMessage(data) worker.onmessage = (e) => showResult(e.data) // Worker keeps running even after we're done! })

两种正确做法:

// ✓ CORRECT - Terminate when done button.addEventListener('click', () => { const worker = new Worker('worker.js') worker.postMessage(data) worker.onmessage = (e) => { showResult(e.data) worker.terminate() // Clean up! } }) // ✓ BETTER - Reuse the same worker const worker = new Worker('worker.js') worker.onmessage = (e) => showResult(e.data) button.addEventListener('click', () => { worker.postMessage(data) // Reuse existing worker })

对反复出现的同类重任务,文档还给出了 Worker 池模式(原文完整实现):预先创建固定数量的 Worker,任务来了分配给空闲的,都忙就进队列,避免每次都付创建开销;池大小通常取navigator.hardwareConcurrency(逻辑 CPU 核心数)。如果你的页面只是偶尔跑一次重计算,复用单个 Worker 就够了,不必引入池。

限制与边界

  • Worker 不能碰 DOM。Worker 的全局作用域里documentwindowlocalStoragesessionStoragealert都是undefined。需要更新界面时,把数据postMessage回主线程再改 DOM。文档指出的最常见错误就是在 Worker 里写document.getElementById(...),它会直接失败。
  • 不是所有 API 都可用。Worker 内可用fetchXMLHttpRequestsetTimeout/setIntervalIndexedDBWebSocketcrypto、部分navigator、只读locationconsole(输出出现在 DevTools)等;classic worker 用importScripts(),module worker 用import/export
  • 别为小任务开 Worker。Worker 创建、消息传递、数据克隆都有开销,文档的经验法则是任务耗时在 50–100ms 以上才值得用;给 3 个数求和这种事留在主线程做。
  • Shared Worker 不属于本篇路径。它是给多个同源标签页共享状态用的,且文档标注其浏览器支持有限(Android 浏览器不支持),与“把重计算移出主线程”这个目标无关。
  • module worker 的兼容性按文档对比表为“现代浏览器”,投产前需确认目标环境;需要更宽兼容时退回 classic worker。

完成验证可以回到主路径的成功标志:点击触发后页面在计算期间保持可交互,worker.onmessage收到 Worker 回传的event.data并更新 DOM,异常场景下onerror/onmessageerror能打印出错误文件与行号。如果需要更复杂的场景——解析大 CSV、处理 WebSocket 流数据、OffscreenCanvas 离屏渲染或从字符串内联创建 Worker——Web Workers 文档 的 Real-World Patterns 一节有对应实现。

【免费下载链接】33-js-concepts📜 33 JavaScript concepts every developer should know.项目地址: https://gitcode.com/GitHub_Trending/33/33-js-concepts

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 22:37:01

C语言复合字面量完全指南:C99语法、存储期与悬垂指针陷阱

说实话&#xff0c;我第一次在别人代码里见到 (struct point){ .x 10, .y 20 } 这种写法时&#xff0c;第一反应是“这玩意是什么&#xff1f;C语言什么时候能这样写了&#xff1f;”查了标准才发现&#xff0c;这是 C99 引入的复合字面量&#xff08;compound literal&…

作者头像 李华
网站建设 2026/9/9 22:34:34

微信群如何发起报名活动?2026年最新投票平台深度实测分享

在微信群里发起一场投票评选活动&#xff0c;如今已经成为班级评优、企业评先、社区互动乃至作品征集中最常见的操作。但很多组织者面对的问题是&#xff1a;在群里发通知让大家报名&#xff0c;然后一个一个私聊收作品、整理表格、再统一录入投票平台——光是收集选手信息这一…

作者头像 李华
网站建设 2026/9/9 22:34:14

C++异常处理实战:从栈展开到RAII的完整指南

C 的异常处理&#xff0c;说难不难&#xff0c;说简单也绝对不简单。我最初学的时候&#xff0c;觉得不就是try、catch、throw这三个关键字嘛&#xff0c;写几个 demo 就会了。但真正在项目里用起来&#xff0c;才发现坑一个接一个&#xff1a;析构函数里能不能抛异常、构造函数…

作者头像 李华
网站建设 2026/9/9 22:33:59

FM17XX读写芯片参考代码详解:ISO14443A/B读卡流程与移植要点

简介&#xff1a;FM17XX系列非接触式读卡芯片的参考代码包&#xff0c;面向嵌入式开发及智能门禁、读卡设备调试人员&#xff0c;解决ISO14443A/B协议下寻卡和底层驱动实现问题。资源内含基于STM32F10x的完整工程&#xff0c;涵盖TypeA/TypeB寻卡流程、PcdRequest等函数调用、S…

作者头像 李华
网站建设 2026/9/9 22:33:37

自动化测试模型详解:线性、模块化、数据驱动与关键字驱动

做了快五年自动化测试&#xff0c;面试别人时我特别喜欢问一个问题&#xff1a;“你现在的脚本属于哪种测试模型&#xff1f;”十个人里有八个能讲清楚 Selenium 怎么定位元素&#xff0c;但问到这一句&#xff0c;一半人会愣住。因为很多人做自动化是“先跑起来再说”&#xf…

作者头像 李华