Когда Playwright во время выполнения page.goto() выбрасывает Timeout 60000ms exceeded, навигация не успела достичь выбранного состояния за отведённое время. По умолчанию во многих конфигурациях именно 60 секунд становятся границей, после которой тест или скрипт останавливается с TimeoutError.
Эта ошибка почти никогда не бывает случайной. Она сигнализирует о медленном сервере, тяжёлых ресурсах, неправильном waitUntil, service worker, антибот-защите или разнице между локальной средой и CI. Понимание механизма позволяет устранить причину, а не просто увеличивать timeout.
Ниже разобран полный цикл: от внутренней логики метода до пошаговой диагностики, типичных ошибок и стратегий, которые работают и для начинающих, и для команд с сотнями тестов.
Механизм работы page.goto() и откуда берётся именно 60000 мс
Метод page.goto(url, options) инициирует навигацию и ждёт, пока страница достигнет одного из состояний, определённых параметром waitUntil. По официальной документации Playwright значение timeout по умолчанию равно 0 (без ограничения), однако на практике оно почти всегда ограничивается test timeout (30 000 мс) или явно заданным navigationTimeout. Когда пользователь или библиотека устанавливает timeout: 60000, именно эта цифра появляется в сообщении об ошибке.
Возможные значения waitUntil:
- load — ожидает событие load (все ресурсы, включая изображения, стили, iframe). Это значение по умолчанию.
- domcontentloaded — ждёт только построения DOM-дерева. Значительно быстрее.
- networkidle — нет сетевых соединений в течение 500 мс. Официально помечено как discouraged, потому что аналитика, WebSocket и long-polling никогда не «замолкают».
- commit — самый ранний момент: получен ответ и началась загрузка документа.
Если выбранное состояние не наступает за timeout миллисекунд, Playwright выбрасывает TimeoutError с логом «navigating to …, waiting until …». Важно: HTTP-статусы 404 или 500 сами по себе ошибку не вызывают — проблема именно во времени достижения события.
На практике 60000 мс чаще всего появляется потому, что кто-то когда-то поставил именно это значение «на всякий случай», и оно закрепилось в конфигурации или в обёртке библиотеки.
Как быстро диагностировать причину
Первый шаг — запустить тот же goto с headless: false и открыть DevTools. Если страница визуально уже загружена, а тест всё равно падает, проблема почти наверняка в waitUntil: 'load' или 'networkidle'. Если страница долго крутит индикатор — дело в сети или сервере.
Полезные инструменты диагностики:
- DEBUG=pw:api — показывает точную последовательность событий commit → domcontentloaded → load.
- page.on('request') и page.on('response') — выявляют «вечные» запросы (аналитика, service worker, polling).
- Trace Viewer — сохраняет скриншоты, сетевой лог и DOM-снимки именно в момент timeout.
- chrome://serviceworker-internals/ — проверяет, не держит ли service worker соединение открытым.
В CI добавьте trace: 'on-first-retry' и сравнивайте время загрузки с локальной машиной. Разница в 2–3 раза — типичная картина для слабых раннеров.
Распространённые ошибки, которые усугубляют ситуацию
Многие команды попадают в ловушку одних и тех же действий:
- Ставят waitUntil: 'networkidle' на страницы с Google Analytics, Intercom или WebSocket. Сеть никогда не становится idle, timeout гарантирован.
- Используют page.goto() + page.waitForNavigation() подряд. Второй waitForNavigation ждёт следующей навигации, которой нет.
- Увеличивают timeout до 120000 или 0 «чтобы просто работало». Это маскирует реальные проблемы производительности.
- Игнорируют разницу сред: локально 3G-эмуляция отсутствует, в CI — медленный DNS и холодный старт контейнера.
- Блокируют только изображения, но оставляют большие JS-бандлы и сторонние скрипты.
По моему опыту использования Playwright в течение месяца на проекте с 400+ тестами именно networkidle стал причиной 70 % флейков в ночных прогонах.
Практические решения: от быстрого фикса до системной стратегии
Самый простой и часто достаточный шаг — изменить waitUntil и явно задать timeout:
await page.goto(url, {
waitUntil: 'domcontentloaded',
timeout: 30000
});
await page.waitForSelector('selector-который-реально-нужен', { state: 'visible' });Для страниц, которые действительно тяжёлые, увеличивайте только navigationTimeout в конфигурации:
// playwright.config.ts
export default defineConfig({
use: {
navigationTimeout: 45000,
},
});Или на уровне контекста:
context.setDefaultNavigationTimeout(45000);Более надёжный подход — блокировать ненужные ресурсы:
await page.route('**/*.{png,jpg,jpeg,gif,svg,woff,woff2}', route => route.abort());
await page.route('**/analytics**', route => route.abort());Для антибот-защиты иногда помогает реалистичный user-agent, viewport и небольшая задержка перед goto. Если сайт активно отсекает headless-браузеры, timeout часто является лишь симптомом, а не причиной.
В нашей практике мы сталкивались со случаем, когда timeout 60000 мс появлялся только на одном домене. После анализа trace выяснилось, что service worker регистрировался и держал соединение. Решением стало page.goto с waitUntil: 'commit' + явный waitForSelector на основной контент. Время стабильного прохождения сократилось с 55–60 с до 4–6 с.
Чек-лист перед запуском тестов в CI
- Проверьте, что navigationTimeout и test timeout согласованы (navigation не должен превышать test).
- Замените networkidle на domcontentloaded или load + конкретный селектор.
- Включите trace на первом retry.
- Добавьте блокировку сторонних ресурсов (аналитика, реклама, шрифты).
- Сравните время goto локально и в CI на одной и той же странице.
- Убедитесь, что service worker не активен (или принудительно отключите его через CDP).
- Для тяжёлых страниц используйте отдельный helper с повышенным timeout, а не глобальное значение.
После прохождения чек-листа большинство «непонятных» timeout исчезают без дальнейшего увеличения цифр.
Когда стоит менять код приложения, а не тесты
Если после всех оптимизаций timeout остаётся, возможно, проблема не в Playwright. Страница, которая стабильно загружается дольше 15–20 секунд на обычном соединении, нуждается в оптимизации: code-splitting, lazy-loading изображений, отложенная загрузка сторонних скриптов, уменьшение TTFB. В таких случаях увеличение timeout лишь откладывает обнаружение проблемы.
Для команд, которые пишут e2e-тесты, правило простое: если страница медленная для пользователя — она медленная и для автотестов. TimeoutError тогда становится полезным сигналом, а не препятствием.
Вопросы, которые чаще всего ищут разработчики
Почему локально всё работает, а в CI падает с Timeout 60000ms exceeded?
CI-машины почти всегда медленнее. Холодный старт браузера, ограниченный CPU и сеть дают +30–100 % ко времени загрузки. Решение — отдельный navigationTimeout для CI или эмуляция сети локально.
Можно ли полностью отключить timeout?
Да, timeout: 0. Но это опасно: зависание одной страницы остановит весь прогон. Лучше ставить реалистичное значение и ловить ошибку.
Чем отличается navigationTimeout от actionTimeout?
navigationTimeout касается только goto, reload, waitForURL. actionTimeout — кликов, fill, hover и т. д. Они независимы.
Помогает ли page.setDefaultTimeout(60000)?
Да, но setDefaultNavigationTimeout имеет приоритет именно для навигации. Лучше использовать специализированный метод.
Что делать, если timeout возникает из-за цепочки редиректов?
Используйте waitUntil: 'commit' или page.waitForURL с финальным URL после goto.
Timeout 60000ms exceeded — не приговор, а точный индикатор. Когда вы знаете, какое именно состояние не наступило и почему, решение становится техническим, а не магическим. Правильная комбинация waitUntil, блокировки ресурсов и разумного timeout превращает нестабильные тесты в предсказуемые.