vyvchy
    Теми розділу

    03 · Веб і мережі для AQA

    Архітектура браузера й рендеринг

    Зміст

    Для більшості тестувальників браузер — чорна скринька: вводиш адресу, отримуєш сторінку. Ця модель розсипається в перший же день автоматизації: знаходить елемент, а клік не проходить; кнопка намальована, але «мертва»; у 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).

    HTML

    DOM

    CSS

    CSSOM

    Дерево рендерингу

    Layout
    геометрія

    Paint
    пікселі та растеризація

    Composite
    складання шарів

    HTML

    DOM

    CSS

    CSSOM

    Дерево рендерингу

    Layout
    геометрія

    Paint
    пікселі та растеризація

    Composite
    складання шарів

    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.

    Ні

    Так

    Ні

    Так

    Ні

    Так

    Ні

    Так

    Ні

    Так

    Локатор знайшов елемент

    Рівно один збіг?

    Уточнити локатор

    Видимий?

    Нульовий розмір
    або display none

    Стабільний?

    Анімація ще їде

    Отримує події?

    Зверху оверлей або тост

    Увімкнений?

    disabled або aria-disabled

    Дія виконується

    Ні

    Так

    Ні

    Так

    Ні

    Так

    Ні

    Так

    Ні

    Так

    Локатор знайшов елемент

    Рівно один збіг?

    Уточнити локатор

    Видимий?

    Нульовий розмір
    або display none

    Стабільний?

    Анімація ще їде

    Отримує події?

    Зверху оверлей або тост

    Увімкнений?

    disabled або aria-disabled

    Дія виконується

    Кожен ромб має точне означення, і саме воно дає діагноз:

    • Видимий — має непорожній і не має обчисленого 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.
    • «Перевірка в режимі пристрою — це перевірка на мобільному?» Перевіряють чесність меж. Ні: це наближення першого порядку, код виконується на десктопі; режим пристрою закриває адаптивність і тип подій, але не залізо.

    Джерела

    Браузер — не одна програма, а набір підсистем

    Конвеєр рендерингу: від байтів до пікселів

    Критичний шлях: що блокує показ

    Reflow, repaint і композитні шари

    Область перегляду і режим пристрою

    Дерево доступності — друге подання сторінки

    Елемент є в DOM, але недосяжний для дії

    Різні рушії — різні дефекти

    Довгі задачі й головний потік (оглядово)

    Типові помилки

    Пояснення

    «Поясни» працює з власним API-ключем Anthropic: запит іде з вашого браузера прямо до Anthropic.

    Свій API-ключ (BYOK)

    Вставте власний ключ Anthropic — пояснення працюватиме на реальній моделі. Ключ зберігається лише у вашому браузері (localStorage), нікуди не надсилається, крім api.anthropic.com, і не логується.