---
title: "Ожидания: правильный сон"
description: "chromedp ожидание элементов без Sleep: Poll и PollFunction держат предикат в браузере, QueryAfter добавляет свой шаг, таймауты и режимы опроса — с примерами."
course: "Chromedp в Go: автоматизация Chrome без Selenium"
url: "https://golangmaster.ru/courses/chromedp/lessons/09-ozhidaniya-elementov/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 20
---

# Ожидания: правильный сон

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

time.Sleep в парсере — лотерея: сайт отвечает то за двести миллисекунд, то за четыре секунды, и обе стороны ошибки одинаково дороги. Правильное chromedp ожидание строится на фактах: Poll опрашивает предикат внутри браузера, poll element go-условия дожидаются узлов, QueryAfter добавляет свой шаг после ожидания. Разберём инструменты по уровням — от условий Query до предикатов.

Программа из прошлых уроков умеет ждать события загрузки и видимость узлов, но живые сценарии требуют третьего измерения: дождаться, когда на странице случится состояние — все картинки загрузились, карточек стало двадцать, счётчик в конце списка показывает готовность. Первый рефлекс — Sleep: подождать пару секунд и надеяться. Этот рефлекс стоит разобрать на части, потому что у него два режима отказа, и оба уже случались у каждого, кто писал скраперы. Медленный сайт: Sleep на две секунды, а рендер занял четыре, — программа читает пустоту и пишет мусор в отчёт. Быстрый сайт: Sleep на десять секунд «с запасом», а страница готова за полсекунды, — обход в двадцать раз длиннее нужного, и на тысяче страниц это уже часы. Ожидание по времени всегда промахивается либо в одну, либо в другую сторону, потому что время — не факт. Факт — это условие: узел появился, предикат вернул истину, счетчик дошёл до значения. Урок про chromedp ожидание элементов собирает инструменты ожидания в систему: условия Query-действий, предикаты Poll и крючок QueryAfter — плюс честный разговор о том, когда Sleep всё-таки допустим и чем он опасен.

### Условия Query-действий: NodeVisible и родня

Фундамент уже знаком по третьему и четвёртому урокам: каждое Query-действие само переспрашивает документ, пока условие не выполнится. Но условия — это отдельное семейство опций, и его стоит увидеть целиком. Дефолтное условие NodeReady: узлы найдены и получены из браузера. NodeVisible требует видимости — проверки offsetWidth, offsetHeight и клиентских прямоугольников; NodeNotVisible ждёт обратного. NodeEnabled дожидается, что у элемента нет атрибута disabled, NodeSelected — что элемент выбран. NodeNotPresent ждёт исчезновения: удобно после снятия оверлея загрузки. Обёртки над ними — знакомые WaitVisible, WaitNotVisible, WaitEnabled, WaitSelected, WaitNotPresent — это те же условия в виде отдельных действий. Два параметра настраивают сам механизм опроса: AtLeast задаёт, сколько узлов должно совпасть — по умолчанию один, для списка из двадцати карточек пишут AtLeast(20); RetryInterval — интервал переспросов, по умолчанию пять миллисекунд. Важно понимать, где живёт этот опрос: на стороне Go. Каждый переспрос — отдельная оценка селектора в браузере, обмен по протоколу; для условий по узлам это дёшево, а вот для сложных состояний страницы нужен инструмент, который опрашивает, не выходя из браузера, — о нём дальше.

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

### Poll: предикат внутри браузера

Poll(expression, res, opts...) — ожидание произвольного условия JavaScript: из выражения собирается предикат, и он исполняется в самой странице, а не в Go-программе. Запросы вида poll element go описывают ровно этот механизм: предикат крутится в браузере, тикает по умолчанию в каждом кадре анимации — через requestAnimationFrame, — и ни один тик не стоит обмена по протоколу. Когда выражение впервые возвращает истинное значение, Poll завершается, а значение расфасовывается в res: логическое — в bool, текст — в строку, живой объект — в низкоуровневый RemoteObject; res может быть и nil, если значение не нужно. Это делает Poll универсальным: он умеет ждать не только узлы, но и данные — длину списка, значение переменной, флаг, который выставил код страницы. Таймаут по умолчанию тридцать секунд, ошибка при нём — ErrPollingTimeout. Один суровый предел: предикат не переживает навигацию — контекст исполнения страницы уничтожается, и Poll вернёт ошибку «Execution context was destroyed». Дожидаться конца загрузки новой страницы — работа Navigate и WaitReady, а Poll — про состояние внутри одной страницы.

### Дождаться, что все картинки загрузились

```go
var ready bool
err := chromedp.Run(ctx,
        chromedp.Navigate(url),
        chromedp.Poll(
                `Array.from(document.images).every(
                        i => i.complete && i.naturalWidth > 0)`,
                &ready,
                chromedp.WithPollingTimeout(15*time.Second),
        ),
)
if err != nil {
        panic(err)
}
fmt.Println("картинки готовы:", ready)
```

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

### PollFunction: аргументы и режимы опроса

Когда условие сложнее одного выражения, его оформляют функцией: PollFunction(pageFunction, res, opts...) оборачивает её в вызов с аргументами — return (функция)(...args). Аргументы передаются опцией WithPollingArgs: предикат вида (n) => document.querySelectorAll('.card').length >= n вместе с WithPollingArgs(20) ждёт, пока карточек станет не меньше двадцати; значения едут как есть — числа, строки, структуры, закодированные в JSON. Вторая степень свободы — режим опроса. Дефолтный тик по кадрам анимации — самый частый и самый прожорливый по CPU страницы; он нужен, когда условие завязано на стили и размеры. WithPollingInterval(interval) переводит опрос на таймер: раз в двести пятьдесят миллисекунд — разумная точка для медленных состояний вроде загрузки данных. WithPollingMutation ждёт событий DOM: предикат проверяется один раз, а дальше — на каждое изменение дерева; это самый экономный режим, и у него есть поучительная история: ранние версии мутирующего опроса умели пропускать таймаут, если страница молчала, поэтому сниппет исполнения доработали страховкой, а тесты библиотеки проверяют случай «таймаут без мутаций». Привычка задавать WithPollingTimeout явно от этого только надёжнее. Опция WithPollingInFrame переносит предикат в указанный фрейм — узлы фрейма достают действием Nodes, и это единственный путь, когда условие живёт внутри iframe.

### QueryAfter: свой шаг после ожидания

Условия и предикаты закрывают вопрос «когда продолжать», но иногда в сам момент готовности нужно что-то сделать: посчитать узлы, вынуть из них данные, запустить свою функцию ровно тогда, когда страница доехала. Для этого у Query есть крючок After, а у библиотеки — обёртка QueryAfter(sel, f, opts...): селектор опрашивается, как обычно, условия выполняются, и затем вызывается ваша функция f с контекстом, идентификатором контекста исполнения и срезом найденных узлов. Сигнатура строгая: func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error — вернуть ошибку значит уронить весь Run, как от любого действия. Именно на этом крючке построены все Query-действия библиотеки: Text, Click и SetValue — это QueryAfter со своей функцией внутри, и теперь вы можете собрать в том же стиле собственное действие. Типовые применения: дожидаться AtLeast(20) карточек и одним махом собрать их узлы для дальнейшей работы; выполнить условие NodeVisible и сразу прокрутить узел; совместить своё ожидание с существующими опциями Query — ByQueryAll, AtLeast, RetryInterval. Это низкоуровневый инструмент, и брать его стоит, когда готовых действий уже не хватает, — но знание, что весь слой Query стоит на нём, делает библиотеку прозрачной. Крючок After многоразовый: функций можно повесить несколько, и исполнятся они по порядку добавления, а QueryAfter — лишь обёртка над одной из них. Ещё две детали из недр Query: порог AtLeast проверяется до условий видимости, поэтому с AtLeast(20) прогон честно крутится, пока карточек меньше двадцати; а битый селектор, скажем со случайным двоеточием, не уходит в вечный переспрос — браузер отвечает ошибкой DOM, и она сразу выезжает наверх.

### Дождаться двадцати карточек и забрать узлы

```go
err := chromedp.Run(ctx,
        chromedp.QueryAfter(".product-card",
                func(ctx context.Context, execCtx runtime.ExecutionContextID,
                        cards ...*cdp.Node) error {
                        fmt.Println("карточек в DOM:", len(cards))
                        return nil
                },
                chromedp.ByQueryAll, chromedp.AtLeast(20),
                chromedp.NodeVisible,
        ),
)
```

QueryAfter дожидается, пока селектор совпадёт минимум с двадцатью видимыми карточками, и только затем вызывает функцию с узлами. Идентификатор execCtx нужен для низкоуровневых вызовов в контексте страницы, срез cards — сами узлы. Те же опции Query управляют поиском: ByQueryAll берёт все совпадения, AtLeast задаёт порог готовности.

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

| Инструмент | Что ждёт | Где крутится опрос |
| --- | --- | --- |
| WaitVisible и родня | узел и его состояние: видимость, включённость, выбор | Go: переспрос селектора, дефолт каждые 5 мс |
| NodeVisible, AtLeast, RetryInterval | условия Query-действий внутри Run | Go: тот же механизм, настраиваемые порог и интервал |
| Poll | выражение JavaScript: состояние, данные, флаги | браузер: тик на каждый кадр анимации |
| PollFunction | функцию с аргументами и режимом interval или mutation | браузер: таймер, события DOM или кадры |
| QueryAfter | готовность узлов плюс ваш шаг с этими узлами | Go: условие Query, затем ваш код |
| chromedp.Sleep | ничего: фиксированная пауза | Go: чистый сон — только для отладки |

### Сколько ждать: таймауты и дедлайны

У каждого ожидания есть верхняя граница, и её выбор — отдельное решение. У Poll и PollFunction — WithPollingTimeout: по умолчанию тридцать секунд, ноль отключает таймаут совсем. У дедлайна контекста — context.WithTimeout, знакомый по третьему уроку: он отсекает весь прогон, включая навигацию и действия. Правило их совместной жизни: таймаут предиката должен быть заметно меньше дедлайна контекста, иначе внешний дедлайн ударит первым, и вы получите не внятную ошибку ErrPollingTimeout, а безликую context deadline exceeded, из которой не видно, какое из условий не доехало. Разумная связка для обхода: контексту — бюджет страницы, скажем двадцать секунд, предикату — пятнадцать; тогда при отказе ошибка укажет на конкретное ожидание. Второе правило — таймауты задаются явно даже там, где есть дефолт: тридцать секунд — значение для человека за консолью, а не для обхода в тысячу страниц, где три зависших предиката стоят минуту времени воркера. Ноль, отключающий таймаут, — инструмент осознанный: он уместен в интерактивной сессии, когда вы смотрите на страницу глазами и готовы прервать сами, и опасен в автоматике, где превращает зависание в вечное.

### Как выбрать ожидание за пять секунд

- ждёте узел — WaitVisible или условие NodeVisible прямо в действии
- ждёте количество — AtLeast(20) на Query или предикат с querySelectorAll
- ждёте состояние или данные — Poll с выражением и явным WithPollingTimeout
- условие требует параметров или сложной логики — PollFunction с WithPollingArgs
- страница меняется редко и опрос дорог — режим WithPollingMutation
- нужен свой шаг в момент готовности — QueryAfter с функцией над узлами
- хочется Sleep — сначала спросите себя, какой факт вы на самом деле ждёте

Что дальше: ожидания — фундамент под оставшимся курсом. Одиннадцатый урок применит их к самой сложной цели — [SPA и динамическому контенту](/courses/chromedp/lessons/11-dinamicheskij-kontent/), где разметку собирает фреймворк и без предикатов не обойтись; пятнадцатый урок про [таймауты и устойчивость](/courses/chromedp/lessons/15-tajmauty-i-prochnost/) соберёт дедлайны, ретраи и падения браузера в систему. Практика — локально, с Go и Chrome: песочница сайта показывает механику без браузера, а настоящие гонки предикатов вы увидите только на живой странице. Кстати, половина багов ожидания воспроизводится в консоли браузера: перед тем как переносить предикат в Go, исполните его выражение в DevTools — совпало там, совпадёт и здесь.

### Проверка: где живёт предикат

**Вопрос:** Где исполняется предикат, переданный в chromedp.Poll?

- В Go-программе: цикл с запросами селектора
- ✅ Внутри страницы: браузер тикает предикат без обменов по протоколу
- На отдельной горутине библиотеки
- На сервере сайта — поэтому Poll уважает rate limit

Poll собирает из выражения предикат и исполняет его в контексте страницы; по умолчанию тик происходит на каждый кадр анимации через requestAnimationFrame. Результат приезжает в Go один раз — когда условие выполнено или истёк таймаут.

### Проверка: таймаут опроса

**Вопрос:** Какой таймаут у Poll по умолчанию и как его отключить?

- Пять секунд; отключается опцией RetryInterval(0)
- ✅ Тридцать секунд; WithPollingTimeout(0) отключает
- Таймаута нет; его задаёт только контекст
- Одна минута; отключается WithPollingMutation

Дефолтный таймаут предиката — тридцать секунд, при превышении возвращается ErrPollingTimeout. WithPollingTimeout задаёт своё значение, ноль отключает ограничение. RetryInterval — про частоту переспросов условий Query, а мутирующий режим — про триггер опроса, не про время.

### Что вернёт Poll после навигации

```go
// err := chromedp.Run(ctx,
//   chromedp.Navigate(url),
//   chromedp.Click("#next"), // кнопка уводит страницу переходом
//   chromedp.Poll("window.__done === true", &ok),
// )
```

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

- Poll молча дождётся готовности новой страницы
- ✅ Ошибка: ожидающий предикат не переживает навигацию — Execution context was destroyed
- Poll вернёт true от первой страницы
- Программа зависнет до дедлайна контекста

Предикат Poll живёт в контексте исполнения конкретной страницы; переход уничтожает контекст, и ожидающий опрос падает с ошибкой Execution context was destroyed — этот случай покрыт тестами библиотеки. Ждать готовность после перехода — работа Navigate, который дожидается загрузки, и WaitReady на узлах новой страницы.

### Соберите ожидание с аргументом

Восстановите вызовы: предикат-функция и опция, передающая ей порог количества.

```go
var enough bool
err := chromedp.Run(ctx,
        chromedp.___(
                "(n) => document.querySelectorAll('.card').length >= n",
                &enough,
                chromedp.___(20),
        ),
)
```

Подсказка: Функцию с аргументами опрашивает PollFunction, значения для аргументов передаёт опция WithPollingArgs.

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

### Расставьте ожидание готовности

Соберите по порядку: контекст, буфер результата, Run с навигацией и предикатом, обработка ошибки.

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

1. `var ready bool`
2. `err := chromedp.Run(ctx,`
3. `  chromedp.Navigate(url),`
4. `  chromedp.Poll(`
5. `    "Array.from(document.images).every(i => i.complete)",`
6. `    &ready,`
7. `    chromedp.WithPollingTimeout(15*time.Second),`
8. `  ),`
9. `)`
10. `if err != nil {`
11. `  panic(err)`
12. `}`

### Карточки: инструменты ожидания

- **chromedp.Poll** — Опрашивает выражение JavaScript внутри страницы до первого истинного значения; результат кладёт в res. Дефолтный тик — кадры анимации, таймаут — тридцать секунд.
- **chromedp.PollFunction** — То же, но предикат — функция: return (fn)(...args). Аргументы передаются WithPollingArgs.
- **Режимы опроса** — raf — каждый кадр, дефолт; WithPollingInterval — таймер; WithPollingMutation — на события изменения DOM.
- **WithPollingTimeout** — Таймаут предиката, дефолт 30 секунд, 0 отключает. Превышение — ErrPollingTimeout. Должен быть меньше дедлайна контекста.
- **chromedp.QueryAfter** — Query-действие с крючком: дожидается узлов и условий, затем вызывает вашу функцию с execCtx и срезом узлов. На нём построены все Query-действия.
- **chromedp.Sleep** — Фиксированная пауза без условия. В исходниках помечена как временная и под угрозой удаления; в продакшене заменяется WaitVisible и Poll.

> **Проверяйте предикат в консоли.** Предикат Poll — обычное выражение JavaScript, и его можно исполнить в консоли DevTools до переноса в Go: если выражение в консоли возвращает false там, где страница, по-вашему, готова, — вопрос к условию, а не к коду. Эта одна проверка экономит циклы «запустил — увидел ошибку — поправил строку», особенно для выражений с querySelectorAll и приведением типов.

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

- Ожидание по времени всегда промахивается: ждать нужно факт — узел, количество, предикат, значение.
- Условия Query-действий: NodeReady, NodeVisible и родня, порог AtLeast и интервал RetryInterval; опрос крутится на стороне Go.
- Poll исполняет предикат в браузере: тик на каждый кадр анимации, ноль обменов по протоколу на тик, результат — в res.
- PollFunction добавляет аргументы через WithPollingArgs; режимы — таймер WithPollingInterval и события DOM WithPollingMutation.
- Таймаут предиката — WithPollingTimeout, дефолт тридцать секунд, ноль отключает; держите его меньше дедлайна контекста.
- QueryAfter — крючок после готовности узлов: ваша функция получает контекст исполнения и срез узлов; на нём стоит весь слой Query.
- Sleep — отладочный костыль с пометкой о будущем удалении: в продакшене его место занимает условие или предикат.
- Предикат не переживает навигацию: ожидание состояния — внутри одной страницы, переход — работа Navigate и WaitReady.

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

Возьмите страницу с ленивой подгруздкой и соберите три версии ожидания готовности списка: WaitVisible на контейнере, Poll с выражением document.querySelectorAll('.item').length >= 20 и PollFunction с предикатом-функцией и WithPollingArgs. Замерьте время каждого подхода, затем намеренно сломайте условие — задайте порог, которого страница не достигнет, — и посмотрите на ErrPollingTimeout. Финал: замените один Sleep в своём старом коде на предикат и сравните стабильность десяти прогонов.

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

### Почему chromedp.Poll, а не WaitVisible — у меня и так узлы ждут?

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

### Есть ли в chromedp отдельное действие вроде Visibility?

Нет: видимость в библиотеке — это условие, а не действие. NodeVisible вешается на любое Query-действие и дожидается, что узел имеет ненулевые размеры и клиентские прямоугольники; WaitVisible — обёртка того же условия в виде отдельного шага; NodeNotVisible ждёт обратного. Проверка живёт в маленьком сниппете JavaScript и исполняется браузером — отдельное имя для неё не понадобилось.

### Poll завис на медленной странице — что читать в ошибке?

Три типовых исхода. ErrPollingTimeout — условие честно не выполнилось за WithPollingTimeout: проверьте предикат в консоли. context deadline exceeded — раньше ударил дедлайн контекста: разнесите таймауты, предикату поменьше, контексту побольше. Execution context was destroyed — предикат пережил навигацию: перенесите ожидание после перехода или ждите готовность новой страницы штатными средствами.
