Парсинг JavaScript и изображений в браузере

Как браузер парсит JavaScript и изображения при рендеринге?

Теория: как идет рендеринг

Во время загрузки документа HTML‑парсер читает HTML потоково и строит DOM‑дерево по мере появления токенов.

Затем (упрощенно) выполняются вычисление стилей и этапы layout/paint, но в контексте задачи важнее следующее: синхронные (блокирующие) скрипты способны остановить парсинг, а изображения обычно грузятся параллельно и не мешают завершить построение DOM.

Сильно упрощенная схема:

HTML bytes
   |
   v
HTML parser  --->  DOM  ----\
   |                         \
   | (находит <script>)       +--> Style calculation --> Layout --> Paint/Composite --> Pixels
   | (находит <img>)         /
   v                        /
Network requests (JS, images, fonts, ...) 

DOMContentLoaded и load

Событие DOMContentLoaded наступает, когда HTML распарсен и отложенные скрипты (например, defer и модульные без async) завершили выполнение.

DOMContentLoaded не обязано ждать загрузки изображений, поэтому тяжелые картинки сами по себе обычно не задерживают это событие, если отсутствуют другие блокировки.

Для корректной инициализации часто проверяется document.readyState, и обработчик DOMContentLoaded добавляется только если состояние равно loading.
// Схема подписки без дублирования
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', init);
} else {
  init();
}

function init() {
  // Инициализация интерфейса
}

Теория: как парсится и выполняется JavaScript

Classic scripts: без атрибутов

Внешний классический <script src="..."></script> без async и defer обычно является парсер‑блокирующим: HTML‑парсер приостанавливается, скрипт загружается и выполняется, после чего парсинг продолжается.

Из‑за этого порядок подключения JS в HTML имеет значение: следующий HTML не будет распарсен, пока текущий скрипт не выполнится.

<!-- Блокирующий пример: парсинг останавливается на script -->
<script src="heavy-lib.js"></script>
<div id="app">Этот div будет распарсен только после выполнения heavy-lib.js</div>

defer и async для classic scripts

defer означает: загрузка идет параллельно парсингу, выполнение откладывается до завершения парсинга; порядок выполнения нескольких defer‑скриптов сохраняется по порядку в HTML.

async означает: загрузка идет параллельно парсингу, но выполнение начинается «как только готово», поэтому порядок между несколькими async‑скриптами не гарантируется.

<!-- defer: порядок сохраняется, выполнение после парсинга -->
<script defer src="vendor.js"></script>
<script defer src="app.js"></script>

<!-- async: порядок не гарантируется, выполнение по готовности -->
<script async src="analytics.js"></script>
<script async src="ads.js"></script>

type="module"

<script type="module"> по умолчанию выполняется после завершения парсинга (если async не задан), то есть ведет себя «как defer по умолчанию».

Атрибут defer для модулей обычно не меняет поведение, а async переводит модуль в режим выполнения по готовности, без гарантий порядка относительно других async‑скриптов.

<!-- module: по умолчанию выполнится после парсинга -->
<script type="module" src="app.mjs"></script>

<!-- module + async: выполнится по готовности, порядок не гарантируется -->
<script type="module" async src="feature-a.mjs"></script>
<script type="module" async src="feature-b.mjs"></script>

Сводная таблица по скриптам

Вариант подключенияСкачиваниеВыполнениеПорядокБлокирует парсинг HTML
Classic без атрибутовЧасто «в лоб»: парсер ждет, пока скрипт загрузитсяНемедленно при встрече тегаПо месту в HTMLДа
Classic deferПараллельно парсингуПосле завершения парсингаГарантированНет
Classic asyncПараллельно парсингуПо готовности (возможно до конца парсинга)Не гарантированВо время загрузки нет, но выполнение прерывает работу
type="module" (без async)Параллельно парсингу, включая зависимостиПосле завершения парсингаНа практике предсказуемо, ключевое — «после парсинга»Нет
Важно отличать «блокировку парсинга» от «блокировки отрисовки»: скрипт может не останавливать парсер (defer/module), но все равно откладывать момент, когда интерфейс становится интерактивным, если инициализация зависит от JavaScript.

Теория: как обрабатываются изображения

Почему img обычно не блокирует парсер

Построение DOM не требует, чтобы изображения были загружены и декодированы, поэтому HTML может быть распарсен полностью, даже если картинки еще в процессе загрузки.

При этом изображения влияют на то, насколько быстро страница выглядит «готовой», потому что для показа нужен декодированный bitmap, а декодирование может занимать заметное время.

Размеры и сдвиги макета

Атрибуты width и height позволяют заранее зарезервировать место под изображение до загрузки файла, снижая вероятность перерасчета макета при появлении картинки.

Если размеры не заданы, место под изображение может уточняться поздно, что повышает вероятность сдвигов layout.

<img
  src="photo.jpg"
  alt="Описание"
  width="800"
  height="600"
  decoding="async"
  fetchpriority="low"
>

decoding: sync/async/auto

Декодирование — это преобразование сжатых данных изображения в формат пикселей для отрисовки.

decoding="sync" является подсказкой предпочесть синхронное декодирование, а decoding="async" — подсказкой предпочесть асинхронное декодирование, чтобы уменьшить вероятность задержки отрисовки другого контента.

decoding="auto" означает, что решение остается за браузером.

loading и fetchpriority

loading="lazy" включает ленивую загрузку: запрос на изображение обычно откладывается до приближения к области видимости.

fetchpriority предназначен для влияния на приоритет загрузки: для важного изображения первого экрана может устанавливаться высокий приоритет, а для второстепенных — низкий.

<!-- Типовой вариант: важное изображение первого экрана — eager/high,
     изображения ниже — lazy/low -->

<img
  src="hero.jpg"
  alt="Hero"
  width="1200"
  height="630"
  loading="eager"
  fetchpriority="high"
  decoding="async"
>

<img
  src="gallery-1.jpg"
  alt="Gallery 1"
  width="800"
  height="600"
  loading="lazy"
  fetchpriority="low"
  decoding="async"
>

Кратко: верным является вариант 2 — классический <script> без атрибутов блокирует парсинг, defer выполняется после парсинга и сохраняет порядок, async выполняется по готовности без гарантий порядка, а type="module" по умолчанию откладывает выполнение до конца парсинга. <img> обычно не блокирует парсинг, но влияет на визуальную стабильность через размеры и на производительность через время декодирования; loading/decoding/fetchpriority управляют моментом загрузки, планированием декодирования и приоритетом запроса.