Привет, Хабр! Сегодня разберёмся с тем, что такое OffscreenCanvas, зачем он нужен и как правильно его использовать.
OffscreenCanvas — это API, которое позволяет рендерить графику в отдельном потоке Worker, не блокируя основной поток, где обрабатывается интерфейс.
Технически это достигается за счёт разделения UI и вычислений:
-
Canvas вне DOM
OffscreenCanvas работает независимо от DOM. У него нет прямого визуального представления в интерфейсе. Это «виртуальный холст», который не участвует в браузерном рендеринге. -
Рендеринг в Worker
Основная фича — рендеринг может выполняться в Web Worker. Браузерный поток UI остается свободным для обработки событий, скролла и других пользовательских взаимодействий. -
Поддержка разных API
OffscreenCanvas поддерживает стандартные API:-
2d
для работы с 2D-графикой; -
webgl
/webgl2
для 3D-рендеринга; -
bitmaprenderer
для прямого отображения растровых изображений.
-
-
Оптимизация многопоточности
За счёт использованияtransferControlToOffscreen
, холст передаётся в Worker как объект с передачей владения (transferable object). Это не копия, а прямой перенос ссылки на объект.
На момент написания статьи OffscreenCanvas поддерживается в современных версиях Chrome, Edge, Firefox и частично в других браузерах. Safari с версии 16.4 поддерживает OffscreenCanvas с контекстом «2d», а начиная с версии 17.0 — и с контекстом WebGL. Однако в некоторых источниках можно увидеть информацию о проблемах с доступом к OffscreenCanvas в Safari Technology Preview.
А теперь подключим OffscreenCanvas на примере.
Подключаем OffscreenCanvas
Начнем с HTML. Будет обычный <canvas>
.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>OffscreenCanvas Example</title> </head> <body> <canvas id="mainCanvas" width="800" height="600"></canvas> <script src="main.js"></script> </body> </html>
Теперь переходим к JavaScript. Главная задача на этом этапе — создать OffscreenCanvas и передать его в Web Worker.
// main.js const canvas = document.getElementById('mainCanvas'); // Создаем Web Worker const worker = new Worker('worker.js'); // Передаем управление холстом OffscreenCanvas в Worker const offscreen = canvas.transferControlToOffscreen(); worker.postMessage({ canvas: offscreen }, [offscreen]);
transferControlToOffscreen()
превращает<canvas>
в объект OffscreenCanvas и передает его в Worker. После этого сам <canvas>
на странице больше недоступен для рисования.
Теперь переходим к логике Worker’а. Будем анимировать частицы:
// worker.js self.onmessage = function (event) { const canvas = event.data.canvas; const ctx = canvas.getContext('2d'); // Настраиваем размеры холста canvas.width = 800; canvas.height = 600; const particles = []; // Генерация частиц 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, color: `hsl(${Math.random() * 360}, 100%, 50%)` }); } // Основной цикл отрисовки function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); for (const particle of particles) { ctx.beginPath(); ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2); ctx.fillStyle = particle.color; ctx.fill(); // Обновление позиции частицы particle.x += particle.vx; particle.y += particle.vy; // Проверка выхода за границы if (particle.x < 0 || particle.x > canvas.width) particle.vx *= -1; if (particle.y < 0 || particle.y > canvas.height) particle.vy *= -1; } // Рекурсивный вызов для плавной анимации requestAnimationFrame(draw); } draw(); };
После передачи холста через transferControlToOffscreen()
основной поток больше не может напрямую взаимодействовать с этим <canvas>
. Визуализация выполняется полностью в Worker.
Для более лучшего взаимодействия между потоками можно юзать MessageChannel
. Это позволяет передавать данные между основным потоком и Worker без лишних копий:
// Основной поток const channel = new MessageChannel(); worker.postMessage({ port: channel.port1 }, [channel.port1]); channel.port2.onmessage = (event) => { console.log('Сообщение от Worker:', event.data); }; // Worker self.onmessage = function (event) { const port = event.data.port; setInterval(() => { port.postMessage({ status: 'Работаю!' }); }, 1000); };
OffscreenCanvas так же поддерживает контекст WebGL:
// worker.js self.onmessage = function (event) { const canvas = event.data.canvas; const ctx = canvas.getContext('2d'); // Настраиваем размеры холста canvas.width = 800; canvas.height = 600; const particles = []; // Генерация частиц 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, color: `hsl(${Math.random() * 360}, 100%, 50%)` }); } // Основной цикл отрисовки function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); for (const particle of particles) { ctx.beginPath(); ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2); ctx.fillStyle = particle.color; ctx.fill(); // Обновление позиции частицы particle.x += particle.vx; particle.y += particle.vy; // Проверка выхода за границы if (particle.x < 0 || particle.x > canvas.width) particle.vx *= -1; if (particle.y < 0 || particle.y > canvas.height) particle.vy *= -1; } // Рекурсивный вызов для плавной анимации requestAnimationFrame(draw); } draw(); };
Попробуйте, тестируйте, а если найдете интересные кейсы, делитесь в комментариях!
18 декабря в Otus пройдет открытый урок «Манипуляции с HTML и CSS с помощью JavaScript — основы динамичного взаимодействия с элементами страницы». Записаться можно по ссылке.
ссылка на оригинал статьи https://habr.com/ru/articles/863634/
Добавить комментарий