Привет, Хабр! Сегодня разберёмся с тем, что такое 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 — основы динамичного взаимодействия с элементами страницы». Записаться можно по ссылке.
eshimischi
В рамках коммерческого проекта реализовывал через OffscreenCanvas трансляцию с видео камеры и захват экрана, обработку WebCodecs в качество (encode/decode) и все это обернуто в WebWorker. Половина функционала экспериментальная в Хром, но все же работало!