Архітектура браузера й рендеринг
Зміст
Для більшості тестувальників браузер — чорна скринька: вводиш адресу, отримуєш сторінку. Ця модель розсипається в перший же день автоматизації: знаходить елемент, а клік не проходить; кнопка намальована, але «мертва»; у Safari блок стилів зник цілком, і в при цьому тиша. Це не випадковості, а конкретні етапи роботи браузера, які ще не відпрацювали або відпрацювали інакше. Хто їх бачить, той відрізняє баг застосунку від власної помилки синхронізації за хвилини, а не за півдня.
Це канонічна глава теми: тут повний виклад архітектури браузера й конвеєра рендерингу, решта глав розділу посилаються сюди.
Браузер — не одна програма, а набір підсистем
Усередині браузер — набір підсистем із власним темпом роботи. Навчальна модель, яку зазвичай наводять, виділяє інтерфейс користувача, (browser shell) як диспетчера, (rendering engine), мережевий шар, (JavaScript engine), UI backend і сховище даних. Модель свідомо спрощена — розкладка роботи між процесами й потоками залежить від версії браузера та платформи, — і важлива в ній не таксономія, а ідея: різні класи багів живуть у різних місцях.
Головне розмежування — між двома рушіями:
| Рушій рендерингу | JS-рушій | |
|---|---|---|
| Відповідає за | структуру й вигляд | поведінку |
| Що робить | парсить HTML і CSS, будує дерева, рахує геометрію, малює | виконує код, який ці дерева змінює |
| Приклади | Blink, Gecko, WebKit | — |
Плутанина тут коштує дорого: з неї ростуть діагнози на кшталт «треба інший селектор», коли насправді сторінка ще не встигла себе перебудувати. «Рушій браузера» — не третя сутність, а друга назва рушія рендерингу. Blink, наприклад, почався як форк бібліотеки WebCore із WebKit — тієї її частини, що відповідає за розкладку, рендеринг і DOM.
Критична деталь: JS-рушій виконується в тому самому головному потоці, що й побудова дерев і розрахунок геометрії. Тому довгий синхронний JavaScript не просто «повільний» — він спиняє рендеринг. Виконання скриптів вплетене в побудову сторінки: HTML може запросити JavaScript, а той змінює DOM.
Дві підсистеми AQA чіпає найчастіше, і саме ззовні. Мережевий шар — точка, де тест підміняє відповіді сервера (Перехоплення й мокання мережі). Сховище — cookie, localStorage, sessionStorage, IndexedDB, кеш — переживає перезавантаження, тож без чистки стану між сценаріями результат залежить від порядку запуску (Кукі, сесії та сховище браузера).
Конвеєр рендерингу: від байтів до пікселів
Конвеєр рендерингу (rendering pipeline) — фіксована послідовність етапів, якими браузер перетворює HTML, CSS і JavaScript на пікселі. Уся дорога від отримання HTML до першого показу пікселів має власну назву — критичний шлях рендерингу (critical rendering path, CRP).
DOM будується інкрементально, ланцюжком «байти → токени → вузли → дерево»: браузер не чекає на весь файл. Що більше вузлів, то довше тривають усі наступні етапи. CSSOM, навпаки, частинами будувати не можна: доки не отримано весь набір стилів, підсумковий вигляд елемента невідомий, бо пізніше правило може перекрити раніше.
Дерево рендерингу — поєднання DOM і CSSOM: вміст зі стилями, і лише ті вузли, які будуть намальовані. Тут ховається асиметрія, яку зобовʼязаний знати кожен AQA: елемент із display: none у дереві рендерингу відсутній — він є в DOM, але геометрії не має; елемент із visibility: hidden присутній — займає місце, просто не видно.
Далі йде пʼятиетапний конвеєр: JavaScript ініціює візуальну зміну → обчислення стилів визначає, які правила застосовуються до яких елементів → layout рахує геометрію (скільки місця займає елемент і де він опиняється на екрані; результат залежить від розміру екрана) → paint заповнює пікселі: текст, кольори, зображення, рамки, тіні → composite складає намальовані шари на екран у правильному порядку.
Деталь, на якій легко сипнутися: растеризація належить до paint, а не до composite. Paint — це насправді дві задачі, створення списку команд малювання й заповнення пікселів, і друга з них зветься растеризацією. На яких саме потоках це відбувається, залежить від архітектури конкретного браузера — розробник цим не керує.
Для тестування конвеєр — діагностична шкала, у якій наявність вузла в DOM найраніша й найслабша гарантія.
Критичний шлях: що блокує показ
Частина ресурсів на критичному шляху блокуюча: доки їх не оброблено, браузер далі не рухається.
CSS блокує рендеринг. Браузер не малює нічого, доки не обробить увесь CSS — саме тому, що пізніші правила можуть перекрити ранні; інакше користувач бачив би спалах нестилізованого вмісту. Побічний ефект: повільний CSS гальмує ще й скрипти після нього.
Звичайний <script> блокує парсинг HTML. Парсер зупиняється, бо скрипт має право змінити потік документа через document.write. Атрибути async і defer знімають це блокування по-різному:
| Атрибут | Парсинг HTML | Коли виконується | Порядок |
|---|---|---|---|
| без атрибутів | зупиняється | одразу, як файл готовий | документа |
async | не зупиняється | щойно файл завантажено | не гарантований |
defer | не зупиняється | після парсингу, перед DOMContentLoaded | документа |
Наслідок для AQA: скрипт із async виконується в непередбачуваний момент, тож усе, що від нього залежить, гоночне за побудовою. Це законне джерело (flakiness) — його ловлять як дефект, а не «лікують» паузою.
Дві події, які плутають найчастіше. DOMContentLoaded настає, коли HTML розібрано й defer-скрипти виконано; вона не чекає на зображення, підфрейми та async-скрипти. load настає пізніше, коли догрузилися всі ресурси. Привʼязувати очікування до load зазвичай надто грубо: сторінка буває інтерактивною задовго до нього, а буває, що й після нього ще домальовує вміст із відповідей API (див. Асинхронне завантаження: SPA/MPA, CSR/SSR).
Reflow, repaint і композитні шари
Конвеєр проганяється щоразу, коли сторінка змінюється, і важливо, скільки етапів доводиться повторити.
Будь-яка зміна дерева рендерингу — нові вузли, змінений вміст, оновлені стилі блокової моделі — запускає layout повторно. Це й називають reflow: перерахунок геометрії. Він дорогий і каскадний, бо зсув одного елемента посуває сотні інших. Repaint — перемальовування без зміни геометрії (колір, фон) — обходиться дешевше.
Але не кожна зміна проганяє весь конвеєр — проходів насправді три:
- зміна геометрії: стилі → layout → paint → composite;
- зміна «тільки-paint»-властивості: layout пропускається;
- зміна, якій не потрібні ні layout, ні paint: одразу композитинг.
На третьому проході тримаються плавні анімації: transform і opacity браузер обробляє на етапі composite, не чіпаючи layout і paint, — тому анімують саме їх, а не top і left. Винести елемент на власний шар можна підказкою will-change, ціна — памʼять GPU, тож її застосовують точково.
Класичний дефект тут — примусовий синхронний layout (forced synchronous layout): читання геометрії одразу після запису в DOM змушує браузер негайно, не чекаючи наступного кадру, дорахувати layout. У циклі це перетворюється на layout thrashing — десятки таких перерахунків за один прохід. Ліки: спершу всі читання, потім усі записи.
Дрібниця, що економить час при розборі: у панелі продуктивності запис paint уже містить растеризацію.
Область перегляду і режим пристрою
(viewport) — видима частина вікна, у якій показується вебконтент. Її розмір обчислюється до розкладки, тож від нього залежить етап layout і те, які адаптивні спрацюють.
Корінь класичної мобільної проблеми — віртуальна область перегляду. Частина вузьких екранів рендерить сторінку у вікні, ширшому за екран (наприклад, 980 пікселів при екрані 640), а потім стискає результат. Медіазапити на 640 і менше тоді не спрацюють ніколи, хоч адаптив написано правильно. Саме для цього існує метатег <meta name="viewport"> із канонічним значенням width=device-width, initial-scale=1. Друге: CSS-піксель не дорівнює апаратному — на щільних екранах один CSS-піксель малюється кількома фізичними (це співвідношення й називають DPR), тож нечітка растрова картинка там не дефект рендерингу, а брак версії більшої роздільності.
Режим пристрою (device mode) у DevTools симулює мобільну область перегляду й обмежує ЦП і мережу. Три речі про нього:
- брейкпоінти сторінки видно, а не вгадують — DevTools показує їх смугами над областю перегляду;
- тип пристрою впливає не лише на вигляд: він визначає, чи породжує взаємодія сенсорні події, чи кліки, тож «на мобільному не працює свайп» перевіряється саме перемиканням типу;
- це наближення першого порядку, а не запуск на пристрої: код виконується на вашому компʼютері, а обмеження ЦП відносне до його потужності й між машинами не переноситься.
Для автотестів висновок один: розмір області перегляду має бути фіксованим і свідомо обраним, інакше той самий локатор на «десктопному» й «мобільному» прогоні може вести до різних елементів.
// playwright.config.ts — фіксований viewport робить прогін відтворюваним
export default defineConfig({
use: {
viewport: { width: 1280, height: 720 },
deviceScaleFactor: 1,
},
});
Матриця браузерів і адаптивна верстка — у главі Кросбраузерність і адаптивна верстка.
Дерево доступності — друге подання сторінки
Розібравши розмітку, браузер будує DOM — а на його основі ще одне, окреме дерево: дерево доступності (accessibility tree). Саме ним платформні API доступності віддають сторінку допоміжним технологіям, зокрема читачам екрана: DOM → браузер → API доступності → читач екрана.
Ключове: DOM і дерево доступності — не одне й те саме. Атрибут role змінює саме дерево доступності, тож перевіряти доступність за вкладкою Elements недостатньо — для цього є окрема панель.
| Властивість вузла | Що це | Приклад |
|---|---|---|
name (імʼя) | як до елемента звернутися | посилання з текстом «Read more» має імʼя «Read more» |
description (опис) | інформація понад імʼя | може бути візуально непомітною |
role (роль) | що це за річ | кнопка, панель навігації, список |
state (стан) | поточний стан | позначено / не позначено, згорнуто / розгорнуто |
Читач екрана зчитує імʼя разом із роллю: для кнопки «OK» він вимовляє щось на кшталт «OK button». Дерево містить ще й інформацію про можливі дії: за посиланням можна перейти, у текстове поле можна вводити.
Чому це первинне для локаторів. Локатори за роллю та доступним іменем спираються рівно на ці поля — і тому не залежать від верстки: перейменування CSS-класів чи рефакторинг дерева доступності не змінюють. Звідси друга вигода: якщо елемент не має доступного імені або має неправильну роль, це одночасно дефект доступності й майбутній нестабільний локатор. Пошук за роллю просто не знайде «кнопку», яка насправді є клікабельним div без ролі.
Стратегії пошуку елемента — у главі DOM, селектори та події, доступність як предмет тестування — у главі Семантичний HTML і доступність.
Елемент є в DOM, але недосяжний для дії
Це корінь більшості в UI-тестах: елемент знайдено, а дія не проходить або проходить «не туди». «Є у дереві» і «готовий до взаємодії» — різні стани, між якими лежить кілька етапів конвеєра.
Перед кожною дією інструмент виконує набір перевірок придатності (actionability) і чекає, поки всі релевантні пройдуть; не дочекався — падає з . Для кліку перевіряється, що локатор указує рівно на один елемент і що елемент видимий, стабільний, отримує події та увімкнений. Набір залежить від дії: для введення тексту, наприклад, перевіряється ще й «редагований» — увімкнений і не readonly.
Кожен ромб має точне означення, і саме воно дає діагноз:
- Видимий — має непорожній і не має обчисленого
visibility: hidden. Елементи нульового розміру йdisplay: noneневидимі. А отopacity: 0вважається видимим — це найчастіша несподіванка. - Стабільний — зберігає той самий bounding box щонайменше два послідовні кадри анімації. Поки елемент їде, дія перечікується.
- Отримує події — елемент є ціллю вказівникової події в точці дії. Саме ця перевірка ловить перехоплений клік: оверлей, модалку, спливний тост.
- Увімкнений — не задизейблений атрибутом
disabled, батьківськимfieldsetчиaria-disabled="true".
// Синхронізація зі станом, а не з годинником
const payButton = page.getByRole('button', { name: 'Оплатити' });
await expect(payButton).toBeEnabled();
await payButton.click();
// Маскування: клік «пройде», навіть якщо кнопку перекриває оверлей
await payButton.click({ force: true });
Опція force вимикає необовʼязкові перевірки — зокрема перестає контролювати, чи справді елемент отримує клік. Це спосіб зробити тест зеленим, приховавши дефект. Падіння «елемент перехоплює вказівникові події» — навпаки, корисне: воно фіксує або реальну проблему UX, або власну помилку тесту, і правильна реакція — закрити оверлей у .
Дві межі наостанок. Локатор — не сам елемент, а рецепт, як його знайти: шукати треба в момент дії, а не наперед. І не всесильне: воно закриває «елемент зʼявився і став клікабельним», але нічого не знає про прикладні стани — чи навішено обробник після гідратації, чи завершився запит, чи оновилися дані. Там потрібне явне очікування прикладного сигналу, і domcontentloaded не рятує: HTML уже розібрано, а дані ще їдуть. Механіку очікувань розібрано в главі Практичні сценарії AQA: флак і синхронізація.
Різні рушії — різні дефекти
MDN називає поширеними рушіями рендерингу Blink, Gecko, WebKit і Trident. Це перелік рушіїв, а не браузерів, і різниця принципова: Chrome, Edge та Opera стоять на Blink, тож дефект розкладки з одного з високою ймовірністю відтворюється в іншому. Firefox — це Gecko. Safari — WebKit, і на ньому ж тримається чимало мобільних браузерів, тому перевірка на iOS закриває більше, ніж один рядок матриці. Trident виглядає історією, але його досі вмикає режим IE в Edge — тобто корпоративний Edge може рендерити сторінку не через Blink.
Різниця не зводиться до CSS: Gecko, наприклад, містить ще й мережевий стек, графічний стек і віртуальну машину JavaScript, тож кросбраузерний дефект може виявитися не «розкладкою», а різницею в поведінці мережі чи виконання скриптів.
Головна властивість кросбраузерних дефектів (cross browser problem) — тиша. Нерозпізнане CSS-оголошення чи правило браузер просто пропускає: без застосування й без помилки в консолі. Те саме з медіазапитами, @font-face і блоками @supports; нерозпізнані HTML-елементи трактуються як анонімні рядкові, без семантики. Три найкорисніші гіпотези при розборі:
- зник цілий блок стилів — шукай невалідний селектор: у списку через кому один невалідний вимикає весь блок;
- «працює тільки в Chrome» — шукай самотній
-webkit--префікс, а не заводь задачу «підтримати Firefox»; - розмітка виглядає правильно, а поводиться дивно — переганяй сторінку через валідатор W3C: DevTools ховає помилки HTML за автовиправленням браузера.
З новими можливостями працюють через детекцію (@supports), а не через префікси навмання: префіксовані реалізації для продакшену й не призначалися.
Довгі задачі й головний потік (оглядово)
JavaScript у браузері виконується в одному потоці: у кожен момент часу рушій виконує рівно один фрагмент коду. Однопотоковий саме рушій, а не браузер — мережеві запити, таймери, декодування зображень виконують окремі механізми поза цим потоком.
Наслідок болючий: довгий синхронний код блокує все — обробку кліків, анімації, перемальовування сторінки, виконання відкладених . Звідси симптом, з якого починається половина хибних баг-репортів: кнопка намальована, але «мертва». З боку автотесту це виглядає як неклікабельний елемент чи таймаут — не тому, що елемента немає, а тому, що потік не встиг обробити подію. Лікується розбиттям роботи на частини або виносом обчислень у Web Worker — окремий потік без доступу до DOM.
Де це видно: панель продуктивності в DevTools показує етапи конвеєра окремими записами — обчислення стилів, layout, paint (уже разом із растеризацією), composite. Обмеження ЦП у режимі пристрою допомагає відтворити картину слабшого заліза, з памʼяткою, що сповільнення відносне до вашої машини й в абсолютні не переноситься.
Далі матеріал належить уже не архітектурі браузера, а вимірюванню продуктивності — пороги метрик, лабораторні й польові вимірювання, бюджети; канон клієнтських метрик живе в розділі про продуктивність. Модель черг і те, чому порядок колбеків не такий, як здається, — у главі Виконання JavaScript та event loop.
Типові помилки
- «Елемент є в DOM — значить, із ним можна працювати». Насправді присутність у дереві — найслабша гарантія: між нею і клікабельністю лежать обчислення стилів, layout, paint і перевірки придатності.
- «
opacity: 0— елемент невидимий, тест його не чіпатиме». Насправді елемент із нульовою прозорістю вважається видимим: він має непорожній bounding box. Невидимими роблятьdisplay: noneі нульовий розмір. - «
force: trueполагодив ». Насправді він вимкнув перевірку «елемент отримує події» — тобто прибрав саме той сигнал, який фіксував дефект. Тест позеленів, оверлей лишився. - «У консолі чисто — значить, кросбраузерних проблем немає». Насправді браузер мовчки пропускає CSS, якого не розуміє: без застосування й без помилки. Перевіряти треба вигляд і поведінку, а не журнал.
- «Перевірив доступність у вкладці Elements». Насправді Elements показує DOM, а доступність живе в окремому дереві:
roleзмінює саме його. Дивитися треба в панель доступності. - «Тест падає на кліку — значить, локатор поганий». Насправді діагноз дає перелік перевірок: «рівно один збіг» — про локатор, а «не отримує події», «не стабільний», «не увімкнений» — уже про стан сторінки, і правити треба передумову.
Підсумок
- Рушій рендерингу відповідає за структуру й вигляд, JS-рушій — за поведінку, і обидва ділять один головний потік: довгий синхронний JavaScript спиняє рендеринг.
- Конвеєр фіксований: DOM і CSSOM → дерево рендерингу → layout → paint → composite. Не кожна зміна проганяє його весь:
transformіopacityдоходять одразу до композитингу, а зміна геометрії тягне повний reflow. - На критичному шляху CSS блокує рендеринг, а звичайний
<script>— парсинг;asyncзнімає блокування ціною непередбачуваного моменту виконання,defer— зі збереженням порядку документа. - Дерево доступності — окреме від DOM подання сторінки. Роль і доступне імʼя, на які спираються локатори, живуть саме там: тому локатор за роллю не залежить від верстки, а його поломка часто означає ще й дефект доступності.
- «Є в DOM» ≠ «готовий до дії». Діагноз дає перелік перевірок придатності: один збіг, видимий, стабільний, отримує події, увімкнений.
forceцей діагноз не лікує, а глушить.
Можливі питання
- «Що відбувається після того, як браузер отримав HTML?» Чекають послідовність — парсинг у DOM, CSS у CSSOM, дерево рендерингу, layout, paint, composite — і хоча б одне «чому»: чому CSS блокує показ, чому DOM будується інкрементально. Слабка відповідь зупиняється на «браузер малює сторінку».
- «Чим
asyncвідрізняється відdefer?» Перевіряють звʼязок із парсингом, а не саме визначення. Сильна відповідь називає три осі — блокування парсера, момент виконання, порядок — і додає наслідок:asyncробить залежну поведінку гоночною. - «Елемент є в DOM, але тест не може на нього клікнути. Твої дії?» Одне з найчастіших питань теми на AQA-позиції. Дивляться на структуру діагностики: видимість, геометрія, перехоплення кліку оверлеєм, анімація, задизейбленість — і чи не запропонує кандидат першим ділом паузу або
force. - «Чим DOM відрізняється від дерева доступності?» Розводить тих, хто вчив локатори напамʼять, і тих, хто розуміє, чому пошук за роллю стабільний. Відповідь: це два різні подання, друге будується на основі першого й віддається читачам екрана.
- «Чому reflow дорожчий за repaint і як цього уникають в анімаціях?» Ключове: reflow перераховує геометрію й каскадно зачіпає сусідів, repaint лише перемальовує; анімують
transformіopacity, бо вони обробляються на етапі composite. - «Перевірка в режимі пристрою — це перевірка на мобільному?» Перевіряють чесність меж. Ні: це наближення першого порядку, код виконується на десктопі; режим пристрою закриває адаптивність і тип подій, але не залізо.
Джерела
Браузер — не одна програма, а набір підсистем
- MDN — Critical rendering path — вплетеність виконання скриптів у побудову сторінки й блокуючі ресурси.
- Chrome for Developers — Inside look at modern web browser (part 1) — багатопроцесна архітектура браузера й межі спрощеної моделі.
- Chrome for Developers — Inside look at modern web browser (part 2) — розподіл роботи між процесами браузера.
- Chrome for Developers — Inside look at modern web browser (part 3) — головний потік рендерера і блокування рендерингу довгим JavaScript.
- MDN — Rendering engine (Glossary) — означення рушія рендерингу й те, що «рушій браузера» — його синонім.
- MDN — Blink (Glossary) — Blink як форк бібліотеки WebCore із WebKit.
- MDN — Document: DOMContentLoaded event — момент готовності розібраного HTML.
- MDN — Window: load event — подія
loadяк пізніша межа завантаження. - MDN — Web Storage API — сховище браузера як підсистема, що переживає перезавантаження.
- RFC 6265 — HTTP State Management Mechanism — cookie як стан, що лишається між сценаріями.
- Playwright — Mock APIs — мережевий шар як точка зовнішнього втручання тесту.
- Playwright — Auto-waiting (actionability) — чому «намальовано» ще не означає «готове до дії».
Конвеєр рендерингу: від байтів до пікселів
- MDN — Critical rendering path — інкрементальна побудова DOM, CSSOM, дерево рендерингу, layout і paint як етапи критичного шляху.
- web.dev — Rendering performance (pixel pipeline) — пʼять етапів конвеєра, растеризація всередині paint, composite як завершальний етап.
- Chrome for Developers — Inside look at modern web browser (part 3) —
display: noneпротиvisibility: hiddenу дереві рендерингу. - Chrome DevTools — Simulate mobile devices with device mode — залежність результату layout від розміру області перегляду.
- Playwright — Auto-waiting (actionability) — наявність вузла в DOM як найслабша гарантія готовності.
Критичний шлях: що блокує показ
- MDN — Critical rendering path — блокуючі запити, блокування рендерингу через CSS і зупинка парсера синхронним скриптом.
- Chrome for Developers — Inside look at modern web browser (part 3) —
asyncіdeferяк спосіб не блокувати парсинг. - MDN — Document: DOMContentLoaded event — що входить у
DOMContentLoadedі чого вона не чекає. - MDN — Window: load event —
loadяк пізніша й грубіша межа готовності. - Chrome for Developers — Inside look at modern web browser (part 1) — межі спрощеної моделі процесів і потоків.
- Chrome for Developers — Inside look at modern web browser (part 2) — розподіл роботи між процесами браузера.
- MDN — Rendering engine (Glossary) — рушій рендерингу як підсистема, що обробляє ці ресурси.
- MDN — Blink (Glossary) — Blink як конкретна реалізація рушія рендерингу.
- MDN — Web Storage API — сховище браузера як підсистема поряд із мережевим шаром.
- RFC 6265 — HTTP State Management Mechanism — cookie як стан, що переживає перезавантаження.
- Playwright — Mock APIs — підміна мережевих відповідей як спосіб керувати завантаженням ресурсів.
- Playwright — Auto-waiting (actionability) — чому
load— надто грубий сигнал готовності для тесту.
Reflow, repaint і композитні шари
- MDN — Critical rendering path — повторний запуск layout після будь-якої зміни дерева рендерингу.
- web.dev — Rendering performance (pixel pipeline) — три скорочені проходи конвеєра, composite як окремий етап і растеризація всередині paint.
- Chrome for Developers — Inside look at modern web browser (part 3) — присутність і відсутність елемента в дереві рендерингу.
- Chrome DevTools — Simulate mobile devices with device mode — залежність геометрії від розміру області перегляду.
- Playwright — Auto-waiting (actionability) — вимога стабільності елемента перед дією.
Область перегляду і режим пристрою
- MDN —
<meta name="viewport">— віртуальна область перегляду,width=device-width,initial-scaleі DPR. - Chrome DevTools — Simulate mobile devices with device mode — смуги брейкпоінтів, тип пристрою й сенсорні події, межі симуляції та відносність обмеження ЦП.
- MDN — Responsive web design — медіазапити як розміру області перегляду.
Дерево доступності — друге подання сторінки
- MDN — Accessibility tree (glossary) — дерево доступності як окреме подання сторінки й чотири властивості вузла.
- W3C — Accessible Rich Internet Applications (WAI-ARIA) 1.2 — доступний обʼєкт, доступне імʼя й опис, API доступності та зчитування ролі читачем екрана.
Елемент є в DOM, але недосяжний для дії
- Playwright — Auto-waiting (actionability) — перевірки придатності й точні означення видимості, стабільності, отримання подій та увімкненості; опція
force. - Playwright — Locators — локатор як рецепт пошуку, що резолвиться в момент дії.
- Playwright — class Page — стани завантаження сторінки й очікування мережевої відповіді.
- Selenium — Waiting Strategies — явне, неявне й та чому їх не змішують.
- Testing Library — Guiding Principles — правило «перевіряй те, що бачить користувач».
- MDN — Document: DOMContentLoaded event —
domcontentloadedяк проміжний, а не готовий стан.
- MDN — Rendering engine (Glossary) — перелік поширених рушіїв рендерингу.
- MDN — Blink (Glossary) — Blink у складі Chromium і його .
- MDN — Gecko (Glossary) — Gecko як рушій Firefox і його склад: мережа, графіка, віртуальна машина JavaScript.
- MDN — WebKit (Glossary) — WebKit, Safari і мобільні браузери на тому самому рушії.
- MDN Glossary — Trident — Trident як рушій Internet Explorer і режим IE в Edge.
- MDN — Introduction to cross-browser testing — Edge і Opera в одному пункті матриці з Chrome, бо стоять на тому самому рушії рендерингу.
- Microsoft Learn — Site compatibility-impacting changes coming to Microsoft Edge — Chromium як рушій, на якому побудований Edge.
- MDN — Handling common HTML and CSS problems — мовчазне ігнорування нерозпізнаного, невалідний селектор, префікси вендорів і
@supports.
Довгі задачі й головний потік (оглядово)
- MDN — JavaScript execution model — однопотокове виконання JS і наслідки блокування головного потоку.
- Chrome for Developers — Inside look at modern web browser (part 3) — головний потік рендерера і зупинка рендерингу довгим JavaScript.
- MDN — Using Web Workers — окремий потік без доступу до DOM як спосіб розвантажити головний.
- Playwright — Auto-waiting (actionability) — як заблокований потік виглядає з боку тесту.
- web.dev — Rendering performance (pixel pipeline) — етапи конвеєра, які видно записами в панелі продуктивності.
- MDN — Critical rendering path — обчислення стилів і побудова дерева рендерингу як окремі кроки.
- Chrome DevTools — Simulate mobile devices with device mode — обмеження ЦП і чому його цифри відносні до машини.
- Playwright — Auto-waiting (actionability) — означення видимості, стабільності, отримання подій; що саме вимикає
force. - Playwright — Locators — локатор як рецепт пошуку й межі його відповідальності.
- Playwright — class Page — стани завантаження сторінки.
- Selenium — Waiting Strategies — чому жорстке очікування програє в обидва боки.
- Testing Library — Guiding Principles — орієнтир «як застосунком користуються».
- MDN — Document: DOMContentLoaded event — межі сигналу «HTML розібрано».
- MDN — Accessibility tree (glossary) — дерево доступності як окреме від DOM подання.
- W3C — Accessible Rich Internet Applications (WAI-ARIA) 1.2 — де перевіряють роль і доступне імʼя.
- MDN — Handling common HTML and CSS problems — мовчазне ігнорування нерозпізнаного CSS і HTML.
Що відбувається з моменту, коли браузер отримав HTML, і до появи пікселів на екрані?
Це не одна дія, а фіксований конвеєр етапів. Парсер перетворює розмітку на DOM покроково: байти стають токенами, токени — вузлами, вузли зшиваються в дерево. Стилі окремо збираються в CSSOM. З двох дерев народжується дерево рендерингу — той самий вміст, але вже зі стилями і лише ті вузли, які реально малюватимуться. Далі layout рахує геометрію: скільки місця займає кожен блок і в якій точці екрана він опиниться. Потім paint заповнює пікселі — текст, фони, рамки, тіні, — і саме всередині цього етапу живе растеризація. Завершує composite: намальовані шари складаються на екран у правильному порядку. Цю дорогу до першої появи пікселів називають критичним шляхом рендерингу (critical rendering path). Для AQA із цього випливає головне: «вузол уже в DOM» — найраніша точка конвеєра, а отже найслабша гарантія того, що з елементом можна працювати.
Чим рушій рендерингу відрізняється від JS-рушія?
Поділ обовʼязків простий: (rendering engine) відповідає за структуру й вигляд, (JavaScript engine) — за поведінку. Перший розбирає розмітку та стилі, складає з них дерева, рахує геометрію й виводить пікселі; другий виконує код, який ці дерева потім переписує. Blink, Gecko та WebKit — це саме рушії рендерингу, а не браузери; словосполучення «рушій браузера» позначає ту саму сутність, третьої тут немає. Ключова деталь, яку рідко називають: обидва працюють в одному головному потоці, тож виконання скрипта й побудова сторінки не йдуть паралельно — вони чергуються. Звідси практичний наслідок: важкий синхронний JavaScript не просто «повільний», він на цей час спиняє рендеринг. Плутанина в цьому місці породжує хибний діагноз «треба інший селектор» там, де насправді сторінка ще не встигла перебудуватися.
Чому CSS називають ресурсом, що блокує рендеринг?
Тому що до обробки всього набору стилів підсумковий вигляд елемента невідомий: правило, яке приїде останнім, має право перекрити все попереднє. Якби браузер малював по дорозі, користувач бачив би спалах нестилізованого вмісту, а потім стрибок верстки. Тому браузер тримає показ, доки CSSOM не зібрано повністю — і саме цим CSSOM відрізняється від DOM, який будується частинами й не чекає на кінець файлу. Побічний ефект неочевидний: повільний CSS затримує ще й скрипти, що йдуть після нього. Для тестувальника це пояснює білий екран на повільній мережі краще, ніж «щось гальмує»: спершу дивимося, чи не тягнеться довго якийсь стильовий файл, і лише потім шукаємо винних у JS.
Чим async відрізняється від defer і чому це питання ставлять AQA, а не тільки фронтендерам?
Звичайний <script> без атрибутів зупиняє парсинг HTML: парсер мусить зачекати, бо скрипт здатен дописати в документ через document.write і змінити те, що збиралися розбирати далі. Обидва атрибути це блокування знімають, але по-різному. З async парсинг не спиняється, а сам файл виконується щойно догрузиться — момент і порядок відносно інших скриптів не гарантовані. З defer парсинг теж не спиняється, але виконання відкладається до кінця розбору документа, зі збереженням порядку, у якому скрипти оголошені, і перед DOMContentLoaded. Для автоматизації різниця не академічна: усе, що залежить від async-скрипта, гоночне за побудовою, тобто його готовність не привʼязана до жодного стабільного моменту. Такий ловлять як дефект і синхронізуються з прикладним сигналом, а не «лікують» його паузою в тесті.
Чим DOMContentLoaded відрізняється від load і чому жодна з цих подій не є «сторінка готова»?
DOMContentLoaded спрацьовує в момент, коли розбір HTML завершено, а defer-скрипти вже відпрацювали. Зображень, підфреймів та async-скриптів вона свідомо не дожидається. load приходить пізніше — після того, як довантажилися всі ресурси сторінки. Спокуса прив'язати очікування до load виглядає безпечною, а насправді вона груба в обидва боки: сторінка часто стає інтерактивною задовго до цієї події, а буває й навпаки — після load екран ще домальовується даними з відповідей API. Тому обидві події описують стан завантаження документа, а не стан застосунку. У тесті орієнтир інший: чекати треба на прикладний сигнал — конкретний елемент, конкретний текст, конкретну відповідь мережі.
Що таке дерево рендерингу і чим у ньому відрізняються display: none та visibility: hidden?
Дерево рендерингу — результат поєднання DOM і CSSOM: вміст зі стилями, причому тільки ті вузли, які будуть намальовані. Саме тут ховається асиметрія, яку AQA зобовʼязаний памʼятати. Вузол із display: none до дерева рендерингу не потрапляє взагалі: у DOM він далі є, з JavaScript до нього можна дотягнутися, але жодної геометрії в нього немає. Елемент із visibility: hidden у дереві присутній: він займає своє місце в потоці, просто не показується. Практично це два різні діагнози при падінні тесту: у першому випадку елемента фізично немає на екрані і місце під ним не зарезервоване, у другому верстка зсунута не буде, а клік усе одно не пройде. І окремо памʼятайте, що ані те, ані інше не збігається з opacity: 0 — про нього окреме питання нижче.
Що таке reflow і чому він дорожчий за repaint?
Reflow — це повторний прогін етапу layout, тобто перерахунок геометрії сторінки. Він запускається щоразу, коли змінюється дерево рендерингу: додалися чи зникли вузли, змінився вміст, оновилися властивості блокової моделі. Дорогий він через каскадність: зсув одного елемента посуває сусідів, ті — своїх, і перерахунок розповзається на сотні вузлів. Repaint — це перемальовування без зміни геометрії, наприклад нового кольору чи фону; він теж не безкоштовний, але коштує помітно менше, бо етап layout пропускається. Для тестувальника з цього виростає практична річ: сторінка, яка під час завантаження щоразу перераховує геометрію, дає «стрибучі» елементи, а клік у момент стрибка або не потрапляє, або потрапляє не туди. Саме тому інструменти автоматизації перед дією чекають, поки елемент перестане рухатися.
Чому в анімаціях радять transform і opacity, а не top і left?
Тому що не кожна зміна проганяє конвеєр повністю — проходів насправді три. Зміна геометрії тягне повний ланцюг: обчислення стилів, layout, paint, composite. Зміна властивості, яка на геометрію не впливає (умовно «тільки-paint»), пропускає layout. А є клас змін, яким не потрібні ні layout, ні paint, — вони йдуть одразу на композитинг. Саме до нього належать transform і opacity, і тому анімації на них тримають плавність, тоді як анімація top/left перераховує геометрію в кожному кадрі. Окремий шар для елемента замовляють підказкою will-change, і платять за нього памʼяттю GPU — тому розставляють її точково, а не всюди. Для QA-ревʼю це готовий чекліст: побачив анімацію позиціонування замість трансформації — маєш кандидата на просідання кадрів.
Що таке примусовий синхронний layout і layout thrashing?
Примусовий синхронний layout (forced synchronous layout) виникає, коли код спершу щось записує в DOM, а одразу після цього читає геометрію. Браузер зобовʼязаний повернути актуальне значення, тому не може відкласти перерахунок до наступного кадру — він робить layout негайно, поза своїм звичайним ритмом. Одноразово це просто дорого; у циклі це перетворюється на layout thrashing, коли за один прохід відбуваються десятки таких примусових перерахунків і кадр не встигає намалюватися. Ліки прості за формулюванням: спершу зібрати всі читання, потім зробити всі записи, не перемішуючи. З боку тестування симптом упізнаваний — сторінка «залипає» саме під час дії, скрол ривками, а профайлер показує довгий блок layout там, де його ніхто не очікував.
Що таке область перегляду і чому в конфізі автотестів її фіксують?
(viewport) — та частина вікна, крізь яку користувач бачить сторінку. Її розмір визначається до розкладки, тож саме від нього залежить результат етапу layout і те, які адаптиву спрацюють. Звідси й вимога до автотестів: розмір задають явно й свідомо, бо інакше один на широкому та вузькому прогоні приведе до різних елементів — наприклад, до пункту меню в горизонтальній навігації або до його близнюка всередині бургер-меню. Плаваючий viewport дає найнеприємніший клас : тест червоніє залежно від машини чи від того, у якому режимі стартував браузер. Тому в конфізі задають і розміри, і масштаб пікселів, а мобільні перевірки виносять в окремий проєкт із власним viewport, а не залишають на волю випадку.
Адаптив написано правильно, медіазапити на 640 пікселів є, але на мобільному вони не спрацьовують. Чому?
Найімовірніше, сторінці бракує метатега viewport. Частина вузьких екранів історично рендерить сторінку у віртуальній області перегляду, ширшій за фізичний екран — наприклад, у 980 пікселів на екрані 640, — а потім стискає готову картинку. Медіазапит порівнюється саме з цією віртуальною шириною, тож умова «640 і менше» не виконується ніколи, хоч CSS бездоганний. Лікує це <meta name="viewport"> з канонічним значенням width=device-width, initial-scale=1: воно каже браузеру рахувати розкладку від реальної ширини пристрою. Поруч живе друга типова плутанина: CSS-піксель і апаратний — різні одиниці, бо на щільних матрицях кожен CSS-піксель розкладається на кілька фізичних, і це співвідношення звуть DPR. Тому розмита растрова картинка на такому екрані означає не поломку рендерингу, а відсутність файлу під вищу роздільність.
Перевірка в режимі пристрою DevTools — це перевірка на мобільному?
Ні, і це питання перевіряє чесність кандидата з межами інструмента. Режим пристрою (device mode) симулює мобільну область перегляду й дозволяє обмежити ЦП та мережу, але код усе одно виконується на вашому компʼютері, вашим десктопним рушієм. Обмеження ЦП при цьому відносне до потужності саме цієї машини, тому цифри не переносяться між ноутбуками й тим паче не годяться як абсолютний . Що режим пристрою закриває добре: адаптивність — брейкпоінти він показує смугами над областю перегляду, тож їх видно, а не вгадують; і тип взаємодії — від обраного типу пристрою залежить, чи породжує дія сенсорні події, чи звичайні кліки, тож гіпотезу «на мобільному не працює свайп» перевіряють саме перемиканням типу. Чого він не закриває: реальне залізо, реальний рушій мобільного браузера й реальні мережеві умови. Тому в матриці він стоїть як швидкий фільтр перед прогоном на пристроях, а не замість нього.
Чим DOM відрізняється від дерева доступності?
Це два різні подання однієї сторінки. З розібраної розмітки браузер робить DOM, а поверх нього — самостійне дерево доступності (accessibility tree), яке через платформні API дістається допоміжним технологіям: ланцюжок виглядає як DOM, далі браузер, далі API доступності, далі читач екрана. Вузол у цьому дереві описують чотири властивості: імʼя (як до елемента звернутися), опис (додаткова інформація понад імʼя), роль (що це за річ — кнопка, список, панель навігації) і стан (позначено чи ні, згорнуто чи розгорнуто). Читач екрана озвучує імʼя разом із роллю: кнопку з підписом «OK» він вимовляє приблизно як «OK button». Практичний висновок: атрибут role змінює саме це дерево, тому перевіряти доступність по вкладці Elements недостатньо — у DevTools для цього є окрема панель. Кандидат, який цього не розділяє, зазвичай і локатори за роллю вважає «просто ще одним селектором».
Чому локатор за роллю й доступним іменем стабільніший за CSS-селектор?
Бо він спирається на властивості дерева доступності, а не на верстку. Перейменування CSS-класів, заміна , рефакторинг сітки — усе це дерево доступності не змінює, доки роль і видимий підпис лишаються тими самими. Звідси стійкість: локатор ламається тоді, коли змінюється те, що бачить і чує користувач, а це чесний привід переглянути тест. Друга вигода бонусна: якщо елемент не має доступного імені або отримав неправильну роль, ви одночасно знайшли дефект доступності й майбутній нестабільний локатор — один симптом на дві проблеми. Найтиповіший приклад — клікабельний div без ролі: пошук за роллю кнопки його просто не знайде, і це правильна поведінка, а не обмеження інструмента.
Елемент є в DOM, але тест не може на нього клікнути. Твої дії?
Спершу я не чіпаю ні паузу, ні force, бо обидва глушать діагностику. Інструмент перед дією проганяє набір перевірок придатності (actionability) і тримає паузу, доки потрібні не стануть істинними; у падінні з він називає ту, що не спрацювала, — це і є діагноз. Для кліку список такий: локатор дає рівно один збіг, а сам елемент видимий, стабільний, отримує події й увімкнений; для введення тексту додається ще «редагований» — тобто увімкнений і без readonly. Далі кожен пункт читається буквально: «більше одного збігу» — проблема локатора; «не видимий» — нульовий розмір або display: none; «не стабільний» — елемент ще їде в анімації; «не отримує події» — зверху лежить оверлей, модалка чи спливний тост; «не увімкнений» — атрибут disabled, батьківський fieldset або aria-disabled="true". Тільки перший пункт стосується самого локатора, решта — стану сторінки, і правити треба тесту, а не селектор. Якщо жодна перевірка не скаржиться, а клік усе одно нічого не робить, наступна гіпотеза — заблокований головний потік або обробник, який ще не навісився після гідратації.
Чому opacity: 0 не робить елемент невидимим з погляду перевірок придатності?
Бо критерій видимості формулюється через геометрію, а не через те, чи бачить елемент людина. Видимим елемент рахується тоді, коли його непорожній, а обчислене значення visibility не дорівнює hidden. Нульова прозорість жодну з цих умов не порушує: коробка на місці, властивість visibility не змінена — тож елемент повністю придатний до дії. Невидимість дає інше: display: none або нульовий розмір. Це одна з найчастіших несподіванок на практиці: тест успішно «клікає» те, чого користувач фізично не бачить, і сценарій проходить там, де мав би впасти. Тому фейд-анімації й «приховані» через прозорість шари перевіряють явним на потрібний стан, а не сподіванням, що інструмент сам відмовиться діяти.
Що насправді робить force: true і чому після нього тест зелений, а баг лишився?
Опція force вимикає необовʼязкові перевірки придатності — зокрема ту, що контролює, чи справді елемент отримує вказівникову подію в точці кліку. Тобто вона прибирає рівно той сигнал, який фіксував проблему: якщо кнопку перекривав оверлей або спливне повідомлення, з force клік «пройде», але реальний користувач у тій самій ситуації не клікне нічого. Падіння «елемент перехоплює вказівникові події» тому й корисне: воно вказує або на дефект UX, або на власну помилку тесту, який не дочекався закриття модалки. Правильна реакція — прибрати перешкоду в передумові: закрити тост, дочекатися зникнення оверлея, згорнути банер. force лишається інструментом для рідкісних свідомих випадків, а не універсальним засобом від червоного прогону — і в код-ревʼю тесту він має супроводжуватися поясненням, чому перевірку тут вимкнено навмисно.
Чому кросбраузерний дефект часто не видно в консолі й з чого починати його розбір?
Такі дефекти мовчазні за природою. Оголошення чи правило, якого рушій не знає, він просто викидає: застосування немає, повідомлення теж немає. Так само поводяться медіазапити, @font-face і блоки @supports, а нерозпізнані HTML-елементи трактуються як анонімні рядкові, тобто без будь-якої семантики. Тому «в консолі чисто» не означає «проблем немає» — перевіряти треба вигляд і поведінку. Три гіпотези закривають більшість випадків. Зник цілий блок стилів — шукайте невалідний селектор: у переліку через кому один зіпсований вимикає весь блок. Працює тільки в Chrome — пошукайте самотній -webkit--префікс, замість того щоб заводити задачу «підтримати Firefox». Вигляд правильний, а поведінка дивна — проженіть сторінку через валідатор W3C, бо DevTools віддає вже автовиправлену браузером структуру й ховає вихідні помилки HTML. Нові можливості підключають детекцією через @supports, а не префіксами навмання: префіксовані реалізації ніколи й не задумувалися як механізм для продакшену.
Чому баг верстки з Chrome майже завжди відтворюється в Edge, а в Safari — ні?
Бо матриця будується не за браузерами, а за рушіями. Chrome, Edge і Opera побудовані на Blink, тому баг розкладки, знайдений в одному з них, майже напевно відтвориться в решті: це один рушій, а не три незалежні перевірки. Firefox працює на Gecko, Safari — на WebKit, а на WebKit тримається й помітна частина мобільних браузерів, тому прогін на iOS покриває не одну клітинку матриці, а кілька. Окремий сюрприз — Trident: здається реліктом, проте режим IE в Edge його досі активує, тож корпоративний Edge цілком може малювати сторінку повз Blink. І остання деталь, яку рідко називають: різниця між рушіями не зводиться до CSS — Gecko, наприклад, містить і мережевий стек, і графічний, і віртуальну машину JavaScript, тож «кросбраузерний дефект» цілком може виявитися різницею в поведінці мережі чи виконання скриптів, а не розкладки.
Що означає «JavaScript у браузері однопотоковий» і як це виглядає з боку автотесту?
Однопотоковий саме JS-рушій: у кожен момент часу він виконує рівно один фрагмент коду. Браузер загалом однопотоковим не є — мережеві запити, таймери, декодування зображень обслуговують окремі механізми поза цим потоком. Наслідок болючий: доки виконується довгий синхронний код, не обробляються кліки, не рухаються анімації, не перемальовується сторінка й не запускаються відкладені . Звідси й найпоширеніший хибний баг-репорт: кнопку видно, а натиснути її неможливо. Тест у цій ситуації повідомить про неклікабельний елемент чи таймаут, хоча причина не у відсутності вузла, а в тому, що потік просто не дійшов до обробника. Лікується це на боці продукту: важку роботу розбивають на частини або виносять у Web Worker — окремий потік, який до DOM доступу не має. А діагностується в панелі продуктивності, де етапи конвеєра видно окремими записами; памʼятайте, що запис paint там уже включає растеризацію, тож шукати її окремим рядком марно.
Три розбори, у яких знання конвеєра рендерингу економить години: падіння «клік не проходить» за перевірками придатності, діагностика мобільного адаптиву через режим пристрою та фіксований viewport, і полювання на кросбраузерний дефект, який мовчить у . Скрізь — не рецепт «зроби так», а що дивитися і чому.
Кейс 1. Клік не проходить: читаємо діагноз замість перебору селекторів
Тест падає на кнопці «Оплатити». коректний, елемент видно на , у DOM він є. Спокуса — додати очікування на дві секунди або force: true, і прогін позеленіє. Спокуса помилкова: інструмент уже назвав причину в тексті падіння, її лишається прочитати.
Перед кожною дією виконується набір перевірок придатності, і повідомлення про вказує, яка саме не пройшла. Далі — переклад із мови інструмента на мову діагнозу.
| Що каже падіння | Яка перевірка не пройшла | Найімовірніша причина | Що робити |
|---|---|---|---|
| resolved to 2 elements | рівно один збіг | локатор ловить і кнопку в шапці, і кнопку в модалці | звузити скоуп: шукати всередині діалогу, а не по всій сторінці |
| element is not visible | видимий | display: none або нульовий розмір | перевірити стан, у якому елемент реально показується, і дочекатися саме його |
| element is not stable | стабільний | елемент ще їде в анімації появи | дочекатися завершення переходу або асерту на кінцевий стан |
| element intercepts pointer events | отримує події | зверху лежить оверлей, тост або невидимий бекдроп | закрити перешкоду в передумові, а не пробивати клік силою |
| element is not enabled | увімкнений | disabled, батьківський fieldset, aria-disabled="true" | дочекатися умови, за якої кнопка вмикається (валідна форма, відповідь API) |
Тільки перший рядок стосується самого локатора. Решта — про стан сторінки, тобто правити треба тесту.
// Погано: пауза наосліп і маскування перевірки
await page.waitForTimeout(2000);
await page.getByRole('button', { name: 'Оплатити' }).click({ force: true });
// Теж погано: умовна гілка на isVisible() нічого не чекає й відповідає одразу,
// тож на швидкому прогоні банер «не побачать», і клік знову перехопить оверлей
const cookieBanner = page.getByRole('dialog', { name: 'Cookie' });
if (await cookieBanner.isVisible()) {
await cookieBanner.getByRole('button', { name: 'Прийняти' }).click();
}
// Добре: банера немає за передумовою — згоду кладемо в контекст до відкриття сторінки
await page.context().addCookies([
{ name: 'cookie_consent', value: 'accepted', domain: 'shop.example', path: '/' },
]);
await page.goto('/checkout');
const payButton = page.getByRole('button', { name: 'Оплатити' });
await expect(payButton).toBeEnabled();
await payButton.click();
Що дивитися і чому:
opacity: 0не рятує від кліку. Видимість визначається геометрією: непорожній і відсутність обчисленогоvisibility: hidden. Прозорий елемент обидві умови виконує, тож тест спокійно «клікне» те, чого користувач не бачить. Якщо сценарій залежить від завершення фейду — кінцевий стан явно, а не сподівайтеся, що інструмент відмовиться діяти.- Оверлей ловиться саме перевіркою «отримує події». У DevTools це підтверджується за секунду: наведіть курсор на точку кліку й подивіться в Elements, який вузол підсвітився. Якщо це не ваша кнопка, а
divз тостом — діагноз закритий, і він не про локатор. - Умовна гілка
if (await …isVisible())— це не передумова, а друга гонка.isVisible()не чекає жодної секунди: він перевіряє локатор і повертає відповідь одразу. На швидкому прогоні банер ще не встиг зʼявитися, гілка не спрацює — і клік перехопить оверлей, який ви «вже закрили». Передумова має бути детермінованою: покласти згоду кукою в контекст (addCookies— її побачать усі сторінки контексту) або підняти тест уже з готовим станом (storageState, що переносить кукі,localStorageта IndexedDB). forceвимикає необовʼязкові перевірки, зокрема цю саму. Тобто прибирає рівно той сигнал, який фіксував проблему. Тест зелений, оверлей на місці, користувач далі не клікне нічого — це приховування дефекту, а не полагоджений тест.- Жодна перевірка не скаржиться, а клік нічого не робить. Тоді гіпотеза інша: або обробник ще не навісився після гідратації, або головний потік зайнятий довгим синхронним кодом і не дійшов до події. Перевіряється панеллю продуктивності — довгий суцільний блок JS у момент кліку видно одразу.
Кейс 2. Мобільний адаптив «не працює»: режим пристрою і фіксований viewport
Приходить баг: на телефоні макет десктопний, хоча медіазапити написані під 640 пікселів і на звуженому вікні Chrome усе перемикається правильно. Це класика, і причина зазвичай не в CSS.
Порядок перевірки:
- Звузити вікно десктопного браузера — адаптив спрацював. Отже, самі медіазапити живі.
- Увімкнути режим пристрою й вибрати мобільний профіль. Смуги над показують сторінки — їх видно, а не вгадують. Якщо на 640 смуга є, а стилі не перемикаються, дивимося глибше.
- Подивитися в
headдокумента наявність метатега viewport.
<!-- без цього рядка вузький екран може рендерити сторінку у віртуальному вікні,
ширшому за фізичне, і медіазапит на 640 не спрацює ніколи -->
<meta name="viewport" content="width=device-width, initial-scale=1" />
Далі — для найчастіших мобільних симптомів.
| Симптом | Гіпотеза | Чим перевірити |
|---|---|---|
| макет десктопний, медіазапити правильні | немає метатега viewport, розкладка рахується від віртуальної ширини | подивитися head, потім перевірити на реальному пристрої |
| картинки нечіткі, верстка ціла | екран щільніший за один CSS-піксель на фізичний (DPR) | порівняти з версією більшої роздільності; це не баг рендерингу |
| свайп чи довгий тап не працює | сторінка чекає на сенсорні події, а прийшли кліки | перемкнути тип пристрою в режимі пристрою — саме він визначає, які події породжуються |
| на телефоні гальмує, на десктопі ні | слабший ЦП | обмеження ЦП у режимі пристрою — але лише як гіпотеза, цифри відносні до вашої машини |
| той самий тест зелений локально й червоний у CI | плаваючий розмір області перегляду | зафіксувати viewport у конфізі й порівняти скріншоти |
// playwright.config.ts — розмір області перегляду має бути свідомим рішенням,
// інакше один локатор на різних прогонах веде до різних елементів
export default defineConfig({
projects: [
{
name: 'desktop',
use: { viewport: { width: 1280, height: 720 }, deviceScaleFactor: 1 },
},
{
name: 'mobile-web',
use: { ...devices['iPhone 13'] },
},
],
});
Що дивитися і чому:
- Розмір області перегляду обчислюється до розкладки. Тому це не косметика, а вхідний параметр етапу layout: змінили ширину — змінилася геометрія, а разом із нею й те, який елемент опиниться під локатором. Пункт меню в горизонтальній навігації та його близнюк усередині бургер-меню — типова пара, на якій це вилазить.
- Режим пристрою — наближення першого порядку. Він чесно закриває адаптивність і тип подій, але код усе одно виконується вашим десктопним рушієм на вашому залізі. У цифри звідси не переносять; для перевірки мобільного рушія потрібен прогін на реальному пристрої або хоча б на WebKit.
- Мобільний проєкт у конфізі — не «те саме, але вужче». Разом із розміром змінюється й тип пристрою, а від нього залежить, чи породжує взаємодія сенсорні події, чи кліки — тож сценарії, писані під мишу, там перевіряють окремо, а не переносять як є.
Кейс 3. У Safari блок поїхав, у консолі тиша
Верстка розсипалася тільки на Safari. Розробник відкриває консоль, бачить нуль помилок і пише «не відтворюється». Насправді тиша тут — не доказ, а симптом: нерозпізнане CSS-оголошення чи правило браузер мовчки пропускає, без застосування й без запису в журнал. Так само поводяться медіазапити, @font-face і блоки @supports, а нерозпізнані HTML-елементи трактуються як анонімні рядкові, тобто без семантики.
Тріаж у три гіпотези, від найдешевшої:
| Гіпотеза | Симптом | Як підтвердити |
|---|---|---|
| невалідний селектор | зник цілий блок стилів, а не одна властивість | у переліку селекторів через кому знайти зіпсований — один такий вимикає весь блок |
| самотній вендорний префікс | «працює тільки в Chrome» | шукати оголошення з -webkit- без стандартного відповідника поруч |
| невалідна розмітка | вигляд правильний, поведінка дивна | прогнати сторінку через валідатор W3C: DevTools показує вже автовиправлену браузером структуру й ховає вихідні помилки |
Мінімальна перевірка на рівні матриці:
// той самий сценарій на трьох рушіях; Chrome і Edge — це один Blink,
// тож окремий рядок під Edge дасть майже ту саму інформацію
export default defineConfig({
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
});
Що дивитися і чому:
- Матриця будується за рушіями, а не за назвами браузерів. Chrome, Edge і Opera побудовані на Blink, тому баг розкладки з одного майже напевно відтвориться в решті. Firefox стоїть на Gecko, Safari — на WebKit, а на WebKit тримається й помітна частина мобільних браузерів, тому прогін на iOS покриває кілька клітинок матриці одразу. Окремий сюрприз — режим IE в Edge: під ним корпоративний браузер малює сторінку через Trident, тобто повз Blink.
- Кросбраузерний дефект не обовʼязково про CSS. У складі рушія живуть і мережевий стек, і графічний, і віртуальна машина JavaScript — тож «поїхала верстка» іноді виявляється різницею в поведінці мережі чи виконання скриптів. Якщо на одному рушії блок не намальовано взагалі, спершу перевірте, чи прийшли всі відповіді, а не тільки стилі.
- Скріншот-порівняння тут корисніше за асерт на текст. Мовчазний дефект стилів не змінює ані DOM, ані вміст, тож функціональний тест лишиться зеленим — ловить його саме візуальна перевірка або асерт на конкретну обчислену властивість.
- Нову можливість підключають детекцією, а не префіксом. Блок
@supportsдає керовану поведінку на рушії, який фічі не має; префіксовані реалізації для продакшену ніколи й не призначалися, тож ставка на них — відкладений баг, а не сумісність.
Підсистеми браузера і два рушії
- Знаю різницю між (структура й вигляд) і (поведінка) та памʼятаю, що «рушій браузера» — друга назва першого, а не третя сутність.
- Можу пояснити, чому обидва живуть в одному головному потоці й чому через це довгий синхронний JavaScript не «гальмує», а спиняє рендеринг; памʼятаю, що однопотоковий саме JS-рушій, а не браузер, і як заблокований потік виглядає з боку тесту (намальована, але «мертва» кнопка, неклікабельний елемент, ).
Конвеєр рендерингу
- Можу назвати конвеєр у правильному порядку: DOM і CSSOM → дерево рендерингу → layout → paint → composite, і знаю, що вся дорога до першого показу зветься критичним шляхом рендерингу.
- Знаю різницю
display: nonevsvisibility: hiddenу дереві рендерингу: перший з дерева випадає й геометрії не має, другий у дереві лишається й місце займає. - Памʼятаю, що растеризація живе всередині paint, а не composite, і що запис
paintу панелі продуктивності вже враховує її.
Критичний шлях: що блокує показ
- Можу пояснити, чому CSS блокує рендеринг (пізніше правило каскаду перекриває раніше, тож потрібен увесь набір) і чому повільний стильовий файл затримує ще й скрипти після себе — на відміну від DOM, який будується частинами.
- Знаю, чому звичайний
<script>зупиняє парсинг HTML, і чим від нього відрізняютьсяasyncтаdeferза трьома осями: блокування парсера, момент виконання, порядок — а також чомуasyncробить залежну поведінку гоночною й дає законний . - Знаю різницю
DOMContentLoadedvsloadі чого перша НЕ чекає (зображення, підфрейми,async-скрипти), а чому друга надто груба як сигнал готовності для тесту.
Reflow, repaint і композитинг
- Можу пояснити, чому reflow дорожчий за repaint (перерахунок геометрії каскадний), і назвати три скорочені проходи конвеєра, включно з прямим шляхом у композитинг для
transformтаopacity; памʼятаю, щоwill-changeкоштує памʼяті GPU. - Можу пояснити примусовий синхронний layout і layout thrashing та правило «спершу всі читання, потім усі записи».
Область перегляду й режим пристрою
- Розумію, що розмір обчислюється до розкладки, тож від нього залежать і геометрія, і — і тому в автотестах його фіксують, інакше один на різних прогонах веде до різних елементів.
- Знаю проблему віртуальної області перегляду й навіщо існує
<meta name="viewport">зі значеннямwidth=device-width, initial-scale=1. - Не плутаю CSS-піксель з апаратним (DPR) і можу назвати межі режиму пристрою: брейкпоінти й тип подій він показує чесно, а залізо — ні, бо код виконується на моїй машині.
Дерево доступності
- Знаю різницю між DOM і деревом доступності, можу назвати чотири властивості вузла (імʼя, опис, роль, стан) і розумію, чому
roleперевіряють в окремій панелі, а не у вкладці Elements. - Можу пояснити, чому локатор за роллю й доступним іменем не залежить від верстки і чому його поломка часто означає одночасно дефект доступності.
Готовність елемента до дії
- Знаю перелік перевірок придатності для кліку: рівно один збіг, видимий, стабільний, отримує події, увімкнений — і що для введення тексту додається «редагований».
- Памʼятаю точні означення: видимий — непорожній без обчисленого
visibility: hidden(томуopacity: 0вважається видимим); стабільний — та сама коробка щонайменше два кадри поспіль; отримує події — є ціллю вказівникової події в точці дії; увімкнений — безdisabled, батьківськогоfieldsetчиaria-disabled="true". - Можу читати діагноз за перевіркою («більше одного збігу» — про локатор, решта — про стан сторінки), розумію, що саме вимикає
forceі чому падіння «елемент перехоплює вказівникові події» — корисний сигнал; памʼятаю межі : прикладних станів воно не знає, іdomcontentloadedтут не рятує.
Різні рушії й головний потік
- Знаю, який браузер на якому рушії (Blink — Chrome, Edge, Opera; Gecko — Firefox; WebKit — Safari і чимало мобільних; Trident досі живий через режим IE в Edge) і що різниця між рушіями не зводиться до CSS: у складі є й мережевий стек, і графічний, і віртуальна машина JavaScript.
- Памʼятаю мовчазність кросбраузерних дефектів і три гіпотези розбору: зник цілий блок стилів → невалідний селектор у переліку через кому; «працює тільки в Chrome» → самотній
-webkit--префікс; дивна поведінка при правильному вигляді → валідатор W3C.
Квіз
Перед стартом
- Питань: 17
- Поріг «зараховано»: ≥70% правильних відповідей.
- Результат впливає на прогрес; завалені питання підуть у чергу повторення.
- Квіз впливає на компліт теми: тема стає «пройдено», лише коли прочитано теорію І квіз складено на ≥70%.
Питання
Рушій рендерингу vs JS-рушій — хто за що відповідає?