Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Senior: питання на співбесіді з теми «DOM і події»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

4 питання

Щоб показати сторінку, браузер рахує layout (розміри й позиції елементів, у Firefox це називають reflow), а потім paint - малює пікселі, і composite - збирає шари.

  • Reflow (layout) - перерахунок геометрії. Дорогий: зміна розміру одного елемента може зачепити батьків, сусідів і нащадків.
  • Repaint - перемальовування без зміни геометрії (колір, тінь). Дешевше.
  • Лише composite - transform і opacity на окремому шарі: найдешевше, часто на GPU.

Layout thrashing - чергування записів і читань геометрії в циклі. Кожне читання (offsetHeight, getBoundingClientRect(), scrollTop) після запису змушує браузер синхронно перерахувати layout:

// Погано: N примусових перерахунків
for (const card of cards) {
  card.style.height = `${card.offsetWidth * 0.75}px`;   // читання після запису попередньої ітерації
}

// Добре: спершу всі читання, потім усі записи
const widths = cards.map((card) => card.offsetWidth);
cards.forEach((card, i) => { card.style.height = `${widths[i] * 0.75}px`; });

Інші правила:

  • Анімувати transform і opacity, а не top, left, width, height.
  • Групувати зміни стилів через клас, а не десяток окремих присвоєнь style.
  • Візуальні оновлення - у requestAnimationFrame, щоб вони збігалися з кадром.
  • content-visibility: auto дозволяє браузеру пропускати layout і paint для невидимих частин довгої сторінки.

Як знайти проблему: вкладка Performance у DevTools - фіолетові блоки «Layout» з попередженням «Forced reflow» показують рядок коду, що спричинив перерахунок.

Докладніше в документації: Reflow

Старий спосіб дізнатися, чи елемент з'явився на екрані, - обробник scroll з getBoundingClientRect() для кожного елемента. Він спрацьовує десятки разів на секунду, виконується в головному потоці й примушує браузер рахувати layout - звідси «смикання» прокрутки.

IntersectionObserver повідомляє, коли елемент перетинає область видимості (або інший елемент-контейнер). Обчислення виконує браузер, асинхронно, без примусового layout.

const observer = new IntersectionObserver((entries) => {
  for (const entry of entries) {
    if (!entry.isIntersecting) continue;

    const img = entry.target;
    img.src = img.dataset.src;   // завантажити зображення
    observer.unobserve(img);     // більше не стежити
  }
}, { rootMargin: '200px' });     // почати трохи заздалегідь

document.querySelectorAll('img[data-src]').forEach((img) => observer.observe(img));

Типові застосування:

  • Нескінченний скрол: спостерігати за «маркером» наприкінці списку й підвантажувати наступну сторінку, коли він стає видимим.
  • Аналітика переглядів: реклама чи блок вважається показаним, коли видно 50% протягом секунди (threshold: 0.5).
  • Анімації при появі, пауза відео поза екраном.

Що варто знати:

  • Для простих зображень лінивого завантаження вже вбудовано: <img loading="lazy">. Спостерігач потрібен для складніших випадків.
  • threshold задає, яку частку елемента має бути видно; rootMargin - розширює чи звужує область перевірки.
  • Спостерігача треба відключати (disconnect()), коли компонент знищено, інакше він триматиме посилання на елементи.
  • Для зміни розміру елементів є споріднений ResizeObserver.

Докладніше в документації: Intersection Observer API

MutationObserver повідомляє про зміни в DOM: додавання й видалення вузлів, зміну атрибутів чи тексту. Він замінив застарілі й повільні mutation events (DOMNodeInserted), які браузери поступово видаляють (Chrome - з версії 127).

const observer = new MutationObserver((mutations) => {
  for (const mutation of mutations) {
    for (const node of mutation.addedNodes) {
      if (node instanceof Element) {
        node.querySelectorAll('[data-tooltip]').forEach(initTooltip);
        if (node.matches('[data-tooltip]')) initTooltip(node);
      }
    }
  }
});

observer.observe(document.body, { childList: true, subtree: true });

Що можна спостерігати (опції observe):

  • childList - додавання й видалення дочірніх вузлів;
  • subtree - разом з усіма нащадками, а не лише прямими дітьми;
  • attributes (+ attributeFilter: ['class', 'disabled'], attributeOldValue) - зміни атрибутів;
  • characterData - зміни тексту у текстових вузлах.

Як він виконується. Колбек викликається асинхронно, пакетом, як мікрозадача після поточного коду. Сто змін в одному циклі - один виклик з масивом записів. Тому він значно дешевший за старі синхронні події.

Де корисний:

  • ініціалізація сторонніх віджетів на вмісті, що з'являється динамічно (підвантаження, Livewire-оновлення, wire:navigate);
  • реакція на зміни, які робить чужий код, якого ви не контролюєте;
  • автоматичні тести й інструменти доступності, що стежать за змінами сторінки.

Підводні камені:

  • subtree: true на document.body - колбек на кожну зміну всієї сторінки. На складних сторінках це відчутно. Спостерігати варто найменший можливий контейнер з мінімумом опцій;
  • нескінченний цикл: колбек змінює DOM у тій самій області - це породжує нові записи. Потрібна перевірка, чи елемент уже оброблено (наприклад, атрибут-позначка);
  • відключення: observer.disconnect() при знищенні компонента, інакше спостерігач триматиме посилання на вузли. takeRecords() дає змогу забрати ще не доставлені записи перед відключенням;
  • не для розміру й видимості: для них є ResizeObserver і IntersectionObserver - зміна розміру не є мутацією DOM.

Альтернатива, якщо ви контролюєте код, що змінює DOM, - викликати ініціалізацію явно чи через власну подію. Спостерігач потрібен саме тоді, коли зміни відбуваються «десь» поза вашим контролем.

Livewire і Alpine самі використовують MutationObserver: Alpine так помічає нові елементи з x-data і ініціалізує їх.

Докладніше в документації: MutationObserver

Веб-компоненти - вбудований у браузер спосіб створювати власні HTML-елементи. Три частини:

1. Користувацькі елементи (custom elements) - власний тег з поведінкою:

class CopyButton extends HTMLElement {
  connectedCallback() {
    this.addEventListener('click', () => {
      navigator.clipboard.writeText(this.getAttribute('text'));
    });
  }
}
customElements.define('copy-button', CopyButton);
<copy-button text="composer require laravel/sanctum">Копіювати</copy-button>

Назва обов'язково містить дефіс - щоб не зіткнутися з майбутніми стандартними тегами. Колбеки життєвого циклу: connectedCallback, disconnectedCallback, attributeChangedCallback (для атрибутів зі списку observedAttributes).

2. Shadow DOM - ізольоване піддерево всередині елемента:

const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
  <style>button { color: red; }</style>
  <button><slot></slot></button>
`;
  • стилі сторінки не проникають усередину, а стилі компонента не витікають назовні - справжня ізоляція CSS;
  • document.querySelector не знаходить елементи всередині тіньового дерева;
  • налаштування ззовні - через CSS-змінні (вони успадковуються крізь межу) і ::part().

3. <template> і <slot> - розмітка-заготовка й місця, куди потрапляє вміст, переданий компоненту.

Коли веб-компоненти доречні:

  • елементи, що мають працювати будь-де: у Blade, Vue, React, на статичній сторінці. Дизайн-системи великих компаній і віджети для вбудовування на чужі сайти;
  • ізоляція стилів від CSS сторінки, яку ви не контролюєте;
  • «острівці» інтерактивності на серверно-рендерених сторінках без фреймворку;
  • довговічність: стандарт браузера не застаріває так, як версії фреймворків.

Обмеження:

  • немає реактивності й шаблонізації з коробки: оновлення DOM при зміні даних - вручну (або бібліотека на кшталт Lit);
  • серверний рендеринг Shadow DOM можливий через декларативний <template shadowrootmode="open">, але екосистема тут слабша, ніж у фреймворків;
  • форми: елементи всередині Shadow DOM не беруть участі у відправці форми без ElementInternals;
  • доступність: зв'язки aria-labelledby і for не працюють крізь межу тіньового дерева;
  • Tailwind не стилізує вміст Shadow DOM - глобальні класи туди не потрапляють.

З фреймворками: Vue вміє компілювати компоненти у веб-компоненти (defineCustomElement), React 19 повноцінно підтримує їх як елементи. Alpine і Livewire працюють з користувацькими елементами як із звичайними тегами (без Shadow DOM).

Докладніше в документації: Веб-компоненти