---
title: "Навигация: переходы и ожидание загрузки"
description: "chromedp navigate и ожидание загрузки на Go: что ждёт Navigate, чем WaitReady отличается от WaitVisible, почему time.Sleep ломает парсер — таблица ожиданий."
course: "Chromedp в Go: автоматизация Chrome без Selenium"
url: "https://golangmaster.ru/courses/chromedp/lessons/03-navigaciya-zagruzka/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# Навигация: переходы и ожидание загрузки

*Курс «Chromedp в Go: автоматизация Chrome без Selenium», урок 3 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/chromedp/lessons/03-navigaciya-zagruzka/*

Первая версия браузерного парсера почти всегда падает одинаково: переход есть, а элемента ещё нет — chromedp navigate вернул управление, JavaScript не успел дорисовать контент, и запрос упал по таймауту. Ожидание загрузки — главный навык в автоматизации браузера на Go: разберём, что именно дожидается Navigate, чем WaitReady отличается от WaitVisible и почему time.Sleep превращает парсер в лотерею.

Запустите парсер на странице с аналитикой и медленным соединением — и получите textbook-ошибку: Run вернул превышение дедлайна ожидания, хотя сайт в браузере открывается без проблем. Причина в том, что «страница загрузилась» — фраза с тремя разными смыслами. Смысл первый: браузер получил HTML и отработал событие load. Смысл второй: узлы, которые вам нужны, появились в DOM. Смысл третий: они отрисованы и видимы — до них можно дотянуться кликом или текстом. HTTP-парсеру достаточно первого смысла, потому что ответ сервера и есть страница. Браузерной программе мало: между load и готовностью контента живёт весь динамический слой — XHR-запросы, рендер, анимации. В этом уроке разберём, что chromedp делает на каждом уровне: что обещает Navigate, какие условия проверяют WaitReady и WaitVisible и почему сон фиксированной длины — это решение, которое работает на вашем ноутбуке и разваливается на CI.

### chromedp navigate: что происходит при переходе

chromedp.Navigate отправляет браузеру команду смены адреса и возвращает управление тогда, когда страница загрузилась — то есть дождалась события load: основная разметка приехала, отработали синхронные скрипты, браузер объявил загрузку завершённой. Это важная гарантия: после Navigate дерево документа уже содержит то, что сервер отдал, — заголовки, шапку, статический каркас. Но она же и граница: всё, что появляется после load, Navigate не дожидается. Данные, подтянутые XHR-запросами, элементы, собранные React или Vue, счётчики и виджеты — к моменту возврата из Navigate их ещё нет. Отсюда правило чтения страниц: Navigate отвечает за смену адреса, а за появление контента отвечают действия с условиями — запросы, которые переспрашивают документ, пока селектор не совпадёт. Эти два уровня часто путают в одну кучу и потом недоумевают, почему «Navigate уже вернулся, а заголовка нет». Разделение простое: переход — синхронный про load, контент — асинхронный про DOM. Полезно знать и механику: Navigate шлёт браузеру команду навигации, а затем ждёт события о завершении загрузки, поэтому «вернулся из Navigate» — факт, подтверждённый браузером, а не догадка по таймеру. Каждый переход в одном контексте заменяет текущую страницу: по сути это то, что делает человек в адресной строке, только из кода.

### Переход и чтение: три уровня готовности в одном Run

```go
var title string
err := chromedp.Run(ctx,
        chromedp.Navigate("https://example.com/news/"),
        chromedp.WaitReady("body"),
        chromedp.WaitVisible("h1"),
        chromedp.Text("h1", &title),
)
```

Navigate дожидается события load — HTML на месте. WaitReady подтверждает, что body есть в DOM, WaitVisible — что заголовок отрисован и имеет реальные размеры, Text забирает его текст. Цепочка избыточна намеренно: она показывает, что переход и готовность контента — разные уровни, и каждый проверяется своим действием.

### Два уровня готовности: WaitReady и WaitVisible

WaitReady и WaitVisible выглядят как близнецы, но проверяют разное, и разница эта стоит часов отладки. WaitReady ждёт, пока узел по селектору появится в дереве документа: запрос переспрашивает DOM с интервалом в несколько миллисекунд, и как только элемент найден — действие исполняется. Это готовность структурная: элемент существует, с ним можно работать через DOM. WaitVisible добавляет второе условие: узел не просто есть, он виден — у него есть реальные размеры и прямоугольник на странице, то есть offsetWidth или offsetHeight отличны от нуля и CSS его не спрятал через display или visibility. Элемент может существовать в DOM и при этом быть невидимым: скрытый шаблон, вкладка, которую никто не открывал, спиннер с нулевыми габаритами. Для чтения текста хватает WaitReady, для клика нужен WaitVisible — клик идёт по координатам, а у невидимого узла координат нет. В хвосте цепочки стоит и третий нюанс: все Query-действия в chromedp уже содержат ожидание готовности по умолчанию, поэтому Text без предварительного WaitVisible всё равно дождётся узла — явное ожидание в примерах ставят ради читаемости и контроля условия. Сам поллинг дешёвый: один переспрос — это оценка крошечной функции в живой странице, а не сетевой запрос, поэтому интервал в несколько миллисекунд не грузит ни сеть, ни процессор.

### sleep против событий: почему time.Sleep ломает парсер

Самая человеческая идея при работе с динамической страницей — «подождать чуть-чуть и потом читать»: вставить Sleep на пару секунд и надеяться, что контент успеет. На одном ноутбуке это даже работает — и закрепляется как привычка. Дальше начинается то, что делает ожидание загрузки инженерной темой, а не кулинарной. Фиксированный сон не связан с фактом готовности: он либо короткий, и парсер падает на медленных страницах, либо длинный, и программа тратит секунды жизни на каждом из тысяч переходов. Хуже того — режим лотереи: на границе таймингов парсер работает через раз, и такие плавающие падения ловятся хуже всего. Правильная альтернатива — событийное ожидание: условия WaitReady, WaitVisible и компании проверяются повторно с малым интервалом, пока факт не случится, поэтому страница готова ровно тогда, когда это реально так, — ни раньше, ни позже. У chromedp есть и собственный chromedp.Sleep — действие, которое уважает отмену контекста, в отличие от голого time.Sleep, но назначение у него то же ограниченное: паузы между действиями, а не ожидание готовности. Для тонких сценариев — поллинг по своему условию — существует отдельный механизм, который разбирается в уроке про [умные ожидания](/courses/chromedp/lessons/09-ozhidaniya-elementov/).

> **Живое место: две секунды, которые работали полгода.** Парсер с Sleep(2 * time.Second) месяцами собирает каталог на быстром интернете — и падает в день, когда сайт добавляет аналитику, замедляющую рендер до трёх секунд. Дальше по классике: increase до пяти секунд — и уже полный обход отнимает часы, потому что сон платится за каждый переход. Спорное мнение, которое стоит держать в голове: иногда Sleep всё же оправдан — например, когда на странице анимация, которая всегда длится известное время, и событие завершения недоступно. Но это заплатка с известным сроком годности, а не инженерия ожидания.

### Инструменты ожидания chromedp

| Инструмент | Что дожидается | Когда брать |
| --- | --- | --- |
| chromedp.Navigate | событие load страницы | всякая смена адреса — переходы и по клику, и по коду |
| chromedp.WaitReady | появление узла в DOM | чтение текста и атрибутов скрытых и видимых узлов |
| chromedp.WaitVisible | узел в DOM плюс реальные размеры | клики, ввод, скриншоты — всё, что требует видимости |
| chromedp.WaitEnabled | узел без атрибута disabled | кнопки, которые активируются после заполнения формы |
| chromedp.WaitNotPresent | исчезновение узлов по селектору | спиннеры загрузки и оверлеи, мешающие следующим шагам |

### Несколько переходов в одном контексте

Контекст chromedp — это сессия, а не одиночный переход: Navigate можно вызывать сколько угодно раз, страница заменяется, а вкладка и подключение живут дальше. На этом строится обход с пагинацией: переход на первую страницу, сбор, переход на вторую — тот же ctx, тот же браузер, история переходов копится штатно. Для движения по истории есть отдельные действия возврата и обновления страницы — Reload перезагружает текущий адрес, NavigateBack шагает назад, как кнопка в браузере, — они пригодятся, когда нужное состояние проще получить повторным открытием, чем репетицией кликов. Практический смысл держать один контекст экономический: браузер уже запущен и прогрет, повторный переход стоит в разы меньше, чем старт новой сессии. Единственное, за чем стоит следить, — состояние между переходами: куки и localStorage живут в сессии, поэтому переходы в одном контексте ведут себя как работа одного человека, а не отдельные визиты. Внутри одного Run действия исполняются строго по порядку, поэтому Navigate, затем ожидание, затем чтение образуют надёжный сценарий. Если же страниц десятки и сотни, последовательность упирается в скорость одного браузера — там начинается архитектура вкладок и пула, но это уже уровень, до которого нужно дойти с работающими ожиданиями.

### Дедлайн контекста: таймаут на прогон

Ожидание в chromedp опирается на контекст: пока условие не выполнено, запрос переспрашивает документ, а когда контекст отменён — всё останавливается с ошибкой дедлайна. Это делает context.WithTimeout честным предохранителем всей программы: не «подожду три секунды и хватит», а «у прогона есть бюджет времени, и любое висящее ожидание его израсходует». Дедлайн вешают на сессию целиком — переходы, ожидания, чтение; при превышении Run вернёт context deadline exceeded, и chromedp остановит браузер, потому что таймаут накрывает и его жизненный цикл. Отсюда два практических правила. Первое: бюджет закладывайте с запасом на старт браузера — истёкший в момент запуска дедлайн убивает весь процесс, а не только текущее действие. Второе: ошибку дедлайна стоит различать явно — она означает «страница или сайт не готовы», а не баг вашего кода, и обрабатывается ретраем или записью в проблемный список. Грамотная обработка таймаутов и повторов — тема отдельного урока про [таймауты и устойчивость](/courses/chromedp/lessons/15-tajmauty-i-prochnost/), здесь же важно само умение читать эту ошибку как сигнал о сети, а не как случайность.

### Дедлайн и честная обработка ошибки

```go
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()

ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
defer cancel()

err := chromedp.Run(ctx,
        chromedp.Navigate(url),
        chromedp.WaitVisible(".price"),
)
if errors.Is(err, context.DeadlineExceeded) {
        log.Printf("не дождались цены за 30 секунд: %s", url)
        return
}
```

Таймаут накрывает весь прогон: если цена не отрисовалась за полминуты, Run вернёт context.DeadlineExceeded, а errors.Is отделит её от других ошибок — например, проблем с сетью. Лог с адресом страницы — материал для ретрая или проблемного списка, а не тихий сбой, который никто не заметил.

> **Спиннеры: дожидайтесь их исчезновения.** У ожиданий есть зеркальный сценарий: мешает не отсутствие элемента, а присутствие лишнего. Оверлей загрузки или крутящийся спиннер перекрывает страницу, и клик по кнопке под ним просто не пройдёт. Для этого есть WaitNotPresent — действие, которое ждёт исчезновения узлов по селектору. Схема: дожидаетесь появления спиннера после действия, затем его исчезновения — и только потом работаете с контентом. Такой паре ожиданий достаточно в большинстве витринных сайтов.

### Правила ожидания одним списком

- Navigate дожидается только load — за контент после load отвечают условия на узлы
- WaitReady — для существования узла в DOM, WaitVisible — для реальной видимости
- клик и ввод требуют видимости: у невидимого узла нет координат для событий
- time.Sleep не связан с готовностью страницы — только chromedp.Sleep как пауза между шагами
- у каждого прогона есть дедлайн через context.WithTimeout — ошибки дедлайна различают явно
- зеркальные ожидания тоже нужны: спиннер дожидается появления и исчезновения

Отдельного слова заслуживают SPA-страницы, где после load начинается самое интересное: фреймворк собирает вёрстку, данные едут отдельными запросами, и полсекунды разницы решают, увидит парсер карточки или пустой каркас. Здесь связка Navigate плюс условия на узлы показывает силу: программа ждёт не время, а факт — появление конкретного элемента, пусть даже через пять секунд после перехода. Как такие страницы устроены изнутри и какие ловушки в них живут — тема урока про [SPA и динамический контент](/courses/chromedp/lessons/11-dinamicheskij-kontent/). Напоминание про песочницу: все примеры этого урока ходят по сети и требуют локального Go с установленным Chrome — запускайте их у себя в терминале; интерактивы сайта проверяют понимание уровней готовности, а не исполняют браузер.

### Проверка: что обещает Navigate

**Вопрос:** chromedp.Navigate вернул управление. Что уже точно произошло?

- ✅ Сработало событие load: HTML получен и обработан
- Все XHR-запросы страницы завершены
- Отрисованы картинки и шрифты
- Исполнен весь JavaScript на странице

Navigate дожидается загрузки страницы — события load. Контент, который появляется после load (XHR-данные, рендер фреймворков), не входит в обещание: его дожидаются условия на узлы вроде WaitVisible.

### Проверка: разница ожиданий

**Вопрос:** Чем WaitVisible отличается от WaitReady?

- Ничем — это алиасы одного условия
- ✅ WaitReady ждёт узел в DOM, WaitVisible — ещё и реальные размеры и не-скрытость
- WaitReady работает только с body
- WaitVisible не дожидается ничего — проверяет один раз

WaitReady проверяет структурную готовность: узел найден в дереве. WaitVisible добавляет условие видимости — наличие размеров (offsetWidth или offsetHeight) и отсутствие CSS-скрытия. Для клика нужна именно видимость: событие идёт по координатам.

### Медленная страница и три секунды сна

```go
// ctx, cancel := chromedp.NewContext(context.Background())
// ctx, cancel = context.WithTimeout(ctx, 10*time.Second)
// defer cancel()
// err := chromedp.Run(ctx,
//   chromedp.Navigate(url),
//   chromedp.Sleep(3*time.Second),
//   chromedp.Text(".price", &price),
// )
// Рендер цены занял 5 секунд. Что произойдёт?
```

**Что выведет программа?**

- Sleep растянется до пяти секунд, price заполнится
- ✅ Sleep закончится через три секунды, Text продолжит ждать узел — цена появится на пятой секунде
- Run вернёт ошибку дедлайна через три секунды
- Text вернёт пустую строку без ошибки

Text — Query-действие: оно переспрашивает документ, пока узел не появится или не истечёт дедлайн контекста. Поэтому на пятой секунде цена будет прочитана. Sleep здесь просто потратил три секунды впустую — факт готовности он не проверял.

### Соберите цепочку готовности

Восстановите действия: переход, готовность дерева, видимость заголовка и чтение текста.

```go
var title string
err := chromedp.Run(ctx,
        chromedp.___("https://example.com/news/"),
        chromedp.___("body"),
        chromedp.___("h1"),
        chromedp.Text("h1", &title),
)
```

Подсказка: Переход делает Navigate, существование в DOM проверяет WaitReady, отрисовку — WaitVisible.

Правильные ответы в пропусках: `Navigate`, `WaitReady`, `WaitVisible`

### Расставьте каркас прогона

Соберите по порядку программу с дедлайном: от создания контекста до перехода и ожидания заголовка.

Правильный порядок строк:

1. `ctx, cancel := chromedp.NewContext(context.Background())`
2. `defer cancel()`
3. `ctx, cancel = context.WithTimeout(ctx, 30*time.Second)`
4. `defer cancel() // снимает дедлайн`
5. `err := chromedp.Run(ctx,`
6. `  chromedp.Navigate("https://example.com/"),`
7. `  chromedp.WaitVisible("h1"),`
8. `)`

### Карточки: ожидания загрузки

- **Что обещает Navigate** — Дожидается события load: HTML получен и обработан. Контент после load — XHR и рендер — в обещание не входит.
- **WaitReady** — Дожидается появления узла по селектору в DOM. Запрос переспрашивает документ с интервалом в несколько миллисекунд.
- **WaitVisible** — WaitReady плюс видимость: у узла есть реальные размеры и он не скрыт CSS. Обязателен перед кликом и вводом.
- **chromedp.Sleep против time.Sleep** — Оба — фиксированная пауза, но chromedp.Sleep уважает отмену контекста. Для готовности страницы не годятся — нужны условия на узлы.
- **context deadline exceeded** — Ошибка истёкшего дедлайна: страница не готова в бюджете времени. Различается через errors.Is и обрабатывается ретраем.
- **WaitNotPresent** — Зеркальное ожидание: дожидается исчезновения узлов по селектору. Для спиннеров и оверлеев, перекрывающих страницу.

Что дальше: переходы и ожидания — двигатель, а следующая ступень — прицел. Четвёртый урок про селекторы даст систему поиска элементов; здесь же отметим два направления. Если захочется довести ожидания до инженерной точности — поллинг по своему условию и редкие условия — держите в плане [урок про умные ожидания](/courses/chromedp/lessons/09-ozhidaniya-elementov/). А когда парсер начнёт ходить по десяткам страниц, вернитесь к пятому уроку о [кликах и вводе](/courses/chromedp/lessons/05-klik-vvod/) — действия пользователя и ожидания всегда идут парой.

## Коротко: что запомнить

- «Страница загрузилась» имеет три уровня: событие load, узел в DOM, узел отрисован — chromedp даёт инструменты на каждый.
- Navigate дожидается load: разметка от сервера на месте, но XHR-данные и рендер фреймворков — позже.
- WaitReady — структурная готовность (узел в DOM), WaitVisible добавляет реальные размеры и не-скрытость.
- Query-действия переспрашивают документ с малым интервалом, пока селектор не совпадёт или не истечёт дедлайн.
- time.Sleep не связан с фактом готовности: на быстрых страницах — потерянные секунды, на границе таймингов — плавающие падения.
- Дедлайн через context.WithTimeout накрывает прогон целиком; DeadlineExceeded различают через errors.Is и решают ретраем.
- Контекст — сессия: Navigate можно звать многократно, а спиннеры дожидаются появления и исчезновения.

## Домашнее задание

Возьмите любую страницу с динамическим блоком — например, ленту новостей, где карточки подтягиваются после открытия. Напишите программу с Navigate, дедлайном и WaitVisible на заголовок, затем намеренно воспроизведите обе ошибки урока: уберите ожидание и оставьте только Navigate перед чтением, а после — замените WaitVisible на Sleep. Зафиксируйте в терминале, как ведёт себя каждая версия, сформулируйте, почему событийное ожидание выиграло, и запишите, на каком уровне готовности ваша страница ломается без ожидания.

## Вопросы и ответы по уроку

### Нужно ли ставить WaitVisible перед каждым действием?

Строго говоря, нет: Query-действия и так дожидаются готовности узла по умолчанию. Явное WaitVisible ставят, когда важно именно видимое состояние — перед кликом, вводом или скриншотом, — и ради читаемости: по коду видно, какое условие программа считает фактом готовности. Если условий несколько, цепочку можно разбить на несколько Run — каждый вернёт свою ошибку, и по логу будет видно, какое именно условие не сбылось.

### Почему нельзя просто поставить большой Sleep и не думать?

Потому что сон не проверяет факт: на медленной странице он окажется коротким и парсер упадёт, а на быстрых — съест лишние секунды на каждом из тысяч переходов. Плюс режим лотереи на границе таймингов: плавающие падения, которые ловятся хуже всего. Условия на узлы дешёвые и точные.

### Navigate вернулся, а элемента нет — это баг chromedp?

Нет, это штатная ситуация динамического сайта: элемент появляется после события load — его подтянул XHR или собрал фреймворк. Решение — условное ожидание: WaitVisible или другое Query-действие, которое переспрашивает документ, пока узел не появится.
