Примечание: тем, кто стремится досконально разобраться в том, как устроены браузеры, настоятельно рекомендую отличную книгу «Browser Engineering» Павла Панчехи и Криса Харрелсона (доступна здесь). Эта серия статей — лишь общий обзор принципов работы браузеров.
Веб-разработчики нередко воспринимают браузер как «черный ящик», который каким-то чудом превращает HTML, CSS и JS в интерактивные веб-приложения. На самом деле современный браузер — будь то Chrome (на базе Chromium), Firefox (Gecko) или Safari (WebKit) — представляет собой чрезвычайно сложное программное решение. Он управляет сетевыми запросами, разбирает (парсит) и выполняет код, рендерит графику с ускорением на графическом процессоре (GPU) и изолирует контент в отдельных процессах для обеспечения безопасности.
В этой серии статей мы подробно рассмотрим, как устроены современные браузеры, сделав акцент на архитектуре и внутреннем устройстве Chromium, но также отметим ключевые отличия в других браузерах. Мы рассмотрим весь цикл: от сетевого стека и конвейера парсинга до рендеринга с помощью Blink, выполнения JS с помощью движка V8, загрузки модулей, многопроцессной архитектуры, песочниц безопасности и инструментов разработчика. Главная цель — дать понятное и доступное объяснение того, что происходит в браузере «под капотом».

Это вторая часть серии.
Продолжим наше увлекательное путешествие.
❯ Загрузка модулей и карта импортов
Модули JavaScript (ES6-модули) используют иной подход к загрузке и выполнению кода, по сравнению с обычными тегами <script>. Вместо одного большого файла, создающего глобальные переменные, модули представляют собой отдельные файлы, которые явно импортируют и экспортируют значения. Разберем, как браузеры (в частности, V8 в Chrome) загружают модули и как в этом процессе используются такие возможности, как динамический импорт (import()) и карты импортов (import maps).
Статические импорт модулей: когда браузер встречает тег <script type="module" src="main.js">, он рассматривает main.js как точку входа модуля. Процесс загрузки выглядит так: браузер загружает main.js, затем парсит его как ES-модуль. Во время парсинга он находит все выражения import (например, import { foo } from './utils.js';). Вместо того, чтобы сразу выполнить код, браузер сначала формирует граф зависимостей модуля. Он начинает загружать все импортированные файлы (в данном случае — utils.js) и рекурсивно обрабатывает каждый из них, находя и подгружая их зависимости. Все это происходит асинхронно. Только после того, как весь граф модулей будет загружен и разобран, браузер приступает к его выполнению. Код модулей выполняется отложено — он запускается лишь тогда, когда все зависимости готовы, причем строго в порядке импорта зависимостей (если модуль A импортирует B, то сначала выполняется B).
Именно поэтому ES-модули нельзя просто загружать по протоколу file:// , если это явно не разрешено, и поэтому для междоменных скриптов требуется поддержка CORS. Браузер не просто добавляет <script> на страницу — он активно связывает и подгружает несколько файлов, выстраивая между ними зависимости.
Динамический импорт модулей: помимо статических операторов import, в стандарте ES2020 появилась функция import(moduleSpecifier) — выражение, которое позволяет загружать модули «на лету». Оно возвращает Promise, который в итоге предоставляет экспортируемые значения модуля.
Например, можно вызвать: const module = await import('./analytics.js'); в ответ на действие пользователя — так реализуется разделение кода (code splitting).
Под капотом import() заставляет браузер загрузить указанный модуль (и его зависимости, если они еще не загружены), затем выполнить его инициализацию и запуск. После этого промис возвращает объект пространства имен (namespace) модуля. Здесь браузер и V8 работают совместно: браузе��ный загрузчик модулей отвечает за загрузку и парсинг, а V8 — за компиляцию и выполнение кода.
Преимущество динамического импорта в том, что его можно использовать даже в обычных скриптах, а не только в модульных — например, внутри встроенного <script>. Он позволяет загружать код по мере необходимости.
Главное отличие от статического импорта состоит в том, что статические импорты разрешаются заранее — еще до выполнения кода браузер загружает весь граф зависимостей. Динамический же импорт работает скорее как загрузка нового скрипта во время выполнения программы, но при этом сохраняет модульную модель и использует промисы.
Карты импортов: одна из ключевых проблем ES-модулей в браузере долгое время была связана с тем, как именно разрешаются имена модулей. В Node.js или в сборщиках модулей (bundlers) значения можно импортировать по имени пакета (например, import { useState } from 'react';). Но в браузере без этапа сборки такой идентификатор не является корректным URL: браузер воспринимает react как относительный путь и, разумеется, не может его загрузить.
Здесь на помощь приходят карты импортов. Карта импорта — это конфигурация в формате JSON, которая указывает браузеру, как сопоставлять идентификаторы модулей с реальными URL. Подключается она через HTML тег <script type="importmap">. Например, в карте импорта можно указать, что идентификатор react должен соответствовать URL: https://cdn.example.com/react@19.0.0/index.js. После этого при выполнении import 'react' браузер с помощью карты сразу понимает, какой адрес использовать, и загружает нужный модуль. По сути, карты импортов позволяют использовать "голые" (bare) идентификаторы (вроде имен npm-пакетов) прямо в браузере, сопоставляя их с CDN-ресурсами или локальными файлами.
Карты импортов значительно упростили разработку без использования сборщиков. Начиная с 2023 года, они поддерживаются всеми основными браузерами: Chrome 89+, Firefox 108+, Safari 16.4+ — т.е. всеми тремя движками. Особенно полезны они для локальной разработки или небольших приложений, где не хочется подключать сборщик. В продакшене крупные проекты по-прежнему часто собирают в сборки (bundles), чтобы сократить количество запросов, но по мере развития браузеров и с появлением HTTP/2/3 подход с большим числом отдельных модулей становится вполне рабочим и эффективным.
Загрузчик модулей в браузере состоит из карты модулей (отслеживающей, что уже загружено), при необходимости карты импорта (для кастомного разрешения идентификаторов) и логики загрузки и парсинга модулей. После загрузки и компиляции код модуля выполняется в строгом режиме (strict mode) и в собственной области видимости верхнего уровня — ничего не попадает в window, если не делать этого специально. Экспортируемые значения кэшируются, поэтому при повторном импорте того же модуля он не выполняется заново, а используется уже готовый результат.
Еще один момент: в отличие от обычных скриптов, ES-модули откладывают выполнение кода до момента готовности зависимостей и выполняются в определенном порядке для каждого графа зависимостей. Например, если main.js импортирует util.js, а util.js импортирует dep.js, порядок выполнения будет следующим: сначала dep.js, затем util.js, и только потом main.js (обход в глубину). Такой детерминированный порядок выполнения позволяет в некоторых случаях обходиться без событий, вроде DOMContentLoaded, так как к моменту запуска главного модуля все его зависимости уже загружены и выполнены.
С точки зрения V8, модули обрабатываются той же цепочкой компиляции, что и обычный JS-код, но для каждого модуля создается отдельный объект ModuleRecord. Движок гарантирует, что верхнеуровневый код модуля выполняется только после того, как будут готовы все его зависимости.
V8 также умеет работать с циклическими импортами модулей, которые разрешены спецификацией и могут приводить к частично инициализированным экспортам. Подробности определены стандартом, но в целом движок сначала создает все экземпляры модулей, затем решает циклы с помощью временных «заполнителей» (placeholders), а после этого выполняет код модулей в порядке, учитывающем зависимости (алгоритм, описанный в спецификации, представляет собой топологическую сортировку ориентированного ациклического графа (DAG) модуля).
Загрузка модулей в браузере — это скоординированная работа сети (загрузка файлов модулей), резолвера модулей (с использованием карт импортов или стандартного разрешения URL) и движка JS (компиляция и выполнение модулей в правильном порядке). Этот процесс сложнее, чем простая загрузка через <script>, но он обеспечивает более модульную и удобную для поддержки структуру кода. Ключевые выводы для разработчиков: используйте модули для организации кода, применяйте карты импортов для "голых" импортов и помните, что модули можно загружать динамически через import(). Браузер берет на себя всю работу по правильному порядку выполнения кода.
Теперь, когда мы разобрались, как работает загрузка и выполнение JS в одной вкладке, можно перейти к устройству браузера как многопроцессной системы — той архитектуре, которая позволяет множеству страниц и приложений работать параллельно, не мешая друг другу.
❯ Многопроцессная архитектура браузеров
Современные браузеры (Chrome, Firefox, Safari, Edge и др.) используют многопроцессную архитектуру, чтобы повысить стабильность, безопасность и изолировать производительность. Раньше весь браузер работал как единый процесс, но теперь разные его компоненты запускаются отдельно. Chrome стал пионером этого подхода в 2008 году, после чего другие браузеры постепенно внедрили аналогичные решения. Рассмотрим архитектуру Chromium и отметим отличия в Firefox и Safari.
В Chromium (Chrome, Edge, Brave и др.) есть один центральный процесс — процесс браузера (Browser Process). Он отвечает за пользовательский интерфейс (адресная строка, закладки, меню — вся «хромовая» оболочка браузера) и за координацию высокоуровневых задач, таких как загрузка ресурсов и навигация. Когда открывается Chrome, и мы видим один процесс в диспетчере задач ОС, это и есть Browser Process. Он же является родительским процессом, порождающим остальные.
Для каждой вкладки (а иногда и для каждого сайта в отдельной вкладке) Chrome создает процесс рендеринга (Renderer Process). Этот процесс запускает движок рендеринга Blink и движок JS V8 для содержимого конкретной вкладки. В целом, каждой вкладке выделяется, как минимум, один процесс рендеринга.

Если открыто несколько независимых сайтов, они будут работать в отдельных процессах (например, сайт A в одном, сайт Б в другом и т.д.). Chrome даже помещает iframe с другими доменами в отдельные процессы (подробнее об этом в разделе про изоляцию сайтов). Процесс рендеринга работает в песочнице (sandbox) и не может напрямую обращаться к файловой системе или сети — для таких операций ему нужно взаимодействовать с процессом браузера.
Другие важные процессы в Chrome:
Процесс GPU: процесс, выделенный для работы с GPU, как было описано ранее. Все запросы на рендеринг и компоновку от процессов рендеринга направляются в GPU-процесс, который выполняет реальные вызовы графического API. Этот процесс изолирован в песочнице, чтобы сбой GPU не приводил к падению рендереров.
Сетевой процесс: в старых версиях Chrome сетевые операции выполнялись в потоке внутри процесса браузера, но теперь это часто отдельный процесс благодаря «сервисификации» (servicification). Он обрабатывает сетевые запросы, DNS и т.д., и также может быть изолирован отдельно.
Служебные (вспомогательные) процессы: используются для разных сервисов, таких как воспроизведение аудио, декодирование изображений и других задач, которые Chrome может выносить в отдельные процессы.
Процесс плагинов: в эпоху Flash и плагинов NPAPI плагины запускались в собственном процессе. Сейчас Flash устарел, поэтому это менее актуально, но архитектура все еще поддерживает запуск плагинов вне основного процесса браузера.
Процессы расширений: расширения Chrome (по сути скрипты, которые могут воздействовать на веб-страницы или браузер) тоже запускаются в отдельных процессах, изолированных от сайтов для повышения безопасности.
Если упростить, это выглядит так: один процесс браузера управляет несколькими процессами рендеринга (по одному на вкладку или на отдельный сайт), а также процессом GPU и несколькими вспомогательными сервисными процессами. В диспетчере задач Chrome (Shift+Esc на Windows или через «Дополнительные инструменты» → «Диспетчер задач») можно увидеть каждый процесс с указанием его типа и объема потребляемой памяти.
Преимущества многопроцессной архитектуры:
Стабильность: если вкладка с веб-страницей упадет или начнет расходовать слишком много памяти, это не обрушит весь браузер — достаточно закрыть только эту страницу, и остальные вкладки продолжат нормально работать. В однопроцессных браузерах один проблемный скрипт мог положить весь браузер. В Chrome при сбое процесса вкладки появляется ошибка «Aw, Snap», и ее можно перезагрузить отдельно.
Безопасность (песочница): запуская веб-контент в изолированном процессе, браузер может ограничить его возможности в вашей системе. Даже если злоумышленник обнаружит уязвимость в движке рендеринга, он останется в песочнице — процесс рендеринга обычно не может читать файлы, открывать сетевые соединения или запускать программы напрямую. Для доступа к таким ресурсам требуется запрос к процессу браузера, который может его подтвердить или отклонить. Песочница реализована на уровне ОС (через объекты заданий (job objects), seccomp-фильтры и т.д. в зависимости от платформы).
Изоляция производительности: тяжелая работа в одной вкладке (сложное веб-приложение или бесконечный цикл) ограничена процессом этой вкладки. Другие вкладки продолжают работать нормально, потому что их процессы не блокируются. ОС может распределять их по разным ядрам процессора, что позволяет эффективно выполнять несколько «тяжелых» страниц одновременно — куда лучше, чем если бы все шло в потоках одного процесса.
Сегментация памяти: каждый процесс имеет собственное адресное пространство, память не разделяется. Это предотвращает доступ одного сайта к данным другого и позволяет ОС полностью освобождать память при закрытии вкладки. Минус — небольшой «оверхед» из-за дублирования ресурсов (каждый рендеринг загружает свою копию движка JS и других необходимых компонентов). Изоляция сайтов: изначально в Chrome использовалась модель «один процесс на вкладку». Со временем ее сменили на «один процесс на сайт» (особенно после уязвимости Spectre — подробнее в следующем разделе про безопасность). По состоянию на 2024 год изоляция сайтов включена по умолчанию у 99% пользователей Chrome на десктопах, а поддержка на Android все еще активно дорабатывается. Это означает, что если у нас открыто две вкладки с example.com, Chrome может использовать один процесс для обеих, чтобы экономить память — ведь это один и тот же сайт, и объединять их достаточно безопасно. Но если на странице example.com есть
iframeс evil.com, Chrome обязательно вынесетiframeв отдельный процесс, чтобы защитить данные основного сайта. Такой подход называется строгой изоляцией сайтов (Strict Site Isolation) и стал включаться по умолчанию, начиная примерно с Chrome 67. Изоляция сайтов увеличивает использование системных ресурсов примерно на 10–13% из-за большего числа создаваемых процессов, однако обеспечивает критически важный уровень безопасности.
Архитектура Firefox, называемая Electrolysis (e10s), долгое время использовала всего один контент-процесс для всех вкладок (много лет Firefox был однопроцессным браузером и лишь около 2017 года начал включать несколько процессов). Начиная с 2021 года Firefox использует несколько процессов для веб-контента (по умолчанию — 8). С запуском Project Fission (изоляция сайтов) Firefox движется к той же модели, что и Chrome: он может выделять отдельные процессы для межсайтовых iframe, и начиная с версии Firefox 108+ изоляция сайтов включена по умолчанию. Это позволяет браузеру при необходимости запускать практически один процесс на сайт, подобно Chrome. Firefox также имеет GPU-процесс (используется WebRender и компоновка) и отдельный сетевой процесс, что похоже на разделение в Chrome. В итоге, архитектура Firefox сейчас очень близка к модели Chrome: родительский процесс, GPU-процесс, сетевой процесс, несколько процессов контента (рендереров) и ряд служебных процессов (например, для расширений, декодирования медиа — медиаплагин может работать изолированно).
Safari (WebKit) также перешел на многопроцессную архитектуру (WebKit2), где любое содержимое вкладки запускается в отдельном процессе WebContent, а центральный UI-процесс управляет ими. WebContent-процессы Safari также изолированы и не могут напрямую обращаться к устройствам или файлам без участия UI-процесса. У Safari есть и сетевой процесс (а также несколько других вспомогательных). Таким образом, несмотря на различия в реализации, концепция везде одинакова: каждая веб-страница выполняется в своем изолированном процессе-песочнице.
Важный момент — межпроцессное взаимодействие (IPC): как процессы обмениваются данными? Браузеры используют механизмы IPC (в Windows это часто именованные каналы (named pipes) или другие средства ОС; в Linux — Unix-сокеты или общая память; у Chrome есть собственная IPC-библиотека Mojo). Например, когда сетевой ответ приходит в сетевой процесс, его нужно передать нужному процессу рендеринга (при участии браузерного процесса, который координирует передачу). Аналогично, когда в JS вызывается fetch(), движок обращается к сетевому API, который отправляет запрос в сетевой процесс и т.д. IPC добавляет сложности, но браузеры активно оптимизируют взаимодействие (например, используют общую память для эффективной передачи больших данных, таких как изображения, и отправляют асинхронные сообщения, чтобы избежать блокировок).
Стратегии распределения процессов: Chrome не всегда создает новый процесс для каждой вкладки — существуют ограничения, особенно на устройствах с малым объемом памяти. В таких случаях браузер может повторно использовать процесс для вкладок одного и того же сайта. Например, если открыть еще одну вкладку того же сайта, Chrome может использовать уже существующий рендерер, чтобы сэкономить память. Поэтому иногда две вкладки одного и того же сайта работают в одном процессе. Кроме того, существует общий лимит на количество процессов, который может масштабироваться в зависимости от объема оперативной памяти (RAM). При достижении лимита, браузер может объединять несколько несвязанных сайтов в одном процессе, хотя при включенной изоляции сайтов старается этого избегать. На Android Chrome использует меньше процессов из-за ограничений памяти — обычно максимум 5–6 процессов для контента.
Еще одна концепция в Chromium — сервисификация: разделение компонентов браузера на сервисы, которые могут работать в отдельных процессах. Например, сетевой сервис (Network Service) вынесен в отдельный модуль, который может выполняться вне основного процесса. Идея в модульности: на мощных устройствах каждый сервис может работать в своем процессе, тогда как на устройствах с ограниченными ресурсами некоторые сервисы могут объединяться в один процесс, чтобы уменьшить накладные расходы. Chrome может решать, как развернуть эти сервисы — во время работы или на этапе сборки. На мощных устройствах процессы разделяются полностью (UI, сеть, GPU и т.д. — все отдельно), а на слабых (Android) браузер и сетевой сервис могут работать в одном процессе, чтобы сократить ��агрузку.
Вывод: архитектура Chromium рассчитана на то, чтобы интерфейс браузера и каждый сайт работали в отдельных песочницах, используя процессы как границу изоляции. Firefox и Safari со временем пришли к похожим решениям. Такая архитектура значительно повышает безопасность и надежность, хотя требует больше памяти. Процессы веб-контента считаются небезопасными, и именно здесь вступает в силу изоляция сайтов, позволяющая изолировать даже разные домены друг от друга в отдельных процессах.
❯ Песочницы и изоляция сайтов
Песочницы и изоляция сайтов — это функции безопасности, которые строятся на многопроцессной архитектуре. Их цель — сделать так, чтобы даже если в браузере выполнится вредоносный код, он не смог легко получить данные с других сайтов или получить доступ к системе.
Изоляция сайтов: мы уже затрагивали этот момент — она означает, что разные сайты запускаются в отдельных процессах рендеринга. После обнаружения уязвимости Spectre в 2018 году Chrome усилил эту защиту. Spectre показал, что вредоносный JS потенциально мог читать чужую память через спекулятивное выполнение CPU. Если два сайта находились в одном процессе, вредоносный сайт мог использовать Spectre, чтобы подглядывать в память конфиденциального сайта (например, банковского). Единственное надежное решение — не позволять сайтам делить процесс вообще. Поэтому Chrome сделал изоляцию сайтов включенной по умолчанию: каждый сайт получает свой процесс, включая iframe с другими доменами. Firefox пошел тем же путем с Project Fission (по умолчанию включен в последних версиях), стремясь к той же цели — каждый сайт в отдельном процессе ради безопасности. Это существенное изменение по сравнению с прошлым, когда родительская страница и несколько iframe с разными доменами могли жить в одном процессе (особенно в одной вкладке). Теперь iframe, вроде <iframe src="https://evil.com">, на странице «хорошего» сайта принудительно помещается в отдельный процесс, что предотвращает утечку данных даже при низкоуровневых атаках.
С точки зрения разработчика, изоляция сайтов в основном прозрачна. Одно из последствий — коммуникация между встроенным iframe и родительской страницей может пересекать границы процессов, поэтому такие методы как postMessage() реализуются через IPC. Но браузер делает это незаметно — для разработчика API остаются привычными и работают, как обычно.
Песочница: каждый процесс рендеринга (и другие вспомогательные процессы) работает в песочнице с ограниченными возможностями. Например, в Windows Chrome использует объект задач и снижает права, чтобы рендер не мог вызывать большинство Win32 API, имеющих доступ к системе. В Linux применяются пространства имен и фильтры Seccomp для ограничения системных вызовов. Процесс рендеринга может выполнять вычисления и отображать контент, но если он попытается открыть файл, камеру или микрофон — доступ будет заблокирован (если только не использовать легальные каналы через процесс браузера с запросом разрешения у пользователя).
Документация WebKit явно указывает, что процессы WebContent не имеют прямого доступа к файловой системе, буферу обмена, устройствам и т.д. — они должны делать запрос через процесс интерфейса браузера, который выполняет посредническую функцию. Именно поэтому, когда сайт запрашивает доступ к микрофону, окно разрешения отображается UI браузера (процесс браузера), а сама запись звука выполняется в контролируемом процессе.
Песочница — критически важный барьер защиты. Даже если злоумышленник найдет уязвимость для выполнения нативного кода в рендерере, он столкнется с ограничениями песочницы — потребуется отдельный эксплойт (так называемый «escape»), чтобы выйти в систему. Этот многоуровневый подход (изоляция сайтов + песочница) является передовым стандартом безопасности в браузерах.
Песочница Firefox также довольно серьезная. До e10s защита была слабее, но со временем ее усилили. Процессы контента Firefox тоже не имеют прямого доступа к системе, а процесс GPU дополнительно изолирован для безопасной работы с драйверами графики.
Процессно-изолированные фреймы (Out-of-Process iframes, OOPIF): в реализации изоляции сайтов в Chrome был введен термин OOPIF — iframe вне процесса. С точки зрения пользователя ничего не меняется, но в архитектуре Chrome каждый фрейм страницы может работать в отдельном процессе рендеринга. Основной фрейм и фреймы того же сайта делят один процесс, а межсайтовые фреймы используют отдельные процессы. Все эти процессы «сотрудничают», чтобы отобразить содержимое одной вкладки, при этом их координирует процесс браузера. Это довольно сложная архитектура, но Chrome использует дерево фреймов, которое может распространяться на несколько процессов.
На практике это означает, что одна вкладка может запускать несколько процессов: один для основного документа и отдельные — для каждого межсайтового поддокумента. Процессы общаются через IPC, например, для передачи событий DOM через границы процессов или для определенных JS-вызовов между контекстами. Веб платформа (через спецификации вроде COOP/COEP, SharedArrayBuffer и др.) развивается с учетом этих ограничений после уязвимости Spectre.
Память и производительность: изоляция сайтов действительно увеличивает расход памяти, так как задействуется больше процессов. Разработчики Chrome отмечают, что в некоторых случаях это может давать 10–20% дополнительной нагрузки на память. Часть этой нагрузки они компенсировали с помощью так называемой «консолидации процессов по принципу наилучших усилий» для вкладок одного сайта и ограничения количества создаваемых процессов (было упомянуто ранее). В Firefox изначально не изолировали каждый сайт из-за ограничений памяти, но после уязвимости Spectre нашли способы делать это эффективнее — с ограничением до 8 привилегированных процессов и динамическим созданием процессов по мере необходимости. Safari исторически использовал сильную многопроцессную модель, хотя не совсем ясно, изолирует ли он межсайтовые iframe; WebKit2 точно изолирует верхнеуровневые страницы. При этом Apple уделяет особое внимание конфиденциальности (например, «Технология интеллектуальной защиты от отслеживания» (Intelligent Tracking Prevention) разделяет куки), но это уже отдельный уровень.
Предзагрузка страниц с другого сайта (cross-site prefetch) ограничена из соображений конфиденциальности и работает, только если у пользователя нет куки для целевого сайта. Это предотвращает слежку за действиями пользователя через предзагруженные страницы, которые он может и не посетить.
В целом, изоляция сайтов обеспечивает соблюдение принципа минимальных привилегий: код с домена A не может получить доступ к данным с домена B, только через веб-API с явного согласия пользователя (например, через postMessage() или общее хранилище). Песочница гарантирует, что даже если код окажется вредоносным, он не сможет напрямую повлиять на систему. Эти меры значительно усложняют эксплуатацию уязвимостей: злоумышленнику теперь нужно использовать цепочку нескольких эксплойтов (например, один для взлома рендерера, другой для выхода из песочницы), чтобы нанести серьезный ущерб.
Для веб-разработчика изоляция сайтов почти незаметна, но она делает работу с вебом безопаснее. Единственное, что стоит учитывать: межпроцессное взаимодействие между сайтами может давать небольшую дополнительную нагрузку (из-за IPC), а некоторые оптимизации, вроде совместного использования скриптов в одном процессе, между разными доменами невозможны. Браузеры постоянно оптимизируют интерфейс обмена сообщениями между процессами, чтобы минимизировать потери производительности.
❯ Сравнение Chromium, Gecko и WebKit
Мы в основном описывали поведение Chrome/Chromium (движок Blink для HTML/CSS, V8 для JS, многопроцессная архитектура через инфраструктуру Aura/Chromium). Другие крупные движки — Gecko от Mozilla (используется в Firefox) и WebKit от Apple (используется в Safari) — преследуют те же базовые цели и имеют схожий конвейер обработки, но есть важные отличия и исторические расхождения.
Общие концепции: все движки разбирают HTML в DOM, CSS — в данные о стилях, вычисляют расположение элементов и выполняют рендеринг/компоновку. Все имеют движки JS с JIT-компиляцией и сборкой мусора. И все современные движки используют многопроцессность (или, как минимум, многопоточность) для параллельной работы и повышения безопасности.
Отличия в CSS/системе стилей
Интересное различие заключается в том, как движки реализуют вычисление стилей CSS:
Blink (Chromium): использует однопоточный движок стилей на C++ (основанный на WebKit). Стили вычисляются последовательно для дерева DOM. Существуют оптимизации с инкрементальной проверкой валидности стилей, но в целом работу выполняет один поток (за исключением небольшой параллелизации для анимации).
Gecko (Firefox): в рамках проекта Quantum (2017) Firefox интегрировал Stylo — новый CSS-движок на Rust с поддержкой многопоточности. Firefox может вычислять стили для разных поддеревьев DOM параллельно, используя все ядра процессора. Это стало значительным улучшением производительности CSS в Gecko: перерасчет стилей может использовать 4 ядра для работы, которую Blink выполняет на одном. Главный плюс подхода Gecko — скорость, но цена — сложность реализации.
WebKit (Safari): движок стилей WebKit, как и Blink, однопоточный (Blink отделился от WebKit в 2013 году, до этого архитектура была общей). В WebKit реализованы интересные решения, например, JIT-компиляция байт-кода для CSS-селекторов: селекторы могут трансформироваться в байт-код, а затем компилироваться JIT для ускорения сопоставления. Blink такой подход не использует, применяя итеративное сопоставление.
Таким образом, в CSS движок Gecko выделяется параллельным вычислением стилей с использованием Rust, тогда как Blink и WebKit полагаются на оптимизированный C++ и некоторые JIT-трюки (в случае WebKit).
Разметка и графика
Все три движка реализуют модель коробки (box model) CSS и алгоритмы разметки. Некоторые возможности могут появляться в одном движке раньше, чем в других (например, когда-то WebKit опережал в поддержке CSS Grid, затем Blink его догнал — часто они синхронизируются через стандартизацию).
Firefox (Gecko) сделал огромный шаг, внедрив WebRender как движок разметки и растеризации. Сейчас WebRender — рендерер по умолчанию в Firefox. Он заметно улучшил производительность, особенно при работе с графически насыщенным веб-контентом. WebRender (также написанный на Rust) принимает список отображаемых элементов и непосредственно рендерит его на GPU, обрабатывая с помощью графического процессора такие задачи, как тесселяция фигур, текста и т.д. По сути, он переносит большую часть работы по отрисовке страницы на GPU.
В Chrome большая часть растеризации по-прежнему выполняется на CPU, а затем отправляется на GPU в виде битовых карт (bitmaps). WebRender старается избегать создания битмапов для целых слоев и вместо этого рисует векторное содержимое прямо на GPU (за исключением глифов текста, которые он кэширует в атласных текстурах (atlas textures)). Это позволяет Firefox потенциально анимировать больше элементов с высокой производительностью — ему не нужно заново растрировать все содержимое при изменении лишь небольших его участков; перерисовка с помощью GPU выполняется очень быстро. Это похоже на то, как игровой движок обновляет кадр: каждый фрейм строится через GPU-вызовы. Минус: подход сложен в реализации и требует тонкой настройки, а также сильнее нагружает видеокарту. Однако с ростом вычислительной мощности GPU этот подход становится все более перспективным. Команда Chrome рассматривала похожую идею (SKIA GPU), но не провела полного пересмотра архитектуры в стиле WebRender.
Safari (WebKit) использует подход, более похожий на тот, что был у старых версий Chrome: он формирует разметку через слои (называемые CALayer, поскольку на macOS и iOS используется система слоев Core Animation). Safari одним из первых перешел к GPU-разметке — еще в iPhone OS и Safari 4 (в 2009 году) некоторые CSS-эффекты, такие как трансформации, аппаратно ускорялись на GPU. В дальнейшем Safari и Chrome разошлись в реализации, но концептуально оба используют плиточную структуру (tiling) и разметку. Safari также активно выносит задачи на GPU и использует тайлинг — особенно на iOS, где отрисовка с помощью плиток изначально стала основой для обеспечения плавной прокрутки.
Оптимизации для мобильных устройств: каждый движок имеет свои особенности для мобильных платформ. WebKit использует концепцию покрытия плитками для прокрутки (исторически применялось в iOS через UIWebView). Chrome на Android использует тайлинг и старается минимизировать задачи растеризации, чтобы выдерживать частоту кадров. Firefox применяет WebRender, который изначально разрабатывался в рамках проекта Servo.
JavaScript-движки
V8 (Chromium): мы уже описывали его уровни: Ignition, Sparkplug, TurboFan и Maglev (по состоянию на 2023 год).
SpiderMonkey (Firefox): исторически имел интерпретатор, затем Baseline JIT и оптимизирующий компилятор IonMonkey. Недавний проект Warp изменил устройство уровней JIT — он упростил работу Ion и сделал ее больше похожей на подход V8 TurboFan по использованию кэшированного байт-кода и информации о типах. SpiderMonkey также использует другой сборщик мусора (тоже поколенческий — с 2012 года он называется Incremental GC, сейчас в основном инкрементальный и частично конкурентный).
JavaScriptCore (Safari): как уже было отмечено, включает четыре уровня: LLInt, Baseline, DFG и FTL. Использует другой сборщик мусора (в WebKit ранее применялся поколенческий сборщик на основе маркировки и очистки (mark-sweep) — так называемый Butterfly или разновидности Boehm; в настоящее время используется bmalloc и др.). Уровень FTL опирается на LLVM — это уникально (V8 и SpiderMonkey используют собственные компиляторы). Такой подход может обеспечивать очень высокую скорость, но требует тяжелой компиляции. JSC иногда показывает выдающиеся результаты на отдельных бенчмарках, хотя V8 часто быстро догоняет — движки регулярно обгоняют друг друга.
По поддержке возможностей ECMAScript все три движка в целом идут в ногу со стандартами — благодаря test262 и сильной конкуренции между собой.
Отличия в многопроцессной архитектуре
Chrome: обычно использует отдельный процесс для каждой вкладки; при изоляции сайтов процессы разделяются на уровне источника (домена), поэтому их может быть очень много (иногда десятки).
Firefox: по умолчанию использует меньше процессов — примерно, 8 контент-процессов на все вкладки, плюс дополнительные, при необходимости (например, для межсайтового
iframeпри включенном Fission). Это означает, что Firefox не выделяет отдельный процесс для каждой вкладки: вкладки делят процессы из общего пула. Такой подход снижает потребление памяти при большом числе вкладок, но в то же время это означает, что сбой одного процесса может сломать сразу несколько вкладок (хотя Firefox пытается группировать их по сайтам, например, все вкладки Facebook — в одном процессе).Safari: обычно создает один процесс на вкладку (или на несколько вкладок). На iOS каждый WebView точно изолируется отдельно. На десктопном Safari ранее также использовалась отдельная изоляция для каждой вкладки. Не до конца ясно, изолируются ли
iframeс разными доменами — Apple не слишком подробно описывает меры против Spectre, но, по крайней мере, для доменов верхнего уровня изоляция есть.
Межпроцессное взаимодействие: всем движкам приходится решать похожие задачи — например, как реализовать alert() (блокирующий выполнение JS) в многопроцессной среде. Обычно основной процесс браузера отображает диалоговое окно оповещения и приостанавливает соответствующий контекст выполнения скрипта. Аналогично обстоит дело с обработкой prompt(), confirm() и реализацией модальных окон. Однако между браузерами есть нюансы реализации: например, в Chrome блокировка потока при вызове alert() не является «настоящей» — рендерер запускает вложенный цикл обработки событий, тогда как Firefox может полностью приостанавливать процесс соответствующей вкладки.
Обработка сбоев: Chrome и Firefox имеют собственные системы отчетов о сбоях, способные перезапустить аварийно завершившийся процесс контента и отобразить сообщение об ошибке во вкладке. При сбое процесса Web Content в Safari, как правило, отображается более простое сообщение об ошибке непосредственно в области содержимого.
Расхождения в реализации возможностей
Некоторые возможности веб-платформы зависят от конкретного движка: например, Chrome поддерживает экспериментальный API document.transition() для плавных переходов DOM, который опирается на архитектуру Blink. Firefox, возможно, реализует аналогичную функциональность с другим подходом. Однако со временем стандарты приводят такие функции к общему виду (прим. пер.: кажется, это уже произошло через View Transition API).
Инструменты разработчика: DevTools в Chrome отличаются высокой степенью проработанности. В Firefox инструменты разработчика тоже на высоком уровне и имеют уникальные возможности (например, инструмент подсветки CSS Grid, редактор форм). Web Inspector в Safari вполне работоспособен, но в отдельных аспектах уступает по функциональности. Эти различия важны для разработчиков при отладке в разных браузерах.
Компромиссы производительности
Исторически Chrome считался самым быстрым благодаря своей многопроцессной архитектуре и движку V8. После выхода Firefox Quantum разрыв значительно сократился, и в некоторых задачах Firefox даже обгоняет Chrome, особенно в графике (WebRender показывает отличную производительность на сложных страницах). Safari зачастую демонстрирует высокие показатели в графике и энергоэффективности на устройствах Apple (здесь производитель уделяет большое внимание оптимизации энергопотребления).
Память: Chrome известен высоким расходом памяти из-за большого количества процессов. Firefox обычно более экономичен. Safari на iOS крайне бережно относится к памяти — это необходимость при ограниченном объеме RAM, поэтому в WebKit реализовано множество механизмов оптимизации памяти.
Внешние разработчики: забавно, но многие улучшения в браузерных движках создаются сторонними командами, такими как Igalia (например, реализация CSS Grid в Blink и WebKit). Благодаря этому некоторые возможности появляются в разных браузерах почти одновременно.
С точки зрения веб-разработчика различия между движками проявляются в следующем:
необходимость тестировать на всех движках, поскольку возможны незначительные различия или ошибки в реализации того или иного свойства CSS или API
различия в производительности: например, определенный код JS может выполняться быстрее в одном движке по сравнению с другим из-за особенностей работы JIT
отсутствие поддержки некоторых API в отдельных браузерах (например, Safari зачастую последним внедряет новые возможности, такие как WebRTC или современные версии IndexedDB)
Но основные концепции, которые мы рассмотрели (сеть → парсин�� → макет → отрисовка → композиция → выполнение JS), применимы ко всем движкам — различаются лишь внутренние реализации и названия этапов:
В Gecko: парсинг → дерево фреймов → список отображения → сцена WebRender или дерево слоев (если WebRender отключен) → композиция
В WebKit: парсинг → дерево рендеринга → графические слои → композиция (через Core Animation)
И у всех есть аналогичные подсистемы: DOM, стилизация, макет, графика, движок JS, сетевые модули, процессы/потоки.
Понимание этих этапов помогает в отладке: например, если анимация «лагает» в Safari, но не в Chrome, это может быть связано с отличиями в механизме отрисовки WebKit. А если стили CSS обрабатываются медленно в Firefox, возможно, задействован участок, который не параллелизуется движком Stylo (хотя такие случаи встречаются редко).
Подводя итог, можно сказать: хотя Chromium, Gecko и WebKit имеют разные реализации и собственные инновации (например, параллельное вычисление стилей в Gecko, WebRender на GPU и т.д.), они все активнее движутся к единым стандартам и даже совместным разработкам. Выбор движка больше важен для производителей платформ и разнообразия в экосистеме, а для веб-разработчика, в первую очередь, важно, чтобы сайт корректно работал везде. Уникальная архитектура каждого движка может приводить к различиям в производительности или проявлению специфических ошибок. Именно поэтому тестирование в разных браузерах и использование их встроенных инструментов диагностики дает ценное понимание поведения приложения. Перечислить все отличия в рамках одной статьи невозможно, но, надеюсь, что этот обзор помог сформировать общее представление: на высоком уровне движки похожи (многопроцессность, общая конвейерная модель), однако сохраняют различия в конкретных технических решениях.
❯ Заключение и дополнительные материалы
Мы прошли весь путь жизни веб-страницы внутри современного браузера — от ввода URL до сетевого взаимодействия и навигации, парсинга HTML, применения стилей, формирования макета, отрисовки и выполнения JS, вплоть до вывода пикселей на экран с помощью GPU. Мы убедились, что браузеры по сути представляют собой миниатюрные ОС: они управляют процессами, потоками, памятью и множеством сложных подсистем, обеспечивая быструю загрузку и безопасное выполнение кода. Понимание этих внутренних механизмов помогает веб-разработчику осознать, почему те или иные рекомендации действительно важны: например, минимизация перерисовок или использование асинхронных скриптов — критичны для производительности; а политики безопасности, такие как запрет смешивания источников во фреймах, существуют не случайно, а ради защиты пользователя.
Несколько ключевых выводов для разработчиков:
Оптимизируйте использование сети: меньше запросов и меньший размер файлов = быстрое начало рендеринга. Браузер многое делает сам (HTTP/2, кэширование, спекулятивная загрузка), но можно помочь ему с помощью подсказок для ресурсов и грамотной политики кэширования. Сетевой стек является высокопроизводительным, но задержка является ключевой.
Структурируйте HTML и CSS эффективно: хорошо организованный DOM и лаконичная CSS-структура (без чрезмерно глубоких деревьев и излишне сложных селекторов) упрощают парсинг и вычисление стилей. Помните, что CSS и DOM формируют вычисленные стили, после чего вычисляется геометрия — значительные изменения DOM или стилей могут вызывать пересчеты.
Группируйте обновления DOM, чтобы избежать многократных пересчетов стилей и разметки. Используйте вкладку «Производительность» в DevTools, чтобы выявить случаи, когда ваш скрипт вызывает лишние пересчеты компоновки или отрисовки.
Для анимаций используйте свойства CSS, совместимые с композицией: анимация свойств
transformилиopacityостается вне основного потока и обрабатывается композитором (compositor), что обеспечивает плавность. По-возможности, избегайте анимирования свойств, влияющих на компоновку.Следите за выполнением JS: несмотря на высокую скорость работы движков JS, длительные задачи блокируют основной поток. Разбивайте тяжелые операции на части, чтобы интерфейс оставался отзывчивым. В некоторых случаях стоит использовать веб-воркеры (Web Workers) для выполнения фоновых задач. Также помните, что интенсивное выполнение JS может вызывать паузы при сборке мусора (в наше время они редки и недолговременны, но могут возникать при значительном увеличении объема используемой памяти).
Используйте механизмы безопасности осознанно — например, применяйте
sandboxдля<iframe>илиrel=noopenerдля<a>, когда это уместно. Теперь вы знаете, что браузер и так изолирует эти элементы — поддержка его механизмов улучшает безопасность и производительность.DevTools — ваш лучший помощник: особенно вкладки Performance и Network, которые четко показывают, что именно делает браузер. Если что-то работает медленно или рывками, инструменты разработчика часто сразу указывают на причину — долгую компоновку, медленную отрисовку и т.д.
Для тех, кто хочет погрузиться еще глубже, отличным ресурсом будет книга «Browser Engineering» Павла Панчеки и Криса Харрелсона (доступна на browser.engineering). Это бесплатная онлайн-книга, где вы шаг за шагом создаете простой веб-браузер: с понятным изложением сетевого взаимодействия, парсинга HTML/CSS, компоновки и других тем. Она может стать отличным дополнением к этой статье и поможет закрепить знания на практике.
Кроме того, серия статей от команды Chrome «Inside look at modern web browser» предлагает доступный обзор с наглядными схемами. Блог V8 (v8.dev) и блог Mozilla Hacks — ценные источники о новейших технологиях движков (новые уровни JIT-компиляции или внутреннее устройство WebRender).
В заключение: современные браузеры — это настоящие шедевры инженерной мысли. Они умело скрывают сложность за простым интерфейсом — мы пишем HTML, CSS и JS, доверяя браузеру все остальное. Но заглянув под капот, мы начинаем понимать, почему одни приемы улучшают производительность, а другие — нет: почему важно не блокировать основной поток, почему стоит избегать избыточной сложности DOM. Мы перестаем просто следовать рекомендациям — и начинаем осознанно выбирать решения. В следующий раз, когда вы будете отлаживать страницу или зададитесь вопросом, почему Chrome или Firefox ведут себя так, а не иначе — у вас уже будет четкая внутренняя модель того, как работает браузер.
Удачи в разработке! Помните: глубина веб-платформы щедро вознаграждает тех, кто стремится ее понять — всегда есть, что узнавать, и инструменты, которые помогут вам в этом.
Дополнительные материалы
Web Browser Engineering — книга о том, как браузеры работают изнутри
Chromium University — бесплатная серия видео с глубокими разборами внутренних механизмов Chromium (особенно классный доклад - Life of a Pixel)
Inside the Browser (серия в блоге Chrome Developers) — четыре части об архитектуре, навигации, рендеринге и потоках ввода
Google Chrome at 17 — история развития браузера Chrome
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩
Комментарии (5)

OlegZH
13.12.2025 09:42Эти меры значительно усложняют эксплуатацию уязвимостей: злоумышленнику теперь нужно использовать цепочку нескольких эксплойтов (например, один для взлома рендерера, другой для выхода из песочницы), чтобы нанести серьезный ущерб.
На этом месте очень хотелось бы привести примеры этого самого ущерба. Что можно сделать? Как только мы это всё тщательно опишем, то мы сможем задать главный вопрос: какова должна быть архитектура системы, которая исключает появление такого ущерба (или, в крайнем случае, существенным образом минимизирует его).

OlegZH
13.12.2025 09:42Следите за выполнением JS: несмотря на высокую скорость работы движков JS, длительные задачи блокируют основной поток. Разбивайте тяжелые операции на части, чтобы интерфейс оставался отзывчивым. В некоторых случаях стоит использовать веб-воркеры (Web Workers) для выполнения фоновых задач. Также помните, что интенсивное выполнение JS может вызывать паузы при сборке мусора (в наше время они редки и недолговременны, но могут возникать при значительном увеличении объема используемой памяти).
Главный вопрос: зачем нужен JavaScript?

flancer
13.12.2025 09:42Для автоматизации действий в браузере - например, валидация вводимых пользователем данных. С этого всё и началось, в общем-то - с кастомизации реакции веб-страницы на действия пользователя. Без какого-либо ЯП этого достичь невозможно в принципе. Предложили LiveScript, который затем стал JavaScript. И понеслось...
Все "странности" JS, начиная с EventLoop, они отсюда - из веб-страницы. Ну и в nodejs перекочевали.

flancer
13.12.2025 09:42Хорошая публикация (y) Браузер уже достаточно давно стоит рассматривать, как интернет-ОС. Пытались делать ChromeOS, но по факту любой браузер уже является операционной системой для выполнения распределённых приложений. Только мы его по инерции воспринимаем, как просмотрщик веб-страничек, хотя AJAX (SPA) появился 20 лет назад, а PWA - десять. Но всё равно - "открыть страничку". Хотя сейчас на "страничке" иногда кода больше, чем текста.
OlegZH
Удивительно! Но!! Всего этого могло не быть. Мы могли до сих пор пользоваться обыкновенными настольными ("нативными") приложениями, использующими родные для ОС компоненты пользовательского интерфейса и общение при помощи сокетов (например). Возможно, был бы предложены каике-то новые протоколы сетевого обмена. Не было бы этого чудовищного нагромаждения технологий и фреймворков. Отладка и тестирование были бы встроенной функцией. (Не надо было бы "тянуть" Selemnium и Playwrite.) Я и не говорю про существенно большую безопасность для пользователя и прозрачность управления.