Привет, Хабр! Сегодня разберёмся с тем, что такое OffscreenCanvas, зачем он нужен и как правильно его использовать.

OffscreenCanvas — это API, которое позволяет рендерить графику в отдельном потоке Worker, не блокируя основной поток, где обрабатывается интерфейс.

Технически это достигается за счёт разделения UI и вычислений:

  1. Canvas вне DOM
    OffscreenCanvas работает независимо от DOM. У него нет прямого визуального представления в интерфейсе. Это "виртуальный холст", который не участвует в браузерном рендеринге.

  2. Рендеринг в Worker
    Основная фича — рендеринг может выполняться в Web Worker. Браузерный поток UI остается свободным для обработки событий, скролла и других пользовательских взаимодействий.

  3. Поддержка разных API
    OffscreenCanvas поддерживает стандартные API:

    • 2d для работы с 2D-графикой;

    • webgl/webgl2 для 3D-рендеринга;

    • bitmaprenderer для прямого отображения растровых изображений.

  4. Оптимизация многопоточности
    За счёт использования 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 — основы динамичного взаимодействия с элементами страницы». Записаться можно по ссылке.

Комментарии (1)


  1. eshimischi
    11.12.2024 20:25

    В рамках коммерческого проекта реализовывал через OffscreenCanvas трансляцию с видео камеры и захват экрана, обработку WebCodecs в качество (encode/decode) и все это обернуто в WebWorker. Половина функционала экспериментальная в Хром, но все же работало!