
页面卡顿怎么办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: // - Cant click anything // - Cant 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 doesnt 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 WorkerDedicated 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。示例引用了页面上三个元素idmax的输入框、idfindPrimes的按钮、idstatus的状态展示元素照搬代码时需要保证页面里存在它们// 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 cant be cloned element: document.body, // DOM nodes cant be cloned sym: Symbol(test) // Symbols cant 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、各种 StreamReadableStream/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 cant 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 were 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),仅供参考