От кода до пикселей: как работает рендеринг. Часть 6. Альтернативные пути рендеринга.
Anastasia KotovaПредыдущие части
От кода до пикселей: как работает рендеринг. Часть 1. Браузерные движки.
От кода до пикселей: как работает рендеринг. Часть 2. Конвейер рендеринга.
От кода до пикселей: как работает рендеринг. Часть 3. Стадии основного потока.
От кода до пикселей: как работает рендеринг. Часть 4. Стадии композитного потока и работа с GPU.
От кода до пикселей: как работает рендеринг. Часть 5. Рендеринг в React.
На протяжении всего цикла мы говорили об одном сценарии рендеринга — браузер получает HTML/CSS/JS и строит DOM. Однако появление пикселей на экране не ограничивается только этим. В этой заключительной части мы рассмотрим альтернативные пути и технологии и сделаем обзор всего, что может быть доступно, когда мы говорим об отрисовке какого-то контента.
SSR: рендеринг на сервере
Server-Side Rendering — это метод создания веб-страниц, при котором готовый HTML-код генерируется на сервере, а не в браузере пользователя. Ещё до эпохи SPA (single page application) именно сервер генерировал HTML и отдавал его браузеру. Сегодня SSR снова популярен, но уже в новой оболочке, благодаря фреймворкам вроде Next.js и Nuxt.
Как это работает:
- Браузер делает запрос к серверу.
- Сервер запускает JavaScript (Node.js, Deno, Bun), исполняет компоненты и генерирует строку HTML.
- Готовая разметка отправляется клиенту, и браузер может немедленно отрисовать страницу.
- Параллельно или после загружается JS-бандл, и происходит гидратация (hydration) — React/Vue «оживляют» уже существующий DOM, навешивая обработчики событий.
Какие преимущества даёт SSR:
- Уменьшается время до отрисовки первого экрана (FCP/LCP), т.к. пользователь видит контент до загрузки, парсинга и исполнения JS.
- Поисковые боты получают готовый HTML без необходимости выполнять JavaScript, таким образом улучшая SEO-индексацию приложения.
- Страница доступна и работает даже при отключённых скриптах.
Гидратация — это дорогой процесс. Браузер получает HTML от сервера, но потом ему всё равно приходится «сверять» его с виртуальным DOM React. Если серверный и клиентский вывод расходятся, React перестраивает DOM заново. Поэтому некоторое время назад стала набирать популярность островная архитектура (Islands Architecture). Имплементацией этого подхода можно считать и React Server Components — механизм, когда гидрируются только интерактивные «острова», так называемые «клиентские компоненты», а «серверные компоненты» остаются статическим HTML.
Canvas: 2D-графика
<canvas> — прямоугольник с программируемым растровым содержимым. В отличие от DOM, canvas не имеет дерева объектов: мы рисуем напрямую через Canvas API.
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#ff6b6b';
ctx.fillRect(10, 10, 100, 50); // прямоугольник
ctx.font = '24px serif';
ctx.fillText('Hello, Canvas!', 10, 100); // текст
Команды вроде fillRect — это инструкции нарисовать что-то на canvas прямо сейчас. После выполнения они не оставляют никакого связного объекта в памяти. Нельзя «выбрать» нарисованный прямоугольник и сдвинуть его. Чтобы «переместить» объект, нужно очистить canvas и нарисовать всё заново.
Вызов canvas.getContext('2d') возвращает объект CanvasRenderingContext2D — в случае Chromium это обёртка над Skia. Когда мы вызываем ctx.fillRect(...), JavaScript-вызов транслируется в команду Skia.
Буфер пикселей — блок в памяти, который выделяет браузер для canvas. Его размер вычисляется как ширина × высота × 4 байта (RGBA: красный, зелёный, синий, прозрачность). Например, для canvas 800×600 это ~1.9 МБ. Skia пишет результат рисования прямо в этот буфер.
Дальше этот буфер должен попасть на экран, и здесь canvas также встраивается в браузерный пайплайн, который мы разбирали в предыдущих частях. Canvas-элемент становится отдельным композитным слоем и передаётся compositor'у. Compositor, в свою очередь, уже компонует его с остальной страницей.
Главная проблема canvas — он живёт в главном потоке браузера. Сложный цикл рисования (игра или редактор) конкурирует за процессорное время с обработкой пользовательского ввода, выполнением JS и другими операциями.
OffscreenCanvas помогает решить эту проблему: благодаря этому холст можно передавать в Web Worker, и весь рендеринг будет происходит в отдельном потоке.
// Главный поток
const offscreen = canvas.transferControlToOffscreen();
worker.postMessage({ canvas: offscreen }, [offscreen]);
// Web Worker
self.onmessage = ({ data }) => {
const ctx = data.canvas.getContext('2d');
// Рисуем здесь, не мешая главному потоку
};
Canvas используется везде, где DOM становится узким местом: 2D-игры, редакторы изображений, графики.
SVG: векторный рендеринг
SVG (Scalable Vector Graphics) — XML-формат для векторной графики, полностью интегрированный в DOM. В отличие от canvas, где вы работаете с пикселями напрямую, SVG описывает намерения: «нарисуй окружность радиуса 50 в точке (100, 100)». Браузер сам решает, как именно превратить это в пиксели — и делает это каждый раз заново при необходимости. Именно поэтому SVG выглядит одинаково чётко и на экране ноутбука, и на 4K-мониторе, и распечатанным на бумаге.
Каждый SVG-элемент — <path>, <circle>, <rect>, <text> — это полноценный DOM-узел. Браузер строит для SVG такое же дерево, как для HTML, применяет к нему CSS-стили (включая анимации и переходы), отслеживает события мыши и клавиатуры. Это даёт огромную гибкость: можно написать document.querySelector('circle').setAttribute('r', 80) и окружность сразу изменит размер. Можно повесить addEventListener('click', ...) на отдельный <path>. Скрин-ридеры могут читать <title> и <desc> внутри SVG.
Когда браузер встречает SVG, процесс рендеринга идёт через те же стадии, что и для обычного HTML, которые мы разбирали в предыдущих частах — только со своей спецификой на каждом шаге.
SVG-разметка парсится в узлы SVGElement. Каждый элемент несёт геометрические атрибуты (cx, cy, r, d) и стилевые (fill, stroke, opacity). Атрибут d у <path>, например M 10 10 L 100 50 C 150 80 200 20 250 50 Z — это последовательность команд «переместись», «нарисуй линию», «нарисуй кривую Безье», «закрой контур».
В отличие от HTML-layout, где браузер вычисляет потоковую раскладку элементов, SVG использует систему координат пользователя (user coordinate system). Атрибут viewBox="0 0 200 200" задаёт внутреннюю систему координат, а реальный размер элемента на странице определяется атрибутами width/height или CSS. Браузер вычисляет матрицу трансформации, которая переводит из одной системы в другую, и применяет её ко всей сцене.
Если SVG-элемент анимируется через CSS transform или opacity, браузер может вынести его на отдельный композитный слой — точно так же, как для обычных HTML-элементов. В этом случае повторная растеризация не нужна: compositor просто применяет трансформацию к уже готовой текстуре. Но если анимируется d (форма пути) или fill (заливка) — это смена геометрии, и браузер будет растеризовать её заново на каждом кадре.
SVG хорош для иконок, иллюстраций, интерактивных диаграмм. Но при тысячах элементов DOM-дерево становится узким местом — тогда переходят на canvas или WebGL.
WebGL и WebGPU: GPU в браузере
Canvas и SVG не подходят, если нужна настоящая 3D-сцена с тысячами объектов, динамическим освещением и 60 FPS. Здесь в дело вступает WebGL — браузерный интерфейс для прямого программирования видеокарты.
Идея проста: CPU (процессор) хорош в последовательных сложных вычислениях, а GPU (видеокарта) — в параллельных простых. Современная GPU имеет тысячи небольших ядер, которые могут одновременно обрабатывать тысячи пикселей или вершин. WebGL — это мост между JavaScript и GPU.
Шейдер — это программа, которая выполняется не на CPU, а прямо на GPU. Вы пишете её на специальном языке GLSL (Graphics Library Shader Language), который синтаксически похож на C. Шейдеры бывают двух видов, и они работают в паре.
Вершинный шейдер (vertex shader) запускается один раз для каждой вершины геометрии. Его задача — сказать GPU, где именно на экране должна оказаться эта вершина. Любой 3D-объект состоит из треугольников, а каждый треугольник — из трёх вершин с координатами в трёхмерном пространстве. Вершинный шейдер принимает 3D-координату и превращает её в 2D-координату на экране с учётом положения камеры, перспективы и трансформаций объекта.
Фрагментный шейдер (fragment shader) запускается один раз для каждого пикселя, который покрывает треугольник. Его задача — сказать GPU, какого цвета должен быть этот пиксель. Именно здесь реализуются освещение, тени, текстурирование и отражения.
Мы не будем подробно останавливаться на том, как работать с WebGL. Отметим только, что WebGL использует canvas.
<canvas id="gl-canvas" width="640" height="480"></canvas>
const canvas = document.querySelector("#gl-canvas");
const gl = canvas.getContext("webgl");
WebGL-canvas — это тот же композитный слой. Сompositor читает результат рендеринга этого слоя как обычную текстуру при финальной сборке страницы.
Писать на сыром WebGL — многословно и непросто: десятки строк на каждую операцию. Именно поэтому существуют высокоуровневые библиотеки, такие как Three.js.
WebGPU — преемник WebGL, разработанный с нуля для современных GPU API. Он частично доступен в большей части браузеров. В первую очередь благодаря этому инструменту GPU теперь можно использовать не только для графики, но и для общих вычислений (GPGPU) — машинное обучение, обработка данных и т.д.
PDF: рендеринг для печати
Когда вы открываете PDF в браузере, он не просто «отображает картинку». Браузеру нужно исполнить целый язык программирования.
PDF (Portable Document Format) вырос из PostScript — языка описания страниц, разработанного Adobe в 1980-х для управления принтерами. PostScript был полноценным языком программирования: принтер буквально выполнял код, который говорил ему, как именно рисовать страницу. PDF — это упрощённая, но похожая система.
Когда вы открываете PDF в Chrome, работает библиотека PDFium, написанная на C++. Она напрямую обращается к системным шрифтам, использует Skia для растеризации, а результат compositor получает как обычную текстуру.
PDF.js — принципиально другой подход: Mozilla реализовала полный PDF-рендерер на чистом JavaScript. Это означает, что весь парсинг, интерпретация операторов, управление шрифтами и растеризация происходят в JavaScript-движке браузера, а результат рисуется через Canvas API.
Конвертировать из HTML в PDF браузер умеет через window.print() и диалог печати. Внутри происходит примечательное: браузер запускает отдельный проход рендеринга с другими правилами (CSS @media print, разбивка на страницы). Chromium передаёт результат в PDFium, который уже отрисовывает контент.
Рендеринг видео
Элемент <video> с точки зрения compositor'а — это отдельный слой. Compositor не ждёт, пока браузер перерисует DOM — он получает свежие видеокадры напрямую из декодера и компонует их в финальное изображение независимо от остальной страницы. Это означает, что видео продолжает воспроизводиться плавно, даже если главный поток браузера занят тяжёлым JavaScript.
Синхронизация видео и аудио — это отдельная задача. Браузер поддерживает presentation timestamp каждого кадра: момент времени, в который его нужно показать. Аудио идёт через отдельный AudioContext с собственными буферами и таймингом. Если декодер отстаёт (например, сложная сцена с быстрым движением), браузер может намеренно пропустить кадр, чтобы не рассинхронизироваться с аудио — мы видим это как резкий скачок вместо плавного движения.
Долгое время JavaScript не мог ни кодировать, ни декодировать видео прямо в браузере напрямую. WebCodecs API добавил эту возможность: теперь доступны VideoDecoder и VideoEncoder — прямые обёртки над аппаратными кодеками, без промежуточных слоёв. Благодаря этому, в частности, появилась возможность делать полноценные видеоредакторы прямо в браузере.
Заключение
Мы начали с браузерных движков и проследили путь от исходного кода до пикселей на экране: через построение DOM, стили, layout, paint, слои, compositor и GPU. Увидели, как React оптимизировал работу через Fiber. И наконец, убедились, что «рендеринг» — понятие куда шире одного конвейера.
Браузер — это удивительная инженерная система. Надеюсь, этот цикл помог сделать её чуть менее загадочной.