OffscreenCanvas is a feature provided by HTML5 that allows Canvas rendering in Web Workers, moving complex graphics calculations from the main thread to background threads.
Core Concepts of OffscreenCanvas
Characteristics
- Can perform Canvas drawing operations in Worker
- Supports most Canvas 2D API and WebGL API
- Transfers Canvas control through
transferControlToOffscreen()method - Suitable for complex graphics rendering and animations
Basic Usage
Main Thread Setup
javascript// Get Canvas element const canvas = document.getElementById('myCanvas'); // Transfer Canvas control to OffscreenCanvas const offscreen = canvas.transferControlToOffscreen(); // Create Worker const worker = new Worker('canvas-worker.js'); // Send OffscreenCanvas to Worker worker.postMessage({ canvas: offscreen }, [offscreen]);
Rendering in Worker
javascript// canvas-worker.js self.onmessage = function(e) { const canvas = e.data.canvas; const ctx = canvas.getContext('2d'); // Draw in Worker function render() { ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw graphics ctx.fillStyle = 'blue'; ctx.fillRect(50, 50, 100, 100); // Continue animation requestAnimationFrame(render); } render(); };
Practical Use Cases
1. Complex Animation Rendering
javascript// Main thread const canvas = document.getElementById('canvas'); const offscreen = canvas.transferControlToOffscreen(); const worker = new Worker('animation-worker.js'); worker.postMessage({ canvas: offscreen }, [offscreen]); // animation-worker.js self.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. Image Processing
javascript// Main thread const canvas = document.getElementById('canvas'); const offscreen = canvas.transferControlToOffscreen(); const worker = new Worker('image-worker.js'); // Load image const img = new Image(); img.onload = function() { worker.postMessage({ canvas: offscreen, image: img }, [offscreen]); }; img.src = 'image.jpg'; // image-worker.js self.onmessage = function(e) { const canvas = e.data.canvas; const ctx = canvas.getContext('2d'); const img = e.data.image; // Set Canvas size canvas.width = img.width; canvas.height = img.height; // Draw original image ctx.drawImage(img, 0, 0); // Get image data const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; // Image processing: grayscale 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 } // Put back processed image ctx.putImageData(imageData, 0, 0); };
3. WebGL Rendering
javascript// Main thread const canvas = document.getElementById('glCanvas'); const offscreen = canvas.transferControlToOffscreen(); const worker = new Worker('webgl-worker.js'); worker.postMessage({ canvas: offscreen }, [offscreen]); // webgl-worker.js self.onmessage = function(e) { const canvas = e.data.canvas; const gl = canvas.getContext('webgl'); // WebGL initialization code 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); } `; // Compile shaders const vertexShader = compileShader(gl, gl.VERTEX_SHADER, vertexShaderSource); const fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); // Create program const shaderProgram = gl.createProgram(); gl.attachShader(shaderProgram, vertexShader); gl.attachShader(shaderProgram, fragmentShader); gl.linkProgram(shaderProgram); // Render 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; }
Interacting with Main Thread
Dynamically Adjust Canvas Size
javascript// Main thread const canvas = document.getElementById('canvas'); const offscreen = canvas.transferControlToOffscreen(); const worker = new Worker('canvas-worker.js'); worker.postMessage({ canvas: offscreen }, [offscreen]); // Listen for window resize window.addEventListener('resize', function() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; worker.postMessage({ type: 'resize', width: canvas.width, height: canvas.height }); }); // canvas-worker.js self.onmessage = function(e) { if (e.data.type === 'resize') { canvas.width = e.data.width; canvas.height = e.data.height; } };
Receive User Input
javascript// Main thread const canvas = document.getElementById('canvas'); const offscreen = canvas.transferControlToOffscreen(); const worker = new Worker('canvas-worker.js'); worker.postMessage({ canvas: offscreen }, [offscreen]); // Send mouse position 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.js let mouseX = 0, mouseY = 0; self.onmessage = function(e) { if (e.data.type === 'mousemove') { mouseX = e.data.x; mouseY = e.data.y; } };
Important Notes
1. Canvas Control Can Only Be Transferred Once
javascript// ❌ Error: Multiple transfers const offscreen1 = canvas.transferControlToOffscreen(); const offscreen2 = canvas.transferControlToOffscreen(); // Error // ✅ Correct: Transfer only once const offscreen = canvas.transferControlToOffscreen();
2. OffscreenCanvas Does Not Support All Canvas APIs
javascript// ❌ Not supported canvas.toDataURL(); // Not available in Worker canvas.toBlob(); // Not available in Worker // ✅ Use ImageBitmap instead const bitmap = await createImageBitmap(canvas);
3. Browser Compatibility
javascript// Check browser support if ('transferControlToOffscreen' in HTMLCanvasElement.prototype) { // OffscreenCanvas supported } else { // Not supported, use fallback }
Performance Optimization
1. Batch Drawing
javascript// ❌ Frequent drawing method calls 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(); } // ✅ Batch drawing 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. Use ImageBitmap
javascript// Load image as ImageBitmap const bitmap = await createImageBitmap(image); // Draw in Worker ctx.drawImage(bitmap, 0, 0);
3. Reduce Rendering Frequency
javascriptlet lastRenderTime = 0; const targetFPS = 30; const frameInterval = 1000 / targetFPS; function render(timestamp) { if (timestamp - lastRenderTime >= frameInterval) { // Perform rendering ctx.clearRect(0, 0, canvas.width, canvas.height); // ... drawing code lastRenderTime = timestamp; } requestAnimationFrame(render); }
Best Practices
- Use OffscreenCanvas for Complex Rendering: Move compute-intensive graphics rendering to Worker
- Control Rendering Frequency Reasonably: Avoid unnecessary redraws
- Batch Processing: Reduce number of drawing calls
- Use ImageBitmap: Improve image loading and rendering performance
- Check Browser Compatibility: Provide fallback solutions
- Release Resources Timely: Clean up resources after use