页面卡顿怎么办?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 }这条链路的工作方式是:
worker.postMessage({ findPrimesUpTo: max })把数据发到 Worker。两条线程不能直接访问对方的变量,一切通信都靠消息。- Worker 端
self.onmessage收到消息后执行计算,算完用self.postMessage({ primes, timeTaken })把结果发回主线程。 - 主线程的
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 里的修改不影响原始对象。能克隆的类型包括:原始值、普通对象和数组、Date、RegExp、Blob/File、ArrayBuffer/TypedArray、Map/Set、标准Error类型、ImageBitmap/ImageData。
不能克隆的会直接抛错:函数、DOM 节点、Symbol、WeakMap/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 类型(Error、RangeError、TypeError等)可以克隆,name和message会被保留,浏览器可能还保留stack和cause。
大数据传输:用 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的第二个参数是要转移的对象数组。常见可转移类型:ArrayBuffer、MessagePort、ImageBitmap、OffscreenCanvas、各种 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 的全局作用域里
document、window、localStorage、sessionStorage、alert都是undefined。需要更新界面时,把数据postMessage回主线程再改 DOM。文档指出的最常见错误就是在 Worker 里写document.getElementById(...),它会直接失败。 - 不是所有 API 都可用。Worker 内可用
fetch、XMLHttpRequest、setTimeout/setInterval、IndexedDB、WebSocket、crypto、部分navigator、只读location、console(输出出现在 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),仅供参考