← Курс/Как браузер загружает страницу?#125 из 257+40 XP

Как браузер загружает страницу?

Краткий ответ

Загрузка страницы — это многоступенчатый процесс: сначала браузер устанавливает сетевое соединение (DNS + TCP), получает HTML и парсит его в DOM-дерево, параллельно строит CSSOM из стилей, затем объединяет их в Render Tree, вычисляет расположение элементов (Layout), рисует пиксели (Paint) и выводит результат на экран (Compositing). Понимание этого процесса критично для оптимизации производительности сайта.

Полный разбор

Этап 1: Сеть

[Браузер] → DNS lookup → IP-адрес сервера
         → TCP Handshake (SYN → SYN-ACK → ACK)
         → TLS Handshake (для HTTPS)
         → HTTP GET /index.html
         ← HTTP 200 OK + тело HTML

**DNS lookup** — перевод домена (example.com) в IP-адрес. Результат кешируется.

**TCP соединение** — три рукопожатия перед передачей данных.

**TTFB (Time To First Byte)** — время от запроса до первого байта ответа. Важная метрика.

Этап 2: Парсинг HTML → DOM

Браузер читает HTML сверху вниз и строит **DOM-дерево** (Document Object Model):

HTML-текст → Tokenizer → Parser → DOM Tree

<html>
  <head>...</head>
  <body>
    <div id="app">
      <p>Hello</p>
    </div>
  </body>
</html>

      Document
        html
       /    \
    head    body
             div#app
              p
           "Hello"

Этап 3: Парсинг CSS → CSSOM

Параллельно с DOM строится **CSSOM** (CSS Object Model). CSS — **блокирующий ресурс для рендеринга**: браузер не начнёт отрисовку, пока не загрузит и не распарсит все CSS-файлы в <head>.

Этап 4: JavaScript — блокировка парсинга

Синхронный <script> **блокирует парсинг HTML**:

HTML парсится → встретили <script src="app.js"> →
СТОП парсинга → скачать app.js → выполнить →
ПРОДОЛЖИТЬ парсинг HTML

Почему? Скрипт может вызвать document.write() и полностью изменить HTML.

**async** и **defer** убирают блокировку:

Обычный:  HTML ███░░░░░░███  (░ = пауза на скрипт)
async:    HTML ████████████  (скрипт скачивается параллельно, выполняется сразу)
defer:    HTML ████████████  (скрипт скачивается параллельно, выполняется после парсинга)

| Атрибут | Скачивание | Выполнение | Порядок |

|---------|-----------|-----------|---------|

| (нет) | блокирует | сразу | да |

| async | параллельно | как скачается | нет |

| defer | параллельно | после парсинга HTML | да |

Этап 5: Render Tree

DOM + CSSOM объединяются в **Render Tree** — только видимые элементы с применёнными стилями. Элементы с display: none не попадают в Render Tree.

Этап 6: Layout (Reflow)

Браузер вычисляет **точные размеры и позиции** каждого элемента. Это дорогая операция — изменение геометрии любого элемента может перезапустить Layout для всей страницы.

Этап 7: Paint

Браузер рисует пиксели: цвета, тексты, изображения, тени. Каждый слой рисуется отдельно.

Этап 8: Compositing

GPU объединяет слои в финальное изображение на экране.

События загрузки

// DOMContentLoaded — HTML распарсен, DOM готов
// Скрипты с defer выполнились. Картинки могут ещё грузиться.
document.addEventListener('DOMContentLoaded', () => {
  console.log('DOM готов — можно работать с элементами')
})

// load — ВСЁ загружено: картинки, стили, скрипты
window.addEventListener('load', () => {
  console.log('Страница полностью загружена')
})

**Правило**: используй DOMContentLoaded для инициализации UI. Используй load только когда нужны размеры картинок.

Критический путь рендеринга (Critical Rendering Path)

Минимальная цепочка для первой отрисовки страницы:

HTML → DOM → Render Tree → Layout → Paint
       CSS → CSSOM ↗

Оптимизация CRP:

  • CSS — в <head> (чтобы не блокировать рендеринг после парсинга)
  • JS — в конце <body> или с defer
  • Минимизировать блокирующие ресурсы
  • Использовать <link rel="preload"> для критических ресурсов
  • Связанные уроки курса

  • DOM — работа с деревом документа
  • Жизненный цикл страницы (DOMContentLoaded, load, beforeunload)
  • async и defer скрипты
  • Как отвечать на собеседовании

    **Начни с общей картины**: "Загрузка страницы проходит в несколько этапов: сеть, парсинг, рендеринг". Это показывает системное мышление.

    **Обязательно упомяни**: блокирующий характер синхронных скриптов и CSS — это ключевой момент для оптимизации. Разницу между DOMContentLoaded и load события.

    **Хорошее дополнение**: упомяни Critical Rendering Path и что ты делал бы для оптимизации (defer, preload, CSS в head).

    **Время ответа**: 2-3 минуты. Можно нарисовать схему на доске.

    Красные флаги ответа

    1. **"Браузер просто открывает страницу"** — полное непонимание процесса. Это показывает, что кандидат никогда не занимался оптимизацией.

    2. **Путаница DOMContentLoaded и load** — очень частая ошибка, из-за которой возникают баги с "элемент не найден".

    3. **Незнание про блокирующие скрипты** — если не знаешь, почему <script> в <head> без defer замедляет загрузку, это серьёзный пробел.

    Примеры

    Симуляция последовательности загрузки браузера с временными метками

    // Симуляция этапов загрузки браузера
    // В реальности эти этапы выполняет движок браузера
    
    function simulateBrowserLoading(url) {
      const steps = []
      const startTime = Date.now()
    
      function log(step, detail = '') {
        const elapsed = Date.now() - startTime
        steps.push({ step, detail, ms: elapsed })
        console.log(`[${String(elapsed).padStart(4)}ms] ${step}${detail ? ': ' + detail : ''}`)
      }
    
      console.log(`=== Загрузка ${url} ===\n`)
    
      // 1. Сеть
      log('DNS lookup', 'example.com → 93.184.216.34')
      log('TCP Handshake', 'SYN → SYN-ACK → ACK')
      log('TLS Handshake', 'HTTPS шифрование')
      log('HTTP GET', `GET / HTTP/1.1 Host: ${url}`)
      log('HTTP 200 OK', 'Получен HTML (12 KB)')
    
      // 2. Парсинг
      log('HTML Parsing', 'Построение DOM-дерева...')
      log('CSS Found', 'Найден <link rel="stylesheet"> — блокирует рендеринг!')
      log('CSS Parsing', 'Построение CSSOM...')
      log('Script Found', 'Найден <script defer> — скачивается параллельно')
      log('DOM Ready', 'DOMContentLoaded событие')
    
      // 3. Рендеринг
      log('Render Tree', 'DOM + CSSOM объединены')
      log('Layout', 'Вычисление позиций и размеров элементов')
      log('Paint', 'Отрисовка пикселей')
      log('Compositing', 'GPU объединяет слои')
    
      // 4. Финал
      log('Script Executed', 'defer-скрипты выполнены')
      log('Images Loaded', 'Все ресурсы загружены')
      log('load event', 'window.onload вызван')
    
      console.log('\n=== Итоговая последовательность ===')
      return steps
    }
    
    simulateBrowserLoading('example.com')
    
    // Демонстрация разницы async/defer/обычный
    console.log('\n=== Влияние скриптов на парсинг ===')
    
    const scenarios = [
      {
        type: 'Обычный <script>',
        timeline: [
          'Парсинг HTML....',
          '⏸️  ПАУЗА: скачиваем script.js',
          '⏸️  ПАУЗА: выполняем script.js',
          'Продолжаем парсинг HTML',
          'DOMContentLoaded'
        ]
      },
      {
        type: '<script async>',
        timeline: [
          'Парсинг HTML.... (параллельно скачиваем script.js)',
          '⏸️  ПАУЗА: выполняем script.js (как только скачался)',
          'Продолжаем парсинг HTML',
          'DOMContentLoaded'
        ]
      },
      {
        type: '<script defer>',
        timeline: [
          'Парсинг HTML.... (параллельно скачиваем script.js)',
          'DOMContentLoaded ← скрипт выполняется ЗДЕСЬ',
          'Выполняем script.js (после полного парсинга)',
        ]
      }
    ]
    
    for (const scenario of scenarios) {
      console.log(`\n${scenario.type}:`)
      scenario.timeline.forEach((step, i) => {
        console.log(`  ${i + 1}. ${step}`)
      })
    }