Проект ЦИТадель
Браузер одновременно является сетевым клиентом, средой исполнения JavaScript, движком разметки и границей безопасности между сайтами. Чтобы понимать задержки интерфейса и ограничения веб-приложений, полезно проследить путь от URL до кадра: как загружаются ресурсы, какие стадии превращают документ в изображение, когда выполняются задачи JavaScript и как процессы браузера изолируют недоверенный код. Общие правила задаются стандартами веб-платформы, но распределение работы по потокам и процессам, устройство JIT-компилятора и детали рендеринга различаются между движками. В практикуме эти механизмы исследуются через инструменты разработчика.
После разбора URL браузер определяет политику запроса, проверяет доступные кэши и при необходимости разрешает имя, устанавливает соединение и отправляет HTTP-запрос. Сетевые стадии подробно разобраны в статьях «TLS: как устроено защищённое соединение» и «HTTP/2 и HTTP/3». Здесь важны три особенности клиента:
Путь удобно описывать пятью логическими стадиями. Это не обязательная последовательность полного пересчёта: браузер обрабатывает данные постепенно, переиспользует прежние результаты и может выполнять часть работы параллельно.
defer откладывает исполнение до окончания разбора, а async разрешает исполнить загруженный скрипт, как только он будет готов. На точный момент также влияют зависимости от таблиц стилей и модульные скрипты.transform и opacity нередко можно применить без новой раскладки и отрисовки, но создание слоя и аппаратное ускорение не гарантируются одним названием свойства.Изменение DOM или стилей помечает часть прежних результатов недействительной. Стоимость зависит от области пересчёта и от того, потребовались ли стили, раскладка, отрисовка или только композиция. Графическая сторона последней стадии подробнее рассмотрена в статье «GPU и графический конвейер».
Скрипты окна, DOM и значительная часть стилей и раскладки обслуживаются главным потоком соответствующего контекста исполнения. Это не единственный поток браузера: сеть, композиция, растеризация, декодирование и сборка мусора могут использовать другие потоки и процессы. Порядок доступной JavaScript работы задаёт цикл событий с несколькими источниками задач и очередью микрозадач.
SharedArrayBuffer агенты могут разделять память и обязаны синхронизироваться атомарными операциями; сообщения сами по себе не исключают все гонки. Общие модели конкурентности рассмотрены в отдельной статье.Движки V8, JavaScriptCore и SpiderMonkey используют несколько уровней исполнения, но их устройство и решения меняются независимо. Например, современный V8 может разобрать код в байткод интерпретатора Ignition, быстро скомпилировать его базовым компилятором Sparkplug, а часто выполняемые участки передать оптимизирующим компиляторам. Они используют сведения о наблюдавшихся типах и формах объектов; если предположение перестаёт выполняться, код может быть деоптимизирован. Из этого не следует, что любое изменение формы оставляет функцию в интерпретаторе или что прикладной код следует подгонять под внутренности конкретной версии. Сначала нужны профиль и измерение.
Память JavaScript управляется трассирующим сборщиком. V8, например, сочетает поколения с параллельными и конкурентными фазами, но другой движок вправе выбрать иную организацию. Сборщик освобождает недостижимые объекты; подписка, замыкание или неограниченный кэш могут по-прежнему удерживать ненужный граф. Механизмы и способы наблюдения разобраны в статье «Как современные языки управляют памятью».
Современные браузеры разделяют работу между процессами, но точная схема зависит от продукта, платформы и ограничений ресурсов. В Chromium есть основной процесс браузера, процессы рендереров и отдельные службы, в том числе сетевые и графические. Site Isolation стремится не помещать документы разных сайтов в один рендерер; это не тождественно правилу «одна вкладка — один процесс», потому что вкладка может содержать межсайтовые фреймы, а несколько документов одного сайта могут переиспользовать процесс. Рендереры работают с ограниченными полномочиями. Песочница и изоляция сайтов уменьшают последствия уязвимости, но остаются слоями защиты, а не доказательством невозможности выхода из процесса.
Правило одного источника (same-origin policy) ограничивает взаимодействие документов и сценариев разных источников; источник обычно задаётся схемой, именем хоста и портом. Оно разделяет DOM и хранилища и запрещает сценариям многие межисточниковые чтения. При этом переходы, отправка форм и встраивание некоторых ресурсов могут быть разрешены. CORS позволяет серверу сообщить браузеру, каким источникам разрешено читать ответ программно; он не отменяет проверку прав на сервере.
XSS и CSRF связаны с этой границей по-разному. При XSS чужой код выполняется с полномочиями уязвимого источника. При CSRF браузер побуждают отправить допустимый для протокола запрос к сайту, где у пользователя есть сеанс; отправка учётных данных зависит от вида запроса, режима fetch, настроек cookie и политики SameSite. Поэтому защита от XSS включает контекстное кодирование или санитизацию данных и CSP как дополнительный слой, а защита от CSRF — отсутствие изменений состояния в безопасных HTTP-методах, CSRF-токены, проверку контекста запроса и подходящие cookie. CORS сам по себе не является защитой от всех таких атак.
Лучше выполнять задания на собственной или специально открытой тестовой странице, предварительно сохранив несохранённые данные. Названия панелей и событий различаются между версиями браузеров.
Задание 1. Конвейер в записи. Запишите загрузку страницы в панели производительности. Найдите события разбора HTML, вычисления стилей, раскладки, отрисовки и композиции, если браузер показывает их отдельно. Отметьте, какие стадии перекрываются с загрузкой ресурсов и какие повторяются после исполнения скрипта. Не ожидайте одной непрерывной последовательности из пяти блоков.
Задание 2. Длинная задача. Во время записи выполните в консоли контролируемое ожидание около секунды: const end = performance.now() + 1000; while (performance.now() < end) {}. Проверьте реакцию страницы на ввод и найдите задачу на временной шкале. Затем разбейте искусственную работу на части с уступкой циклу событий и сравните отзывчивость и полное время.
Задание 3. Раскладка и композиция. Сравните две анимации одинакового движения: изменением left у позиционированного элемента и свойством transform. Включите подсветку перерисовок и запишите обе. Опишите наблюдавшиеся стадии, но не объявляйте второй вариант композиционным без подтверждения записи: повышение элемента в слой зависит от реализации.
Задание 4. Микрозадачи и задачи. Выполните setTimeout(() => console.log('задача'), 0); Promise.resolve().then(() => console.log('микрозадача')); console.log('синхронно'); и объясните порядок. Затем создайте ограниченную цепочку из нескольких тысяч микрозадач и проверьте, когда браузер сможет отрисовать заранее запрошенный requestAnimationFrame.
Задание 5. Процессы и источники. В диспетчере задач браузера найдите доступные процессы рендереров, сети и GPU и сопоставьте их с открытыми документами и межсайтовым фреймом. Затем выполните fetch к ресурсу другого источника без разрешающего CORS-заголовка. Сравните ошибку JavaScript с записью в панели Network: простой запрос мог уйти, а предварительно проверяемый — остановиться после OPTIONS. Отдельно учтите, что fetch по умолчанию не посылает cookie произвольному другому источнику.
Задание 6*. Кэш и приоритеты. При включённом кэше сравните обычную загрузку, перезагрузку и повторный переход. Найдите ответы из памяти или диска, условные запросы и статус 304. Выпишите наблюдаемые приоритеты ресурсов и проверьте, меняются ли они после обнаружения ресурса в документе. Не путайте настройку DevTools «Disable cache» с поведением обычного пользователя.