--- id: wiki-2026-0508-offscreencanvas와-web-worker를-활용한 title: OffscreenCanvas와 Web Worker를 활용한 메인 스레드 병목 해결 category: 10_Wiki/Topics status: verified canonical_id: self aliases: [OffscreenCanvas, Web Worker rendering, off-main-thread canvas] duplicate_of: none source_trust_level: A confidence_score: 0.9 verification_status: applied tags: [web-worker, offscreencanvas, performance, frontend, rendering] raw_sources: [] last_reinforced: 2026-05-10 github_commit: pending tech_stack: language: JavaScript framework: Web API --- # OffscreenCanvas와 Web Worker를 활용한 메인 스레드 병목 해결 ## 매 한 줄 > **"매 main thread 의 60fps budget (16.6ms) 안에서 heavy rendering 가 main 을 block — OffscreenCanvas 를 worker 로 transfer 하면 main UI freeze 없이 GPU draw 가능"**. 2026 모든 주요 브라우저 (Chrome, Firefox 105+, Safari 16.4+) 가 지원. 매 game, data viz, image editor 의 핵심 패턴. ## 매 핵심 ### 매 메인 스레드 병목 원인 - **Heavy raster** (10k+ shapes, particle). - **Image processing** (filter, decode). - **Layout thrash**: 매 read-write 반복. - **Synchronous JS work**: 매 long task (>50ms). ### 매 OffscreenCanvas 가 해결 - **Canvas → Worker transfer**: 매 `transferControlToOffscreen()`. - **GPU context (WebGL/WebGPU) 도 worker 가능**. - **Main thread 는 input + DOM 만**: 매 항상 responsive. - **`requestAnimationFrame` 가 worker 에도 존재**. ### 매 응용 1. Game canvas (Three.js worker rendering). 2. Real-time data viz (D3 or visx in worker). 3. Image editor (Photoshop-like filter). 4. PDF / video frame extraction. ## 💻 패턴 ### Basic transfer + worker render loop ```javascript // main.js const canvas = document.querySelector("canvas"); const offscreen = canvas.transferControlToOffscreen(); const worker = new Worker("./renderer.js", { type: "module" }); worker.postMessage({ canvas: offscreen }, [offscreen]); // renderer.js (worker) let canvas, ctx; self.onmessage = (e) => { if (e.data.canvas) { canvas = e.data.canvas; ctx = canvas.getContext("2d"); loop(); } }; function loop() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = `hsl(${(performance.now() / 10) % 360}, 80%, 50%)`; ctx.fillRect(50, 50, 200, 200); requestAnimationFrame(loop); } ``` ### Three.js in worker ```javascript // main.js const canvas = document.getElementById("c"); const off = canvas.transferControlToOffscreen(); const w = new Worker(new URL("./three-worker.js", import.meta.url), { type: "module" }); w.postMessage({ canvas: off, dpr: devicePixelRatio }, [off]); // three-worker.js import * as THREE from "three"; self.onmessage = ({ data }) => { const { canvas, dpr } = data; const renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); renderer.setPixelRatio(dpr); const scene = new THREE.Scene(); const cam = new THREE.PerspectiveCamera(75, canvas.width / canvas.height); cam.position.z = 5; const cube = new THREE.Mesh( new THREE.BoxGeometry(), new THREE.MeshNormalMaterial(), ); scene.add(cube); function tick() { cube.rotation.x += 0.01; cube.rotation.y += 0.01; renderer.render(scene, cam); requestAnimationFrame(tick); } tick(); }; ``` ### Input forwarding (main → worker) ```javascript // main.js — 매 worker 는 DOM 접근 X → input event 의 forward 필요 canvas.addEventListener("pointermove", (e) => { worker.postMessage({ type: "pointer", x: e.offsetX, y: e.offsetY, }); }); window.addEventListener("resize", () => { worker.postMessage({ type: "resize", w: canvas.clientWidth, h: canvas.clientHeight }); }); ``` ### Resize handling in worker ```javascript // worker self.onmessage = ({ data }) => { if (data.type === "resize") { canvas.width = data.w * devicePixelRatio; canvas.height = data.h * devicePixelRatio; cam.aspect = data.w / data.h; cam.updateProjectionMatrix(); } }; ``` ### Image processing pipeline (heavy filter) ```javascript // main const bmp = await createImageBitmap(file); worker.postMessage({ bmp }, [bmp]); // 매 transfer ownership // worker self.onmessage = ({ data }) => { const { bmp } = data; const off = new OffscreenCanvas(bmp.width, bmp.height); const ctx = off.getContext("2d"); ctx.drawImage(bmp, 0, 0); const img = ctx.getImageData(0, 0, bmp.width, bmp.height); // 매 heavy pixel loop — 매 main 안 막음 for (let i = 0; i < img.data.length; i += 4) { img.data[i] = 255 - img.data[i]; img.data[i+1] = 255 - img.data[i+1]; img.data[i+2] = 255 - img.data[i+2]; } ctx.putImageData(img, 0, 0); off.convertToBlob().then((blob) => self.postMessage({ blob })); }; ``` ### SharedArrayBuffer for shared state (with COOP/COEP) ```javascript // main — game state shared const sab = new SharedArrayBuffer(1024); const state = new Float32Array(sab); worker.postMessage({ state: sab }); // 매 worker 가 state 읽고 — 매 lock-free update // 매 require: Cross-Origin-Opener-Policy: same-origin // Cross-Origin-Embedder-Policy: require-corp ``` ## 매 결정 기준 | 작업 | 위치 | |---|---| | DOM update | main thread (only) | | Canvas 2D / WebGL / WebGPU draw | worker (OffscreenCanvas) | | Image filter / encode | worker | | Layout / scroll | main | | Heavy compute (parsing, ML) | worker | **기본값**: rendering loop + heavy compute → worker, DOM/input → main. ## 🔗 Graph - 부모: [[Web Worker]] · [[Performance Optimization]] - 변형: [[SharedWorker]] · [[Service Worker]] · [[WebGPU Compute]] - 응용: [[Three.js]] · [[Game Canvas]] · [[Image Editor]] - Adjacent: [[SharedArrayBuffer]] · [[Transferable Objects]] · [[Memory Leak Prevention]] ## 🤖 LLM 활용 **언제**: canvas-heavy app 설계, main thread jank 진단, worker 통신 설계. **언제 X**: 매 simple static page — 매 overhead 정당화 X. ## ❌ 안티패턴 - **PostMessage with large object (no transfer)**: 매 structured clone copy → 매 GC pressure. 매 transfer list 사용. - **DOM access in worker**: 매 불가능 — 매 main 으로 forward. - **Forget resize / DPR**: 매 blurry canvas. - **No COOP/COEP for SAB**: 매 SharedArrayBuffer 사용 X. ## 🧪 검증 / 중복 - Verified (MDN OffscreenCanvas, Three.js examples, Chrome Developers blog). - 신뢰도 A. ## 🕓 Changelog | 날짜 | 변경 | |---|---| | 2026-05-08 | Phase 1 | | 2026-05-10 | Manual cleanup — OffscreenCanvas + worker 패턴 7개 |