ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

说明 Worker 池内存优化

说明 Worker 池内存优化 Worker 池而不是单 Worker最多 2 个 Worker 较稳 2个会不会爆内存。 去掉大 PNG dataURL 中转 是什么意思不是已经没有png 图片了吗这些都仔细说明把重着色底图改成 Blob/ImageBitmap/ArrayBuffer 传给 Worker避免 20MB base64 字符串 对小白仔细说明并列出代码可以逐个拆开说。1. “最多 2 个 Worker”会不会爆内存有可能但比“开很多 Worker”稳得多。关键不是 Worker 数量本身而是每个 Worker 里同时拿着多大的图片数据。一个 Worker 做重着色时通常会占这些内存原图像素 RGBA 内存 宽 * 高 * 4 bytes 例如 3000 x 3000 9,000,000 * 4 ≈ 36MB如果过程中还有原始图 ImageBitmap OffscreenCanvas 像素缓冲 getImageData 返回的 Uint8ClampedArray 输出 Blob / ArrayBuffer那单个任务可能临时吃掉几十 MB 到一两百 MB。所以“最多 2 个 Worker”的意思是同时最多只让 2 张底图在后台处理 其他任务排队不是创建无限 Worker也不是每张图都新开 Worker。更稳的策略const workerCount Math.min( 2, navigator.hardwareConcurrency || 1 );如果想更保守低内存设备只开 1 个const memoryGB navigator.deviceMemory || 4; const workerCount memoryGB 4 ? 1 : Math.min(2, navigator.hardwareConcurrency || 1);结论2 个 Worker 不一定爆内存但要配合任务排队、复用 Worker、处理完释放大对象。移动端或低配机器建议自动降到 1 个。2. “去掉大 PNG dataURL 中转”是什么意思不是已经没有 PNG 图片了吗这里说的“PNG dataURL”不一定是你项目里有一个 .png 文件。它可能是代码运行时临时生成的比如const dataUrl canvas.toDataURL(image/png);这个东西长这样data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...也就是说Canvas / 图片 / SVG / 任意图像 ↓ 转成 PNG 编码 ↓ 再转成 base64 字符串 ↓ 作为 dataURL 传来传去所以即使你“没有 PNG 文件”只要代码里用了canvas.toDataURL(image/png)或者类似逻辑它还是会在内存里造出一个“PNG dataURL”。3. 为什么 20MB base64 字符串很糟假设真正的图片二进制是 20MB。base64 会膨胀大约 33%20MB 二进制 → 约 26.6MB base64 文本但 JS 字符串通常还可能按 UTF-16 存储大约 2 bytes 一个字符26.6MB 文本 → 可能接近 53MB JS 字符串内存更糟的是中间经常会产生多份拷贝canvas 像素内存 PNG 编码结果 base64 字符串 postMessage 拷贝 Worker 里再解码 Worker 里的 canvas / imageData所以你看到的“20MB base64 字符串”实际峰值内存可能远不止 20MB。4. 更好的做法是什么不要这样const dataUrl canvas.toDataURL(image/png); worker.postMessage({ image: dataUrl });改成传这些Blob ImageBitmap ArrayBuffer推荐优先级ImageBitmap适合传给 Worker 直接画到 OffscreenCanvas ArrayBuffer适合传原始二进制可 transfer避免复制 Blob适合保存/传递图片文件数据通常比 dataURL 轻5. 推荐方案主线程把底图转成 ImageBitmap传给 Worker主线程const worker new Worker(/workers/recolor-worker.js, { type: module }); async function recolorBaseImage(imageUrl, color) { const response await fetch(imageUrl); const blob await response.blob(); const imageBitmap await createImageBitmap(blob); return new Promise((resolve, reject) { const id crypto.randomUUID(); worker.onmessage (event) { const { id: resultId, blob, error } event.data; if (resultId ! id) return; if (error) { reject(new Error(error)); return; } resolve(blob); }; worker.postMessage( { id, imageBitmap, color }, [imageBitmap] ); }); }注意这一句worker.postMessage(message, [imageBitmap]);第二个参数 [imageBitmap] 表示“转移所有权”。简单理解不是复制一份图片给 Worker 而是把这块图像资源交给 Worker 主线程这边不再持有它这对内存更友好。Worker 里self.onmessage async (event) { const { id, imageBitmap, color } event.data; try { const canvas new OffscreenCanvas( imageBitmap.width, imageBitmap.height ); const ctx canvas.getContext(2d, { willReadFrequently: true }); ctx.drawImage(imageBitmap, 0, 0); imageBitmap.close(); const imageData ctx.getImageData( 0, 0, canvas.width, canvas.height ); const data imageData.data; const target hexToRgb(color); for (let i 0; i data.length; i 4) { const alpha data[i 3]; if (alpha 0) continue; const gray data[i] * 0.299 data[i 1] * 0.587 data[i 2] * 0.114; const strength gray / 255; data[i] target.r * strength; data[i 1] target.g * strength; data[i 2] target.b * strength; } ctx.putImageData(imageData, 0, 0); const blob await canvas.convertToBlob({ type: image/webp, quality: 0.92 }); self.postMessage({ id, blob }); } catch (error) { self.postMessage({ id, error: error.message }); } }; function hexToRgb(hex) { const value hex.replace(#, ); return { r: parseInt(value.slice(0, 2), 16), g: parseInt(value.slice(2, 4), 16), b: parseInt(value.slice(4, 6), 16) }; }这里没有 toDataURL(image/png)输出是canvas.convertToBlob(...)这比 base64 dataURL 友好很多。6. 如果要 Worker 池代码长这样主线程 Worker 池class WorkerPool { constructor(workerUrl, size) { this.workers Array.from({ length: size }, () ({ worker: new Worker(workerUrl, { type: module }), busy: false })); this.queue []; } run(payload, transferList []) { return new Promise((resolve, reject) { this.queue.push({ payload, transferList, resolve, reject }); this.schedule(); }); } schedule() { const idle this.workers.find((item) !item.busy); const task this.queue.shift(); if (!idle || !task) return; idle.busy true; const id crypto.randomUUID(); const handleMessage (event) { if (event.data.id ! id) return; idle.worker.removeEventListener(message, handleMessage); idle.worker.removeEventListener(error, handleError); idle.busy false; if (event.data.error) { task.reject(new Error(event.data.error)); } else { task.resolve(event.data); } this.schedule(); }; const handleError (error) { idle.worker.removeEventListener(message, handleMessage); idle.worker.removeEventListener(error, handleError); idle.busy false; task.reject(error); this.schedule(); }; idle.worker.addEventListener(message, handleMessage); idle.worker.addEventListener(error, handleError); idle.worker.postMessage( { id, ...task.payload }, task.transferList ); } }使用const memoryGB navigator.deviceMemory || 4; const workerCount memoryGB 4 ? 1 : Math.min(2, navigator.hardwareConcurrency || 1); const recolorPool new WorkerPool( /workers/recolor-worker.js, workerCount ); async function recolor(imageUrl, color) { const response await fetch(imageUrl); const blob await response.blob(); const imageBitmap await createImageBitmap(blob); const result await recolorPool.run( { imageBitmap, color }, [imageBitmap] ); return result.blob; }7. 用 ArrayBuffer 传也可以主线程async function recolorByArrayBuffer(imageUrl, color) { const response await fetch(imageUrl); const buffer await response.arrayBuffer(); return recolorPool.run( { buffer, mimeType: response.headers.get(content-type) || image/png, color }, [buffer] ); }Workerself.onmessage async (event) { const { id, buffer, mimeType, color } event.data; try { const blob new Blob([buffer], { type: mimeType }); const imageBitmap await createImageBitmap(blob); // 后面逻辑和前面的 ImageBitmap 版本一样 } catch (error) { self.postMessage({ id, error: error.message }); } };ArrayBuffer 的好处是可以 transferworker.postMessage({ buffer }, [buffer]);这表示这块二进制内存从主线程转交给 Worker避免复制。一句话总结“去掉大 PNG dataURL 中转”不是说项目里不能有 PNG 文件而是说不要把图片通过 canvas.toDataURL(image/png) 变成巨大的 base64 字符串再传给 Worker。更稳的方式是主线程拿到 Blob / ArrayBuffer / ImageBitmap用 postMessage 的 transfer list 交给 WorkerWorker 里用 OffscreenCanvas 处理最后用 convertToBlob() 输出。Worker 池最多 2 个一般是稳定上限低内存设备自动降到 1 个更安全。
返回列表