Коли 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 рази — типова картина для слабких runner-ів.
Поширені помилки, які погіршують ситуацію
Багато команд потрапляють у пастку одних і тих самих дій:
- Ставлять 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 виникає через redirect-ланцюжок?
Використовуйте waitUntil: ‘commit’ або page.waitForURL з фінальним URL після goto.
Timeout 60000ms exceeded — не вирок, а точний індикатор. Коли ви знаєте, який саме стан не настав і чому, рішення стає технічним, а не магічним. Правильна комбінація waitUntil, блокування ресурсів і розумного timeout перетворює нестабільні тести на передбачувані.