Помилка: Page.goto: Timeout 60000ms exceeded. — повний гід з діагностики та виправлення

Коли 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:

text
await page.goto(url, {
  waitUntil: 'domcontentloaded',
  timeout: 30000
});
await page.waitForSelector('selector-який-реально-потрібен', { state: 'visible' });

Для сторінок, що дійсно важкі, збільшуйте лише navigationTimeout у конфігурації:

text
// playwright.config.ts
export default defineConfig({
  use: {
    navigationTimeout: 45000,
  },
});

Або на рівні контексту:

text
context.setDefaultNavigationTimeout(45000);

Більш надійний підхід — блокувати непотрібні ресурси:

text
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

  1. Перевірте, що navigationTimeout і test timeout узгоджені (navigation не повинен перевищувати test).
  2. Замініть networkidle на domcontentloaded або load + конкретний селектор.
  3. Увімкніть trace на першому retry.
  4. Додайте блокування сторонніх ресурсів (аналітика, реклама, шрифти).
  5. Порівняйте час goto локально і в CI на одній і тій самій сторінці.
  6. Переконайтеся, що service worker не активний (або примусово вимкніть його через CDP).
  7. Для важких сторінок використовуйте окремий 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 перетворює нестабільні тести на передбачувані.

Leave a Reply

Your email address will not be published. Required fields are marked *