面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 02026年5月27日 12:46

Web Worker 7 大限制及解决方案一览

为什么 Worker 有这么多限制Worker 的限制不是偷懒,是设计上的安全选择。浏览器最核心的约束是:DOM 操作不是线程安全的。两个线程同时改同一个 DOM 节点,后果不可预测。所以 Worker 干脆被隔离了——不能碰 DOM、不能碰大部分浏览器 API,只能通过 postMessage 通信。理解了这个前提,限制就不是"不能做什么",而是"怎么绕过去"。限制一:不能访问 DOM这是最大的限制。Worker 里没有 document、没有 window、没有任何 DOM API。// ❌ Worker 里直接报错document.getElementById('app');window.innerWidth;解决方式:计算在 Worker 里做,DOM 操作回主线程执行。// Worker:只算数据self.onmessage = (e) => { const positions = calculateLayout(e.data.items); self.postMessage({ positions });};// 主线程:拿到结果后操作 DOMworker.onmessage = (e) => { const { positions } = e.data; positions.forEach(({ id, x, y }) => { document.getElementById(id).style.transform = `translate(${x}px, ${y}px)`; });};这个模式有个名字叫"数据驱动渲染"——Worker 产出数据,主线程负责映射到 DOM。虚拟 DOM 框架(React/Vue)天然适合这种模式:Worker 里做 diff 计算,把最小更新集传给主线程 apply。如果需要频繁操作 Canvas,用 OffscreenCanvas 把 Canvas 上下文转移给 Worker:const canvas = document.getElementById('canvas');const offscreen = canvas.transferControlToOffscreen();worker.postMessage({ canvas: offscreen }, [offscreen]);// Worker 里直接绘制self.onmessage = (e) => { const ctx = e.data.canvas.getContext('2d'); ctx.fillStyle = 'red'; ctx.fillRect(0, 0, 100, 100);};限制二:不能用 localStoragelocalStorage 是同步 API,多线程同时读写会产生竞态条件,所以 Worker 被禁止访问。解决方式:用 IndexedDB 替代。IndexedDB 是异步的,Worker 可以直接使用。// Worker 里直接操作 IndexedDBconst request = indexedDB.open('myDB', 1);request.onupgradeneeded = (e) => { e.target.result.createObjectStore('data', { keyPath: 'id' });};request.onsuccess = (e) => { const db = e.target.result; const tx = db.transaction('data', 'readwrite'); tx.objectStore('data').put({ id: 1, value: 'from worker' });};如果你非要从 Worker 里读写 localStorage 的数据,让主线程做中转:// Worker 请求读取self.postMessage({ type: 'getLocalStorage', key: 'token' });// 主线程中转worker.onmessage = (e) => { if (e.data.type === 'getLocalStorage') { const value = localStorage.getItem(e.data.key); worker.postMessage({ type: 'localStorageResult', key: e.data.key, value }); }};但这样每读一次都要跨线程通信,性能很差。能用 IndexedDB 就用 IndexedDB。限制三:不能发起 XHR 请求XMLHttpRequest 的同步模式(open(method, url, false))会阻塞线程,在 Worker 里被禁止。但异步 XHR 其实也不推荐——用 fetch 替代。解决方式:Worker 里用 fetch,它是异步的且完全支持。// Worker 里直接发请求self.onmessage = async (e) => { const response = await fetch('https://api.example.com/data'); const data = await response.json(); self.postMessage({ data });};WebSocket 和 EventSource 也能在 Worker 里正常使用,不受限制。限制四:不能加载跨域脚本Worker 脚本必须和主页面同源。跨域 URL 直接创建会报 SecurityError。解决方式 1:Blob URL 内联。// 先 fetch 跨域脚本内容,再创建 Blob Workerconst response = await fetch('https://cdn.example.com/worker.js');const code = await response.text();const blob = new Blob([code], { type: 'text/javascript' });const worker = new Worker(URL.createObjectURL(blob));注意:这绕过了同源限制但引入了新风险——你加载的跨域代码可能被篡改。确保 CDN 可信,最好配上 SRI(Subresource Integrity)。解决方式 2:importScripts 可以加载跨域脚本(Worker 内部)。// worker.jsimportScripts('https://cdn.example.com/lib.js');importScripts 不受同源限制,但受 CSP 的 script-src 约束。限制五:没有 window 对象Worker 的全局对象是 self(DedicatedWorkerGlobalScope),不是 window。很多挂在 window 上的东西在 Worker 里不存在。| 主线程有 | Worker 里 | 替代方案 ||----------|-----------|----------|| window | self | 直接用 self || window.location | self.location(只读) | 能读不能改 || window.navigator | self.navigator | 大部分属性可用 || window.alert() | 不存在 | 用 postMessage 通知主线程 || window.setTimeout | self.setTimeout | 正常可用 || window.fetch | self.fetch | 正常可用 || window.indexedDB | self.indexedDB | 正常可用 |限制六:通信有序列化开销postMessage 默认用结构化克隆,数据要拷贝一份。小数据无所谓,大数据(几 MB 以上)拷贝开销可能比计算本身还大。解决方式:| 方案 | 适用场景 | 原理 ||------|----------|------|| Transferable | 大 ArrayBuffer/Blob 单向传输 | 所有权转移,零拷贝 || SharedArrayBuffer | 高频双向读写同一块数据 | 共享内存,Atomics 同步 || 批量发送 | 大量小消息 | 攒批发,减少序列化次数 |详见 Web Worker 通信全解析。限制七:脚本路径是相对 HTML 的// 如果 HTML 在 /pages/index.html// Worker 脚本在 /workers/task.jsnew Worker('task.js'); // ❌ 会找 /pages/task.jsnew Worker('/workers/task.js'); // ✅ 绝对路径在打包工具里更容易搞错。Vite/Webpack 5 的正确写法:const worker = new Worker( new URL('./worker.js', import.meta.url), { type: 'module' });import.meta.url 是当前模块的 URL,new URL 相对于它解析,打包工具会正确处理路径。总结:一张表搞定| 限制 | 解决方案 ||------|----------|| 不能访问 DOM | Worker 算数据,主线程操作 DOM;用 OffscreenCanvas || 不能用 localStorage | 用 IndexedDB 替代 || 不能用同步 XHR | 用 fetch 替代 || 不能加载跨域脚本 | Blob URL 或 importScripts || 没有 window 对象 | 用 self 替代 || 通信有序列化开销 | Transferable / SharedArrayBuffer / 批量发送 || 脚本路径问题 | new URL('./worker.js', import.meta.url) |这些限制的本质就是一条:Worker 是数据处理器,不是 UI 控制器。把计算放进去,把渲染留在外面,架构对了限制就不是问题。
服务端阅读 02026年5月27日 12:45

Web Worker 调试指南:DevTools、消息追踪与内存分析

Worker 调试为什么难Worker 跑在独立线程里,console.log 能用但输出混在主线程日志里不好找,断点默认不生效,报错了堆栈和主线程是断开的。但只要知道工具在哪,调试 Worker 并不比调主线程难多少。Chrome DevTools:最常用的方式找到 Worker 线程打开 DevTools → Sources 面板 → 左侧 Threads 区域。主线程和 Worker 线程会分开列出,点击 Worker 线程就能看到它的源码、设断点、看调用栈。如果 Threads 区域没出现 Worker,检查两个地方:DevTools 设置(F1)→ 勾选"Workers"下的"Auto-expand"确认 Worker 已经被创建——在 Console 里输入 chrome && chrome.debugger 确认在 Worker 里打断点和主线程一样:Sources 面板里打开 Worker 的 JS 文件,点行号设断点。Worker 里代码执行到断点会暂停,主线程不受影响(但 postMessage 会排队等 Worker 恢复)。专用 Worker 的 ConsoleWorker 里的 console.log 会输出到 DevTools Console,但前面没有线程标识,容易和主线程日志混淆。建议在 Worker 里加前缀:// worker.jsfunction log(...args) { console.log('[Worker]', ...args);}log('开始处理数据', data.length);Shared Worker 和 Service Worker 的调试入口这两种 Worker 不在页面的 DevTools 里直接显示,需要单独打开:Shared Worker:访问 chrome://inspect/#workers,能看到所有 Shared Worker 实例,点击 inspect 打开独立 DevTools 窗口Service Worker:DevTools → Application 面板 → Service Workers 区域,可以查看注册状态、手动触发 update、模拟推送事件console 之外的调试手段结构化日志比加前缀更进一步,用结构化日志让 Worker 的输出可追溯:// worker.jsfunction log(level, event, data = {}) { console.log(JSON.stringify({ source: 'worker', level, event, timestamp: Date.now(), ...data }));}log('info', 'task-start', { taskId: 1, dataSize: 10000 });log('error', 'task-failed', { taskId: 1, error: err.message });这样日志可以统一采集和分析,线上排查问题时不用对着混在一起的 Console 猜哪条是 Worker 输出的。消息日志:窥探通信内容Worker 的 bug 经常出在通信环节——发了消息但格式不对,或者该回消息的没回。写一个消息拦截器记录所有 postMessage:// 主线程:拦截 Worker 通信function createDebugWorker(url) { const worker = new Worker(url); const originalPostMessage = worker.postMessage.bind(worker); worker.postMessage = (data, transfer) => { console.log('[Main → Worker]', JSON.stringify(data).slice(0, 200)); originalPostMessage(data, transfer); }; worker.onmessage = (e) => { console.log('[Worker → Main]', JSON.stringify(e.data).slice(0, 200)); }; return worker;}const worker = createDebugWorker('worker.js');Worker 端也加一层:// worker.jsconst originalPostMessage = self.postMessage.bind(self);self.postMessage = (data, transfer) => { console.log('[Worker → Main]', JSON.stringify(data).slice(0, 200)); originalPostMessage(data, transfer);};这样每次通信都有日志,消息丢了、格式错了一目了然。上线前记得删掉或用环境变量控制开关。Performance 面板分析 Worker 性能DevTools Performance 面板会录制所有线程的活动。录制一段操作后,在时间轴上能看到:Main 线程的活动(紫色是渲染,黄色是脚本)Worker 线程的活动(独立一行,黄色标记脚本执行)postMessage 的发送和接收时间点如果发现 Worker 任务执行时间过长,点击对应的黄色条块能看到函数调用栈和耗时分布,精确定位热点函数。常见调试场景Worker 没有响应排查步骤:确认 Worker 创建成功——worker.onerror 有没有触发确认消息发出去了——用消息拦截器看 [Main → Worker] 日志确认 Worker 收到了消息——在 Worker 入口加 log('received', e.data)确认 Worker 没有卡在死循环——Performance 面板看 Worker 线程是否一直在执行确认 Worker 没有报错——检查 Console 是否有未捕获异常最常见的两个原因:Worker 脚本路径错了(创建时就失败了,但 onerror 没监听),或者消息格式不匹配(Worker 里 e.data.type 判断分支没命中)。内存泄漏Worker 长时间运行后内存持续上涨:DevTools → Memory 面板 → 选择 Worker 线程 → 拍 Heap Snapshot对比两次 Snapshot,看哪些对象只增不减常见原因:闭包引用了大对象、事件监听器没移除、定时器没清除// Worker 里常见的泄漏模式self.onmessage = (e) => { const hugeData = e.data; // 泄漏:闭包引用了 hugeData,永远不会被 GC setInterval(() => { console.log(hugeData.length); // hugeData 被闭包持有 }, 1000);};修复方式:用完即释放,或者定时器保存引用,不需要时 clearInterval。Shared Worker 连不上SharedWorker 的调试入口在 chrome://inspect/#workers。常见问题:port.start() 忘了调用——消息收不到但不报错连接 URL 必须完全一致(包括 query string)——两个页面用不同 URL 创建的 SharedWorker 是两个独立实例同源策略——不同源的页面不能共享同一个 Worker调试工具速查| 工具 | 用途 | 入口 ||------|------|------|| DevTools Sources | 断点、单步、调用栈 | F12 → Sources → Threads || DevTools Console | Worker 日志 | F12 → Console || DevTools Performance | Worker 性能分析 | F12 → Performance || DevTools Memory | Worker 内存快照 | F12 → Memory → 选 Worker 线程 || chrome://inspect/#workers | Shared/Service Worker 调试 | 地址栏直接访问 || Application → Service Workers | Service Worker 状态管理 | F12 → Application |上线前的调试清理调试代码(日志拦截器、前缀 console、消息追踪)上线前必须清理或条件化。推荐用环境变量控制:const DEBUG = typeof self !== 'undefined' && self.location?.search?.includes('debug=1');function log(...args) { if (DEBUG) console.log('[Worker]', ...args);}这样开发时 URL 加 ?debug=1 就能看到 Worker 日志,线上默认关闭不影响性能。
服务端阅读 02026年5月27日 12:43

Dedicated、Shared、Service Worker:三种 Web Worker 详解

三种 Worker,三种用途浏览器里能叫"Worker"的有三种,干的事完全不一样:| 类型 | 一句话定位 | 和页面关系 | 典型用途 ||------|-----------|-----------|----------|| Dedicated Worker | 后台计算线程 | 一对一,页面关了它就销毁 | 排序、解析、图像处理 || Shared Worker | 多页面共享的后台线程 | 多对一,所有同源页面共享 | 跨标签页状态同步 || Service Worker | 网络代理 + 离线缓存 | 独立生命周期,页面关了还活着 | PWA、离线、请求拦截 |别搞混——Dedicated Worker 是拿来干活的,Shared Worker 是拿来共享的,Service Worker 是拿来代理网络的。Dedicated Worker:用得最多的那个绝大多数时候你说的"Web Worker"就是它。一个页面创建,只有这个页面能用,页面关了 Worker 也跟着销毁。// 创建const worker = new Worker('worker.js');// 双向通信worker.postMessage({ type: 'start', data: payload });worker.onmessage = (e) => console.log('结果:', e.data);// 关闭worker.terminate();也可以用 Blob URL 创建内联 Worker,不用单独的 JS 文件:const code = ` self.onmessage = (e) => { const result = heavyCalc(e.data); self.postMessage(result); };`;const worker = new Worker(URL.createObjectURL(new Blob([code], { type: 'text/javascript' })));Dedicated Worker 的生命周期很简单:创建 → 运行 → terminate 或页面关闭。没有什么"激活""等待"状态,不需要管理复杂状态机。Shared Worker:跨标签页的共享线程多个同源标签页可以共用同一个 Shared Worker 实例。适合做跨页面状态同步——比如用户在标签页 A 加了购物车商品,标签页 B 实时看到数量更新。// 每个页面都这样创建,浏览器会复用同一个实例const worker = new SharedWorker('shared-worker.js');// 注意:SharedWorker 用 port 通信,不是直接 onmessageworker.port.start();worker.port.postMessage({ type: 'cart-update', item: 'iPhone 17' });worker.port.onmessage = (e) => { console.log('收到:', e.data);};Worker 端也不一样,用 onconnect 接收新连接:// shared-worker.jsconst clients = [];self.onconnect = (e) => { const port = e.ports[0]; clients.push(port); port.onmessage = (event) => { // 广播给所有连接的页面 clients.forEach(client => { client.postMessage(event.data); }); };};Shared Worker 的坑:调试困难——Chrome DevTools 里要单独打开 Shared Worker 的调试面板(chrome://inspect/#workers)所有连接断开后 Worker 才会销毁,不是最后一个页面关了就立刻死port.start() 容易忘写,忘写了消息收不到但也不报错Service Worker:不是普通 WorkerService Worker 是三种里最特殊的。它不是用来做计算的,而是浏览器的网络代理层:拦截请求:页面发出的 fetch 请求先经过 Service Worker,可以改写响应、返回缓存离线支持:把资源缓存下来,断网时也能访问推送通知:即使页面没打开,也能收到服务端推送后台同步:网络恢复时自动重试失败的请求// 注册navigator.serviceWorker.register('/sw.js');// sw.jsself.addEventListener('install', (event) => { // 安装时预缓存资源 event.waitUntil( caches.open('v1').then(cache => cache.addAll(['/index.html', '/app.js'])) );});self.addEventListener('fetch', (event) => { // 拦截请求,先查缓存 event.respondWith( caches.match(event.request).then(cached => cached || fetch(event.request)) );});Service Worker 的生命周期和其他两种完全不同:安装(install) → 激活(activate) → 运行中 ↑ ↓ 等待(waiting) ← 更新发现关键区别:Service Worker 在页面关闭后仍然存活,浏览器会在需要时唤醒它。这也是为什么它能处理推送通知和后台同步。Service Worker 不能做的事:同步 XHR、访问 DOM、访问 localStorage。和 Dedicated Worker 一样受 API 限制,但更严格——连 self.localStorage 都没有,只能用 Cache API 和 IndexedDB。怎么选| 场景 | 选哪个 ||------|--------|| 页面内耗时计算(排序、解析) | Dedicated Worker || 多标签页共享状态 | Shared Worker || 离线缓存、请求拦截 | Service Worker || 推送通知 | Service Worker || 后台数据同步 | Service Worker || 图像/音视频处理 | Dedicated Worker |一个常见错误:用 Shared Worker 做计算密集型任务。Shared Worker 的设计初衷是共享状态,不是共享算力。如果多个页面同时往一个 Shared Worker 发计算任务,它还是单线程处理,反而互相等待。另一个常见错误:把 Service Worker 当普通 Worker 用。Service Worker 的生命周期管理复杂,它会在不可预期的时间被浏览器唤醒和终止。在它里面做长耗时计算是不靠谱的——可能算到一半就被杀了。
服务端阅读 02026年5月27日 12:42

Web Worker 性能优化:通信、并行与内存管理

先搞清楚瓶颈在哪Worker 性能优化不是玄学,瓶颈就三个地方:创建开销、通信开销、计算开销。先 Profiler 看哪个是瓶颈,再对症下药,别瞎优化。创建开销:复用比重建快 100 倍new Worker() 不是免费的。浏览器要分配线程、解析脚本、初始化上下文,一次创建大概 10-50ms。如果你每次任务都新建再 terminate,开销比任务本身还大。Worker 池和数据库连接池一个道理——预先创建好,任务来了分配,做完了归还:class WorkerPool { constructor(workerUrl, size = navigator.hardwareConcurrency || 4) { this.workers = []; this.queue = []; for (let i = 0; i < size; i++) { const worker = new Worker(workerUrl); worker.busy = false; worker.onmessage = (e) => { const { resolve } = worker.task; delete worker.task; worker.busy = false; this.processQueue(); resolve(e.data); }; this.workers.push(worker); } } exec(data) { return new Promise((resolve) => { const worker = this.workers.find(w => !w.busy); if (worker) { worker.busy = true; worker.task = { resolve }; worker.postMessage(data); } else { this.queue.push({ data, resolve }); } }); } processQueue() { if (this.queue.length === 0) return; const worker = this.workers.find(w => !w.busy); if (!worker) return; const { data, resolve } = this.queue.shift(); worker.busy = true; worker.task = { resolve }; worker.postMessage(data); }}// 使用const pool = new WorkerPool('worker.js', 4);const result = await pool.exec({ type: 'sort', data: largeArray });Worker 池适合任务频繁但单个任务不太大的场景。如果任务很少(比如页面生命周期内就跑一两次),直接 new Worker() 就行,别过度设计。通信开销:序列化才是大头Worker 通信的瓶颈不在网络,在序列化。postMessage 默认用结构化克隆,数据量大的时候拷贝耗时惊人。Transferable:零拷贝传大数据const buffer = new Float64Array(1_000_000);// 慢:结构化克隆,拷贝 8MB 数据worker.postMessage({ data: buffer });// 快:转移所有权,零拷贝worker.postMessage({ data: buffer }, [buffer.buffer]);// 注意:转移后主线程不能再访问 buffer实测数据:| 数据大小 | 结构化克隆 | Transferable ||----------|-----------|--------------|| 100KB | ~0.5ms | ~0.05ms || 1MB | ~5ms | ~0.1ms || 10MB | ~50ms | ~0.2ms |10MB 以上的数据,不用 Transferable 等于白用 Worker。SharedArrayBuffer:跳过序列化Transferable 虽然零拷贝,但只能单向传——发过去主线程就没了。如果你需要双向频繁读写同一块数据,用 SharedArrayBuffer:const shared = new SharedArrayBuffer(1024 * 1024);const view = new Float64Array(shared);// 主线程和 Worker 共享同一块内存worker.postMessage({ shared });// Worker 里直接读写self.onmessage = (e) => { const view = new Float64Array(e.data.shared); Atomics.store(view, 0, 42); // 原子写入};需要配合 Atomics 做原子操作,服务端还要配 COOP/COEP 头,门槛比 Transferable 高。但高频通信场景下收益巨大——完全没有序列化开销。批量发送:减少通信次数每秒 postMessage 100 次和 1 次发 100 条数据,后者快得多。序列化有固定开销(即使数据很小也要走一遍结构化克隆流程),减少次数比减少数据量更有效:// 慢:逐条发送data.forEach(item => worker.postMessage(item));// 快:攒批发送worker.postMessage({ batch: data });计算开销:用多 Worker 并行单 Worker 的计算速度和主线程 JS 一样,只是不卡 UI。要真正加速,得把任务拆给多个 Worker 并行跑:function parallelSort(data, workerCount = 4) { const chunkSize = Math.ceil(data.length / workerCount); const chunks = []; for (let i = 0; i < workerCount; i++) { chunks.push(data.slice(i * chunkSize, (i + 1) * chunkSize)); } return Promise.all(chunks.map((chunk, i) => { return new Promise((resolve) => { const worker = new Worker('sort-worker.js'); worker.onmessage = (e) => resolve(e.data); worker.postMessage(chunk); }); })).then(sortedChunks => { // 合并已排序的分片 return mergeSortedArrays(sortedChunks); });}实测 100 万元素数组排序:| 方案 | 耗时 ||------|------|| 主线程单线程 | ~800ms(UI 卡死) || 单 Worker | ~800ms(UI 正常) || 4 Worker 并行 | ~250ms(UI 正常) |Worker 数量不要超过 CPU 核心数,navigator.hardwareConcurrency 可以拿到。多了反而会因为线程调度开销变慢。内存管理Worker 占的内存不会自动释放,必须显式 terminate()。如果页面生命周期内不再需要某个 Worker,立刻关掉:// 任务完成后关闭worker.onmessage = (e) => { handleResult(e.data); worker.terminate(); // 释放线程和内存};// 或者超时强制关闭const timeout = setTimeout(() => worker.terminate(), 30000);worker.onmessage = (e) => { clearTimeout(timeout); handleResult(e.data);};长时间运行的 Worker 要注意内存泄漏——Worker 里的闭包、事件监听器、定时器如果不用了不清理,内存会持续上涨。在 Worker 里加个定期自检:setInterval(() => { const used = performance.memory?.usedJSHeapSize; if (used && used > 50 * 1024 * 1024) { // 超过 50MB self.postMessage({ type: 'memory-warning', used }); }}, 10000);懒加载:按需创建 Worker不是所有 Worker 都要在页面加载时就创建。用 new URL() + 动态 import 实现按需加载,首屏不需要的 Worker 等用到时再创建:async function getWorker() { if (!workerInstance) { workerInstance = new Worker( new URL('./heavy-worker.js', import.meta.url), { type: 'module' } ); } return workerInstance;}// 用户点击"导出"按钮时才创建button.onclick = async () => { const worker = await getWorker(); worker.postMessage(exportData);};优化优先级按收益从大到小排:Transferable 替代结构化克隆(大数据场景立竿见影)Worker 池复用(频繁创建销毁场景收益大)批量发送减少通信次数(高频小消息场景)多 Worker 并行(计算密集型场景)SharedArrayBuffer(超高频双向通信场景,门槛高但收益最大)懒加载(首屏性能敏感场景)
服务端阅读 02026年5月27日 12:40

Web Worker 安全攻防:同源策略、CSP 和通信风险

Worker 不是法外之地很多人以为 Worker 跑在独立线程里,安全性就天然有保障。恰恰相反——Worker 引入了新的攻击面:跨域脚本加载、postMessage 注入、SharedArrayBuffer 竞态,每一个都可能被利用。本文把 Web Worker 相关的安全问题和防御手段讲清楚。同源策略:第一道防线Worker 脚本必须和主页面同源(协议 + 域名 + 端口一致)。这是浏览器强制的,不是建议。// 跨域加载 → 直接报错new Worker('https://evil.com/worker.js'); // SecurityError// 同源加载 → 正常new Worker('/workers/task.js');但同源策略有绕过方式,而这些绕过方式本身就是安全隐患。Blob URL 的风险用 Blob URL 可以绕过同源限制,创建内联 Worker:// 从任意字符串创建 Workerconst code = 'self.onmessage = (e) => { /* ... */ }';const blob = new Blob([code], { type: 'text/javascript' });new Worker(URL.createObjectURL(blob));问题在于:如果 code 的内容来自用户输入或外部 API,攻击者就能注入任意代码在 Worker 里执行。永远不要用不受信任的数据构造 Worker 脚本。用完后必须 URL.revokeObjectURL() 释放,否则内存泄漏。importScripts 的跨域加载Worker 内部可以用 importScripts() 加载外部脚本,这个方法不受同源限制:// worker.jsimportScripts('https://cdn.example.com/lib.js'); // 允许跨域这是个设计选择——Worker 需要加载工具库。但这也意味着如果 CDN 被入侵或者 DNS 被劫持,恶意脚本就跑进了你的 Worker。防御方式:在服务端配置 Content-Security-Policy 的 script-src 指令,限制 importScripts 能加载哪些来源的脚本。CSP 对 Worker 的约束Worker 有自己的执行上下文,CSP 的约束方式和主页面不同:同源 Worker 脚本(通过 URL 加载):不受创建它的页面的 CSP 限制Blob/data URL Worker:继承创建它的页面的 CSP 策略Worker 内的 importScripts:受 Worker 自身的 CSP 约束(如果有)这意味着如果你想限制 Worker 的行为,需要给 Worker 脚本的 HTTP 响应也加上 CSP 头:Content-Security-Policy: script-src 'self' cdn.example.compostMessage 通信安全postMessage 是 Worker 和主线程唯一的通信通道,也是 XSS 注入的潜在入口。验证消息来源主线程收到的消息不一定来自你的 Worker。特别是 SharedWorker 和 Service Worker 场景下,多个页面都能发消息:// 主线程:验证消息来源和格式worker.onmessage = (e) => { const data = e.data; // 类型校验 if (typeof data !== 'object' || data === null) return; if (typeof data.type !== 'string') return; // 只处理已知的消息类型 const allowedTypes = ['result', 'progress', 'error']; if (!allowedTypes.includes(data.type)) return; // 处理消息 handleMessage(data);};// Worker 端同理:验证主线程发来的数据self.onmessage = (e) => { const data = e.data; if (!data || typeof data.type !== 'string') return; // ...};不要直接执行消息里的代码// 危险!永远不要这么做self.onmessage = (e) => { eval(e.data.code); // 任意代码执行 new Function(e.data.fn)(); // 同样危险};看似明显,但在模板引擎或动态逻辑场景里容易踩进去。如果必须根据消息执行不同逻辑,用白名单映射:const handlers = { sort: (data) => { /* ... */ }, filter: (data) => { /* ... */ },};self.onmessage = (e) => { const handler = handlers[e.data.type]; if (handler) handler(e.data.params);};SharedArrayBuffer 的安全门槛SharedArrayBuffer 允许主线程和 Worker 共享同一块内存,没有序列化开销。但它也带来了竞态条件风险——两个线程同时写同一个内存位置,数据就乱了。浏览器对 SharedArrayBuffer 有严格的安全要求,服务端必须返回以下两个响应头,否则 new SharedArrayBuffer() 直接抛错:Cross-Origin-Opener-Policy: same-originCross-Origin-Embedder-Policy: require-corp这两个头不是"建议加",而是强制要求。原因是为了防止 Spectre 类的侧信道攻击——没有这些头,恶意页面可以通过 SharedArrayBuffer 读取跨域内存数据。如果加上 COEP 后你的页面加载第三方资源(图片、脚本)出错了,需要给这些资源的响应加上 Cross-Origin-Resource-Policy: cross-origin 头。Worker 里能访问什么、不能访问什么从安全角度看,Worker 的 API 限制本身就是一种防护:| 能访问 | 不能访问 | 安全意义 ||--------|----------|----------|| fetch、WebSocket | document、DOM | 不能直接篡改页面 || IndexedDB | localStorage | 避免同步 I/O 竞态 || Cache API | window、parent | 隔离全局作用域 || Notifications | XMLHttpRequest | 推荐用 fetch 替代 || performance | location(只读) | 不能跳转页面 |这些限制意味着即使 Worker 代码被攻破,攻击者也无法直接操作 DOM 或窃取 localStorage 中的 token。Worker 的攻击半径被刻意缩小了。实际攻击场景场景 1:CDN 供应链攻击。你的 Worker 用 importScripts('https://cdn.example.com/lib.js'),CDN 被入侵后恶意代码跑进了 Worker。防御:CSP 限制 script-src,或改用 npm 包 + 打包工具。场景 2:postMessage 中间人。攻击者在页面注入脚本拦截 Worker 通信,篡改消息内容。防御:消息加签名校验,关键字段用加密传输。场景 3:Blob Worker 代码注入。从服务端获取的配置数据直接拼进 Worker 代码字符串,攻击者通过配置接口注入恶意代码。防御:Worker 代码和数据严格分离,用 postMessage 传配置,不拼字符串。安全检查清单Worker 脚本是否只从同源加载?如果是 Blob URL,代码来源是否可信?importScripts 加载的外部脚本是否有 CSP 保护?postMessage 通信是否做了类型校验和白名单过滤?有没有用 eval 或 new Function 执行消息中的代码?SharedArrayBuffer 是否配了 COOP/COEP 响应头?Worker 脚本 MIME 类型是否为 text/javascript?Blob URL 用完后是否调用了 revokeObjectURL?
服务端阅读 02026年5月27日 12:37

Web Worker 实战:让页面不再卡顿

JavaScript 的单线程困局浏览器里,JS 和 UI 渲染共享同一个线程。这意味着一件事:JS 代码跑多久,页面就卡多久。当你排序 10 万条数据、解析 20MB 的 JSON、或者做复杂的图像运算时,用户看到的不是加载动画,而是冻住的页面——滚动没用,点击没用,连浏览器标签页都显示"无响应"。Web Worker 就是冲着这个问题来的:给 JS 开一条独立线程,把耗时任务丢过去跑,主线程继续处理 UI。Worker 到底是什么Worker 是浏览器提供的一个独立执行环境,和主线程平级运行。几个关键事实:独立线程:Worker 有自己的调用栈和事件循环,不会阻塞主线程独立全局对象:Worker 里没有 window,取而代之的是 self(DedicatedWorkerGlobalScope)不能碰 DOM:document、element、localStorage 一概不可用只能用消息通信:postMessage 发,onmessage 收,数据走结构化克隆同源限制:Worker 脚本必须和页面同源怎么用创建和通信// 主线程const worker = new Worker('worker.js');// 发数据给 Workerworker.postMessage({ type: 'sort', data: largeArray });// 接收 Worker 返回的结果worker.onmessage = (e) => { console.log('结果:', e.data.result);};// 出错处理worker.onerror = (e) => { console.error(`Worker 错误: ${e.message} (${e.filename}:${e.lineno})`);};// 不用了就关掉worker.terminate();// worker.jsself.onmessage = (e) => { const { type, data } = e.data; if (type === 'sort') { const result = data.sort((a, b) => a - b); self.postMessage({ result }); }};内联 Worker:不想多一个文件有时候 Worker 代码很短,单独建文件嫌麻烦。可以用 Blob URL 创建内联 Worker:const code = ` self.onmessage = (e) => { const result = heavyCalc(e.data); self.postMessage(result); };`;const blob = new Blob([code], { type: 'application/javascript' });const worker = new Worker(URL.createObjectURL(blob));这在单文件组件或沙箱环境里特别好用。多个 Worker 并行一个 Worker 不够就开多个。浏览器对 Worker 数量没有硬限制,但每个 Worker 都占一个线程,开太多反而有调度开销。通常根据 CPU 核心数来定:const cores = navigator.hardwareConcurrency || 4;const workers = Array.from({ length: cores }, () => new Worker('worker.js'));// 把任务分片给多个 Workerconst chunkSize = Math.ceil(data.length / cores);const results = await Promise.all( workers.map((worker, i) => { const chunk = data.slice(i * chunkSize, (i + 1) * chunkSize); return new Promise((resolve) => { worker.onmessage = (e) => resolve(e.data.result); worker.postMessage({ data: chunk }); }); }));什么时候该用 Worker不是所有耗时操作都需要 Worker。判断标准很简单:会不会阻塞主线程超过 50ms? 会就上 Worker,不会就不必。值得用 Worker 的场景:大数据排序、过滤、聚合(超过 1 万条记录的客户端处理)文件解析(CSV、JSON、Excel)图像处理(Canvas 像素操作、滤镜)加密运算(RSA、AES 大数据量加密)实时数据流处理(WebSocket 推送数据的聚合计算)不需要 Worker 的场景:fetch 请求——本来就异步,不阻塞主线程简单的 DOM 操作——Worker 做不了定时器——setTimeout/setInterval 本身不阻塞少量数据运算(几百条数据的遍历)Worker 的限制和绕过方式| 限制 | 绕过方式 ||------|----------|| 不能访问 DOM | 把计算结果 postMessage 回主线程,主线程操作 DOM || 不能用 localStorage | 用 IndexedDB 替代,Worker 可以访问 || 不能用 XMLHttpRequest | 用 fetch 替代,Worker 支持 || 不能用 window 对象 | 用 self 替代全局对象 || 同源限制 | 用 Blob URL 创建内联 Worker || 通信有序列化开销 | 大数据用 Transferable 零拷贝,高频通信用 SharedArrayBuffer |Worker 的三种类型Dedicated Worker:最常见的,和一个页面绑定,页面关了 Worker 也销毁。Shared Worker:多个页面共享同一个 Worker 实例。适合多标签页同步状态的场景,比如购物车数量、未读消息数。创建方式不同:const worker = new SharedWorker('shared-worker.js');worker.port.onmessage = (e) => { /* 收消息 */ };worker.port.postMessage({ type: 'sync' });Service Worker:本质是网络代理,拦截请求、管理缓存。PWA 的核心,和普通 Worker 用途完全不同,别混为一谈。常见踩坑坑 1:频繁通信拖垮性能。每秒 postMessage 几百次,序列化开销比计算本身还大。解决方案:批量发送,攒够一批再传;或者改用 SharedArrayBuffer 共享内存。坑 2:Worker 里抛的异常主线程收不到。必须在主线程监听 worker.onerror,否则 Worker 静默挂掉你都不知道。坑 3:Transferable 传完后原数据变空。postMessage({ buffer }, [buffer]) 之后,主线程的 buffer.byteLength 变成 0。如果主线程还需要这个数据,先拷贝一份再传。坑 4:Worker 脚本路径是相对 HTML 的,不是相对 JS 文件的。在打包工具(Webpack/Vite)里容易路径搞错,建议用 new URL('./worker.js', import.meta.url) 让打包工具正确处理。// Vite/Webpack 5 的正确写法const worker = new Worker( new URL('./worker.js', import.meta.url), { type: 'module' });性能实测在 Chrome 120 / M1 MacBook Pro 上,对 100 万元素数组做排序:| 方案 | 耗时 | 主线程影响 ||------|------|-----------|| 主线程直接排序 | ~800ms | UI 完全卡死 || Worker 排序 | ~800ms | UI 正常响应 || 4 个 Worker 分片排序 | ~250ms | UI 正常响应 |Worker 不加速计算,但释放主线程。多 Worker 并行才是真正的加速——代价是代码复杂度上去了,需要分片和合并结果。
服务端阅读 02026年5月27日 12:32

Web Worker 通信全解析:从 postMessage 到共享内存

两种通信方式:拷贝和共享Worker 和主线程之间不共享内存(SharedArrayBuffer 除外),数据必须"过桥"。过桥有两种方式:结构化克隆(默认):数据完整拷贝一份,双方各持一份,互不影响。类似你复印一份文件给同事。Transferable 转移:数据所有权直接移交,发送方丧失访问权。类似你把原件直接递给同事,自己手里没了。// 结构化克隆(默认)—— 数据拷贝worker.postMessage({ data: largeArray });// 主线程和 Worker 各有一份,largeArray 仍在// Transferable 转移 —— 所有权移交const buffer = new ArrayBuffer(1024 * 1024); // 1MBworker.postMessage({ buffer }, [buffer]);// buffer.byteLength === 0,主线程不能再用了选哪种?小数据无所谓,大数据(超过 100KB 的 ArrayBuffer、Blob)用 Transferable,否则拷贝开销能吃掉你 Worker 带来的全部性能收益。结构化克隆支持什么postMessage 不是 JSON.stringify,它用的是浏览器内置的结构化克隆算法,能处理的东西比 JSON 多:能传的:对象、数组、字符串、数字、布尔值、Date、RegExp、Blob、File、ArrayBuffer、TypedArray、Map、Set、ImageData、Error不能传的:函数、DOM 节点、Symbol、有循环引用的对象(部分情况)一个容易踩的坑:对象的方法和原型链不会被克隆。你传一个 class 实例过去,对面收到的是一个纯数据对象,方法全丢了。如果 Worker 需要调用方法,要么传纯数据重新构造,要么用 RPC 模式。双向通信的实战写法简单的 echo 通信谁都会写,但生产环境里你需要的是"请求-响应"模式——主线程发任务,Worker 算完回结果,最好还能 Promise 化。// 主线程:封装 RPC 风格的 Worker 通信class WorkerRPC { constructor(url) { this.worker = new Worker(url); this.id = 0; this.pending = new Map(); this.worker.onmessage = (e) => { const { id, result, error } = e.data; const { resolve, reject } = this.pending.get(id); this.pending.delete(id); error ? reject(new Error(error)) : resolve(result); }; } call(method, params) { return new Promise((resolve, reject) => { const id = ++this.id; this.pending.set(id, { resolve, reject }); this.worker.postMessage({ id, method, params }); }); }}// 使用const rpc = new WorkerRPC('worker.js');const sorted = await rpc.call('sort', { data: largeArray });// worker.js:处理 RPC 调用const handlers = { sort: ({ data }) => data.sort((a, b) => a - b), filter: ({ data, condition }) => data.filter(condition),};self.onmessage = async (e) => { const { id, method, params } = e.data; try { const result = await handlers[method](params); self.postMessage({ id, result }); } catch (err) { self.postMessage({ id, error: err.message }); }};这样主线程就可以 await rpc.call('sort', data) 了,比裸写 postMessage + onmessage 干净很多。SharedArrayBuffer:真正的共享内存结构化克隆和 Transferable 本质上还是"传数据",有拷贝或转移开销。如果你要的是两个线程同时读写同一块内存,用 SharedArrayBuffer。// 主线程:创建共享内存const shared = new SharedArrayBuffer(1024);const view = new Int32Array(shared);worker.postMessage({ shared });// Worker:直接读写同一块内存self.onmessage = (e) => { const view = new Int32Array(e.data.shared); // 用 Atomics 做原子操作,避免竞态 Atomics.add(view, 0, 1); Atomics.store(view, 1, 42);};关键点:共享内存没有自动同步机制,必须用 Atomics API 做原子操作,否则两个线程同时写一个位置,数据就乱了。Atomics 提供了 add、sub、compareExchange、wait/notify 等操作,基本够用。注意:SharedArrayBuffer 有安全限制,服务端必须返回 Cross-Origin-Opener-Policy: same-origin 和 Cross-Origin-Embedder-Policy: require-corp 两个响应头,否则浏览器直接拒绝。很多开发者在本地调试时发现能用,部署到生产环境就不行,就是这个头没配。其他通信通道除了 postMessage,还有几个不太常见但特定场景好用的通信方式:MessageChannel:创建一对互相连接的端口,可以传给 Worker 作为私有通道。适合多个 Worker 之间直接通信,不经过主线程中转。const channel = new MessageChannel();worker1.postMessage({ port: channel.port1 }, [channel.port1]);worker2.postMessage({ port: channel.port2 }, [channel.port2]);// 两个 Worker 现在可以直接通信了BroadcastChannel:同源下所有标签页和 Worker 都能收发的广播通道。适合跨标签页同步状态。const bc = new BroadcastChannel('app-sync');bc.postMessage({ type: 'data-updated', payload: newData });bc.onmessage = (e) => { /* 收到其他页面的广播 */ };通信性能的实际影响很多人以为 Worker 通信开销可以忽略,实际上结构化克隆的耗时跟数据量正相关。实测数据:| 数据量 | 结构化克隆耗时 | Transferable 耗时 ||--------|---------------|-------------------|| 10KB | ~0.1ms | ~0.05ms || 1MB | ~5ms | ~0.1ms || 10MB | ~50ms | ~0.2ms || 100MB | ~500ms | ~0.5ms |数据量越大,结构化克隆越慢,Transferable 优势越明显。10MB 以上的数据,不用 Transferable 基本等于白用 Worker——拷贝时间比计算时间还长。实践建议:如果 Worker 间通信频率高(每秒几十次以上),即使单次数据量小,也要考虑 SharedArrayBuffer + Atomics,省掉反复序列化的开销。错误处理别忘了Worker 内部抛出的异常不会冒泡到主线程,必须显式监听:worker.onerror = (e) => { console.error('Worker 出错了:', e.message); console.error('文件:', e.filename, '行号:', e.lineno); // 可以选择重新创建 Worker};// Worker 内部也要处理异常self.onmessage = (e) => { try { const result = riskyOperation(e.data); self.postMessage({ id: e.data.id, result }); } catch (err) { self.postMessage({ id: e.data.id, error: err.message }); }};生产环境里 Worker 挂了不重启,等于你的后台任务全停了。建议封装一个自动重启的 Worker 管理器:onerror 触发后 terminate 旧 Worker,new 一个新的,再把未完成的任务重放一遍。
服务端阅读 02026年5月27日 12:27

Web Worker vs WebAssembly:线程和速度是两码事

一句话搞清楚Web Worker 解决的是"线程"问题——把 JavaScript 搬到后台跑,不卡 UI;WebAssembly 解决的是"速度"问题——让浏览器跑接近原生性能的代码。它俩不是竞争关系,更像是搭档:Worker 出线程,WASM 出算力,加在一起才是完整方案。核心区别| 维度 | Web Worker | WebAssembly ||------|-----------|-------------|| 解决什么问题 | JavaScript 单线程阻塞 | JavaScript 性能天花板 || 运行环境 | 独立线程,仍是 JS 引擎 | 沙箱虚拟机,跑二进制指令 || 语言 | 只能写 JavaScript | C/C++、Rust、Go 等编译而来 || 性能天花板 | 和主线程 JS 一样 | 接近原生(通常快 5-20 倍) || DOM 访问 | 不行,靠 postMessage 中转 | 不行,同样靠 JS 桥接 || 浏览器 API | fetch、IndexedDB、WebSocket 等 | 几乎没有,全靠 JS 胶水代码 || 通信成本 | 结构化克隆(深拷贝),可用 Transferable 零拷贝 | 调用 JS 函数,有上下文切换开销 || 适用场景 | I/O 密集、后台任务、并发处理 | 计算密集、图像/音视频/加密/物理引擎 |简单说:Worker 是多线程方案,WASM 是加速方案。你选哪个,取决于瓶颈在哪——是"主线程太忙"还是"JS 跑得不够快"。什么时候用 Web Worker主线程被卡了,就用 Worker。最常见的信号:页面操作出现明显延迟,Chrome DevTools 的 Performance 面板里看到长任务(Long Tasks)超过 50ms。典型场景:大列表排序/过滤。前端拿到 10 万条数据做客户端筛选,主线程直接冻住。丢给 Worker 后,筛选完把结果 postMessage 回来,UI 全程流畅。文件处理。用户上传 CSV/JSON 大文件,在 Worker 里解析、校验、转换格式,主线程只负责显示进度条。实时数据流。WebSocket 推过来的行情数据,Worker 负责解包、聚合、计算指标,主线程只做渲染。// 主线程const worker = new Worker('data-worker.js');// 大数据丢给 Worker 处理,用 Transferable 避免拷贝开销const buffer = new Float64Array(1_000_000);worker.postMessage({ data: buffer }, [buffer.buffer]);worker.onmessage = (e) => { // 拿到处理结果,更新 UI renderChart(e.data.result);};注意 Transferable Objects 的用法:postMessage 的第二个参数传 [buffer.buffer],数据直接转移所有权而不是拷贝,大数据场景下差距巨大。什么时候用 WebAssemblyJS 算不过来了,就用 WASM。典型信号:计算密集循环在 Profiler 里占了大量时间,而且算法本身已经是 O(n log n) 级别,没法再优化了。典型场景:图像处理。给图片加滤镜、裁剪、缩放,像素级操作在 JS 里慢得感人。用 Rust 或 C 写 WASM 模块,处理速度能提升 5-10 倍。加密/解密。AES-256 加密 100MB 数据,JS 版本可能要好几秒,WASM 版本几百毫秒搞定。物理引擎/游戏。碰撞检测、粒子系统,每帧都要大量浮点运算,WASM 是刚需。音视频编解码。FFmpeg 编译成 WASM 在浏览器里跑,已经是很成熟的方案了。// 加载 WASM 模块const response = await fetch('image-processor.wasm');const { instance } = await WebAssembly.instantiateStreaming(response);// 调用导出函数const imageData = ctx.getImageData(0, 0, width, height);const ptr = instance.exports.process(imageData.data, width, height);WASM 最大的限制是它不能直接调浏览器 API。你需要写 JS 胶水代码(glue code)来桥接,比如 WASM 算完结果后通过共享内存传给 JS,JS 再操作 DOM 或 Canvas。两者结合:Worker 里跑 WASM真正高性能的 Web 应用,往往不是二选一,而是把 WASM 塞进 Worker 里——Worker 解决线程问题,WASM 解决速度问题,各司其职。以浏览器端图像处理为例:// 主线程:只管 UIconst worker = new Worker('wasm-image-worker.js');function processImage(file) { const img = new Image(); img.onload = () => { const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); const imageData = ctx.getImageData(0, 0, img.width, img.height); // 把像素数据转移到 Worker(零拷贝) worker.postMessage({ pixels: imageData.data.buffer, width: img.width, height: img.height }, [imageData.data.buffer]); }; img.src = URL.createObjectURL(file);}worker.onmessage = (e) => { // 拿到处理后的像素,渲染到 Canvas const { pixels, width, height } = e.data; const newImageData = new ImageData(new Uint8ClampedArray(pixels), width, height); ctx.putImageData(newImageData, 0, 0);};// wasm-image-worker.js:加载 WASM + 执行计算let wasm = null;self.onmessage = async (e) => { if (!wasm) { const { instance } = await WebAssembly.instantiateStreaming(fetch('filter.wasm')); wasm = instance.exports; } const { pixels, width, height } = e.data; // 在 WASM 里处理像素 const resultPtr = wasm.applyFilter(pixels, width, height); const result = new Uint8Array(wasm.memory.buffer, resultPtr, width * height * 4); // 结果传回主线程 self.postMessage({ pixels: result.buffer, width, height }, [result.buffer]);};这个架构的好处:主线程零负担,Worker 线程跑 WASM 接近原生速度,数据通过 Transferable 零拷贝传递。三重优化叠加,效果远超单独用任何一种。性能差异有多大实际测一下才有体感。以"100 万元素数组求平方根"为例:| 方案 | 耗时(近似) ||------|-------------|| 主线程 JS | ~500ms(期间 UI 卡死) || Worker + JS | ~500ms(UI 不卡,但计算一样慢) || Worker + WASM | ~50ms(UI 不卡,计算快 10 倍) |数据来源:Chrome 120,M1 MacBook Pro,具体数值因硬件和算法而异,但量级关系稳定。关键点:Worker 不加速计算,只解放主线程;WASM 才是加速计算的。如果你把慢代码移到 Worker 里,它还是一样慢,只是不卡 UI 了。要真正快,得用 WASM。选择决策别纠结,按这个思路来:主线程卡不卡? 卡 → 上 WorkerWorker 里的计算够不够快? 不够 → 把热点函数编译成 WASM两者都不需要? 那就别用,引入 Worker 有通信开销,WASM 有编译和加载成本一个常见的误区是"用了 Worker 就快了"——不是的,Worker 只是换个地方跑,JS 该慢还是慢。另一个误区是"WASM 能替代 JS"——也不是,WASM 搞不了 DOM、调不了 fetch、处理不了事件循环,离了 JS 胶水代码它寸步难行。选对工具,别选"更高级"的。
前端阅读 02月21日 15:44

OffscreenCanvas 如何在 Web Worker 中进行渲染?

OffscreenCanvas 是 HTML5 提供的一个功能,允许在 Web Worker 中进行 Canvas 渲染,从而将复杂的图形计算从主线程移到后台线程。OffscreenCanvas 的核心概念特点可以在 Worker 中进行 Canvas 绘图操作支持大部分 Canvas 2D API 和 WebGL API通过 transferControlToOffscreen() 方法将 Canvas 控制权转移适用于复杂的图形渲染和动画基本使用主线程设置// 获取 Canvas 元素const canvas = document.getElementById('myCanvas');// 将 Canvas 控制权转移到 OffscreenCanvasconst offscreen = canvas.transferControlToOffscreen();// 创建 Workerconst worker = new Worker('canvas-worker.js');// 将 OffscreenCanvas 发送给 Workerworker.postMessage({ canvas: offscreen }, [offscreen]);Worker 中渲染// canvas-worker.jsself.onmessage = function(e) { const canvas = e.data.canvas; const ctx = canvas.getContext('2d'); // 在 Worker 中进行绘图 function render() { ctx.clearRect(0, 0, canvas.width, canvas.height); // 绘制图形 ctx.fillStyle = 'blue'; ctx.fillRect(50, 50, 100, 100); // 继续动画 requestAnimationFrame(render); } render();};实际应用场景1. 复杂动画渲染// 主线程const canvas = document.getElementById('canvas');const offscreen = canvas.transferControlToOffscreen();const worker = new Worker('animation-worker.js');worker.postMessage({ canvas: offscreen }, [offscreen]);// animation-worker.jsself.onmessage = function(e) { const canvas = e.data.canvas; const ctx = canvas.getContext('2d'); let particles = []; function initParticles() { for (let i = 0; i < 1000; i++) { particles.push({ x: Math.random() * canvas.width, y: Math.random() * canvas.height, vx: (Math.random() - 0.5) * 2, vy: (Math.random() - 0.5) * 2, size: Math.random() * 3 + 1 }); } } function updateParticles() { particles.forEach(p => { p.x += p.vx; p.y += p.vy; if (p.x < 0 || p.x > canvas.width) p.vx *= -1; if (p.y < 0 || p.y > canvas.height) p.vy *= -1; }); } function render() { ctx.clearRect(0, 0, canvas.width, canvas.height); particles.forEach(p => { ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fillStyle = `rgba(100, 150, 255, 0.7)`; ctx.fill(); }); updateParticles(); requestAnimationFrame(render); } initParticles(); render();};2. 图像处理// 主线程const canvas = document.getElementById('canvas');const offscreen = canvas.transferControlToOffscreen();const worker = new Worker('image-worker.js');// 加载图像const img = new Image();img.onload = function() { worker.postMessage({ canvas: offscreen, image: img }, [offscreen]);};img.src = 'image.jpg';// image-worker.jsself.onmessage = function(e) { const canvas = e.data.canvas; const ctx = canvas.getContext('2d'); const img = e.data.image; // 设置 Canvas 大小 canvas.width = img.width; canvas.height = img.height; // 绘制原始图像 ctx.drawImage(img, 0, 0); // 获取图像数据 const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; // 图像处理:灰度化 for (let i = 0; i < data.length; i += 4) { const avg = (data[i] + data[i + 1] + data[i + 2]) / 3; data[i] = avg; // R data[i + 1] = avg; // G data[i + 2] = avg; // B } // 放回处理后的图像 ctx.putImageData(imageData, 0, 0);};3. WebGL 渲染// 主线程const canvas = document.getElementById('glCanvas');const offscreen = canvas.transferControlToOffscreen();const worker = new Worker('webgl-worker.js');worker.postMessage({ canvas: offscreen }, [offscreen]);// webgl-worker.jsself.onmessage = function(e) { const canvas = e.data.canvas; const gl = canvas.getContext('webgl'); // WebGL 初始化代码 const vertexShaderSource = ` attribute vec4 aVertexPosition; void main() { gl_Position = aVertexPosition; } `; const fragmentShaderSource = ` void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); } `; // 编译着色器 const vertexShader = compileShader(gl, gl.VERTEX_SHADER, vertexShaderSource); const fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); // 创建程序 const shaderProgram = gl.createProgram(); gl.attachShader(shaderProgram, vertexShader); gl.attachShader(shaderProgram, fragmentShader); gl.linkProgram(shaderProgram); // 渲染 function render() { gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.clear(gl.COLOR_BUFFER_BIT); gl.useProgram(shaderProgram); gl.drawArrays(gl.TRIANGLES, 0, 3); requestAnimationFrame(render); } render();};function compileShader(gl, type, source) { const shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); return shader;}与主线程交互动态调整 Canvas 大小// 主线程const canvas = document.getElementById('canvas');const offscreen = canvas.transferControlToOffscreen();const worker = new Worker('canvas-worker.js');worker.postMessage({ canvas: offscreen }, [offscreen]);// 监听窗口大小变化window.addEventListener('resize', function() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; worker.postMessage({ type: 'resize', width: canvas.width, height: canvas.height });});// canvas-worker.jsself.onmessage = function(e) { if (e.data.type === 'resize') { canvas.width = e.data.width; canvas.height = e.data.height; }};接收用户输入// 主线程const canvas = document.getElementById('canvas');const offscreen = canvas.transferControlToOffscreen();const worker = new Worker('canvas-worker.js');worker.postMessage({ canvas: offscreen }, [offscreen]);// 发送鼠标位置canvas.addEventListener('mousemove', function(e) { const rect = canvas.getBoundingClientRect(); worker.postMessage({ type: 'mousemove', x: e.clientX - rect.left, y: e.clientY - rect.top });});// canvas-worker.jslet mouseX = 0, mouseY = 0;self.onmessage = function(e) { if (e.data.type === 'mousemove') { mouseX = e.data.x; mouseY = e.data.y; }};注意事项1. Canvas 控制权只能转移一次// ❌ 错误:多次转移const offscreen1 = canvas.transferControlToOffscreen();const offscreen2 = canvas.transferControlToOffscreen(); // 报错// ✅ 正确:只转移一次const offscreen = canvas.transferControlToOffscreen();2. OffscreenCanvas 不支持所有 Canvas API// ❌ 不支持canvas.toDataURL(); // 在 Worker 中不可用canvas.toBlob(); // 在 Worker 中不可用// ✅ 使用 ImageBitmap 代替const bitmap = await createImageBitmap(canvas);3. 浏览器兼容性// 检查浏览器支持if ('transferControlToOffscreen' in HTMLCanvasElement.prototype) { // 支持 OffscreenCanvas} else { // 不支持,使用回退方案}性能优化1. 批量绘制// ❌ 频繁调用绘制方法for (let i = 0; i < 1000; i++) { ctx.beginPath(); ctx.arc(particles[i].x, particles[i].y, particles[i].size, 0, Math.PI * 2); ctx.fill();}// ✅ 批量绘制ctx.beginPath();for (let i = 0; i < 1000; i++) { ctx.moveTo(particles[i].x, particles[i].y); ctx.arc(particles[i].x, particles[i].y, particles[i].size, 0, Math.PI * 2);}ctx.fill();2. 使用 ImageBitmap// 加载图像为 ImageBitmapconst bitmap = await createImageBitmap(image);// 在 Worker 中绘制ctx.drawImage(bitmap, 0, 0);3. 降低渲染频率let lastRenderTime = 0;const targetFPS = 30;const frameInterval = 1000 / targetFPS;function render(timestamp) { if (timestamp - lastRenderTime >= frameInterval) { // 执行渲染 ctx.clearRect(0, 0, canvas.width, canvas.height); // ... 绘制代码 lastRenderTime = timestamp; } requestAnimationFrame(render);}最佳实践复杂渲染使用 OffscreenCanvas:将计算密集型图形渲染移到 Worker合理控制渲染频率:避免不必要的重绘批量处理:减少绘制调用次数使用 ImageBitmap:提高图像加载和渲染性能检查浏览器兼容性:提供回退方案及时释放资源:使用完毕后清理资源
服务端阅读 02月21日 15:24

Service Worker 的生命周期是什么,如何实现离线缓存?

Service Worker 是一种特殊的 Web Worker,它作为网络代理运行在浏览器和服务器之间,提供离线功能、推送通知和后台同步等能力。Service Worker 的核心概念特点独立于页面生命周期运行拦截和处理网络请求必须在 HTTPS 环境下运行(localhost 除外)可以实现离线缓存和资源预加载支持推送通知和后台同步注册 Service Worker// 检查浏览器支持if ('serviceWorker' in navigator) { // 注册 Service Worker navigator.serviceWorker.register('/service-worker.js') .then(function(registration) { console.log('Service Worker 注册成功:', registration.scope); }) .catch(function(error) { console.log('Service Worker 注册失败:', error); });}Service Worker 生命周期1. Install(安装)// service-worker.jsconst CACHE_NAME = 'my-cache-v1';const urlsToCache = [ '/', '/styles/main.css', '/script/main.js', '/images/logo.png'];self.addEventListener('install', function(event) { // event.waitUntil 延迟安装完成 event.waitUntil( caches.open(CACHE_NAME) .then(function(cache) { console.log('已打开缓存'); return cache.addAll(urlsToCache); }) ); // 立即激活新的 Service Worker self.skipWaiting();});2. Activate(激活)self.addEventListener('activate', function(event) { event.waitUntil( caches.keys().then(function(cacheNames) { return Promise.all( cacheNames.map(function(cacheName) { // 删除旧版本的缓存 if (cacheName !== CACHE_NAME) { console.log('删除旧缓存:', cacheName); return caches.delete(cacheName); } }) ); }) ); // 立即控制所有客户端 self.clients.claim();});3. Fetch(拦截请求)self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request) .then(function(response) { // 缓存命中,返回缓存 if (response) { return response; } // 缓存未命中,发起网络请求 return fetch(event.request).then( function(response) { // 检查是否为有效响应 if (!response || response.status !== 200 || response.type !== 'basic') { return response; } // 克隆响应(响应流只能使用一次) const responseToCache = response.clone(); caches.open(CACHE_NAME) .then(function(cache) { cache.put(event.request, responseToCache); }); return response; } ); }) );});缓存策略1. Cache First(缓存优先)self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request) .then(function(response) { return response || fetch(event.request); }) );});2. Network First(网络优先)self.addEventListener('fetch', function(event) { event.respondWith( fetch(event.request) .then(function(response) { const responseToCache = response.clone(); caches.open(CACHE_NAME).then(function(cache) { cache.put(event.request, responseToCache); }); return response; }) .catch(function() { return caches.match(event.request); }) );});3. Network Only(仅网络)self.addEventListener('fetch', function(event) { event.respondWith(fetch(event.request));});4. Cache Only(仅缓存)self.addEventListener('fetch', function(event) { event.respondWith(caches.match(event.request));});5. Stale While Revalidate(缓存优先,后台更新)self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request).then(function(cachedResponse) { const fetchPromise = fetch(event.request).then(function(networkResponse) { caches.open(CACHE_NAME).then(function(cache) { cache.put(event.request, networkResponse.clone()); }); return networkResponse; }); return cachedResponse || fetchPromise; }) );});推送通知订阅推送// 主线程function subscribeUser() { return navigator.serviceWorker.register('/service-worker.js') .then(function(registration) { const subscribeOptions = { userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array( 'BEl62iUYgUivxIkv69yViEuiBIa-IbRMhKDbjjVdMlzQJd0_...' ) }; return registration.pushManager.subscribe(subscribeOptions); }) .then(function(pushSubscription) { console.log('已接收推送订阅:', pushSubscription); return pushSubscription; });}处理推送消息// service-worker.jsself.addEventListener('push', function(event) { const options = { body: event.data ? event.data.text() : '新消息', icon: '/images/icon.png', badge: '/images/badge.png', vibrate: [100, 50, 100], data: { dateOfArrival: Date.now(), primaryKey: 1 } }; event.waitUntil( self.registration.showNotification('推送通知', options) );});// 处理通知点击self.addEventListener('notificationclick', function(event) { event.notification.close(); event.waitUntil( clients.openWindow('/') );});后台同步注册同步事件// 主线程navigator.serviceWorker.ready.then(function(registration) { registration.sync.register('sync-messages');});处理同步事件// service-worker.jsself.addEventListener('sync', function(event) { if (event.tag === 'sync-messages') { event.waitUntil(syncMessages()); }});function syncMessages() { return fetch('/api/sync-messages', { method: 'POST', body: JSON.stringify(getPendingMessages()) });}调试 Service WorkerChrome DevTools打开 Chrome DevTools切换到 "Application" 面板左侧选择 "Service Workers"可以查看状态、更新、注销等操作更新 Service Worker// 手动更新navigator.serviceWorker.ready.then(function(registration) { registration.update();});// 监听更新navigator.serviceWorker.addEventListener('controllerchange', function() { console.log('Service Worker 已更新'); window.location.reload();});最佳实践HTTPS 要求:生产环境必须使用 HTTPS缓存版本管理:使用版本号管理缓存错误处理:添加完善的错误处理渐进增强:确保在不支持 Service Worker 的浏览器中也能正常工作资源清理:及时清理旧版本的缓存性能优化:合理选择缓存策略安全性:验证请求来源,防止 CSRF 攻击