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

Питання на співбесіді з JavaScript

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

114 питань

Service Worker - скрипт, який браузер запускає у фоні між сторінкою й мережею. Він перехоплює запити сторінок свого сайту й вирішує, що відповісти: з мережі, з кешу чи згенерувати відповідь сам. На ньому будуються офлайн-режим, PWA, push-сповіщення, фонова синхронізація.

// реєстрація на сторінці
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js');
}
// sw.js
const CACHE = 'static-v3';

self.addEventListener('install', (event) => {
  event.waitUntil(caches.open(CACHE).then((cache) => cache.addAll(['/offline.html'])));
});

self.addEventListener('fetch', (event) => {
  if (event.request.mode === 'navigate') {
    event.respondWith(fetch(event.request).catch(() => caches.match('/offline.html')));
  }
});

Життєвий цикл:

  1. install - підготовка: завантажити й закешувати потрібні файли;
  2. waiting - новий воркер чекає, доки закриються всі вкладки зі старим. Це захищає від ситуації, коли одна сторінка працює зі старими файлами, а запити обслуговує новий воркер;
  3. activate - новий воркер бере керування; тут видаляють старі кеші;
  4. fetch - перехоплення запитів.

self.skipWaiting() і clients.claim() прискорюють перемикання, але ціною можливої невідповідності версій.

Стратегії кешування:

  • cache first - для хешованих статичних файлів, що не змінюються;
  • network first - для HTML і API: свіже, а кеш лише як запас при відсутності мережі;
  • stale-while-revalidate - миттєво з кешу, паралельно оновити кеш з мережі.

Бібліотека Workbox реалізує ці стратегії й генерує воркер під час збирання (у Vite - vite-plugin-pwa).

Чим небезпечна помилка:

  • воркер живе довше за сторінку. Він встановлений у браузері користувача й перехоплює запити навіть після того, як ви виправили сайт. Якщо воркер помилково кешує HTML «назавжди», користувачі бачитимуть стару версію сайту, і деплой цього не виправить;
  • оновлення самого воркера - браузер перевіряє, чи змінився sw.js, при навігації на сайт (за замовчуванням в обхід HTTP-кешу, і щонайменше раз на 24 години). URL sw.js має лишатися незмінним: якщо перейменувати файл, старий воркер про новий так і не дізнається;
  • план відступу: заздалегідь мати «вимикач» - новий sw.js, що видаляє кеші й знімає реєстрацію (self.registration.unregister()).

Обмеження: лише HTTPS (виняток - localhost), без доступу до DOM, область дії (scope) - каталог, з якого віддано скрипт, і нижче.

Для звичайного сайту (не PWA) Service Worker часто зайвий: HTTP-кешування з хешованими іменами файлів дає більшу частину вигоди без ризиків.

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

Дві суперечливі вимоги: статичні файли мають кешуватися якомога довше (швидкість, трафік), а після деплою користувачі мають одразу отримати нову версію. Розв'язок - різна політика для HTML і для ресурсів з хешем у назві.

1. Ресурси з хешем у назві (app-3f9a1c.js, app-8b2e.css - так їх називає Vite) - кешувати «назавжди»:

Cache-Control: public, max-age=31536000, immutable

Вміст файлу ніколи не змінюється: нова версія - нова назва. immutable каже браузеру не перевіряти файл навіть при оновленні сторінки.

2. HTML - завжди перевіряти актуальність:

Cache-Control: no-cache

no-cache не означає «не кешувати» - браузер зберігає копію, але перед використанням питає сервер, чи вона актуальна. Не кешувати взагалі - це no-store (для сторінок з персональними даними).

Саме HTML посилається на конкретні хешовані файли: новий HTML після деплою - нові назви ресурсів - браузер їх завантажить.

Перевірка актуальності (ревалідація):

  • сервер віддає ETag: "abc123" чи Last-Modified;
  • браузер наступного разу питає If-None-Match: "abc123";
  • якщо не змінилося - 304 Not Modified без тіла: економія трафіку, але все одно запит по мережі.

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

  • довге кешування файлів без хешу (/js/app.js з max-age=86400) - після деплою користувачі добу бачать старий JavaScript з новим HTML, і все ламається;
  • старі файли видалено при деплої. Користувач з відкритою вкладкою (або HTML з кешу CDN) просить app-старийхеш.js, а його вже немає - 404 і зламана сторінка. Хорошою практикою є тримати файли попередньої збірки ще якийсь час. CDN з кешем може приховувати проблему: віддає старий файл зі свого кешу, поки той не витісниться;
  • динамічні частини (import()) з тієї ж причини падають у давно відкритих вкладках - потрібна обробка помилки завантаження й пропозиція оновити сторінку;
  • Vary не вказано для відповідей, що залежать від заголовків (Accept-Encoding, мова) - CDN може віддати не той варіант.

Шари кешування: кеш браузера, CDN (Cloudflare), зворотний проксі. Для CDN є окремі директиви: s-maxage (лише для спільних кешів) і stale-while-revalidate (віддати застаріле, поки оновлюється у фоні).

У Laravel + Vite файли з public/build/assets мають хеші - для них вебсервер (Nginx, Caddy) налаштовують з immutable. HTML формує Laravel, і для гостьових сторінок кешування на рівні CDN налаштовується окремо від статики.

Перевірка: вкладка Network у DevTools - колонка Size показує (disk cache)/(memory cache) чи розмір, а заголовки відповіді - реальну політику.

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

Proxy - обгортка над об'єктом, що перехоплює базові операції з ним: читання й запис властивостей, видалення, перевірку in, перебір ключів, виклик функції. Перехоплювачі називають «пастками» (traps).

const target = { name: 'Оля' };

const logged = new Proxy(target, {
  get(obj, key, receiver) {
    console.log(`читання ${String(key)}`);
    return Reflect.get(obj, key, receiver);
  },
  set(obj, key, value, receiver) {
    console.log(`запис ${String(key)} = ${value}`);
    return Reflect.set(obj, key, value, receiver);
  },
});

logged.name;       // читання name
logged.age = 30;   // запис age = 30

Reflect - набір функцій, що виконують ту саму базову операцію «за замовчуванням». У пастці його використовують, щоб після власної логіки виконати стандартну поведінку правильно - зокрема з параметром receiver, від якого залежить this для гетерів.

Пастки: get, set, has (in), deleteProperty, ownKeys (Object.keys, for...in), apply (виклик функції), construct (new), defineProperty, getPrototypeOf та інші - для кожної внутрішньої операції мови.

Реактивність Vue 3:

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      track(target, key);                    // запам'ятати: поточний ефект залежить від key
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver);
      trigger(target, key);                  // перезапустити ефекти, що залежать від key
      return result;
    },
  });
}

Під час рендеру компонента кожне читання реактивної властивості реєструє залежність. Зміна властивості перезапускає лише ті рендери й computed, що від неї залежать.

Чому Vue 3 перейшов з гетерів/сетерів (Vue 2) на Proxy:

  • нові властивості відстежуються автоматично - у Vue 2 доводилося викликати Vue.set;
  • індекси масиву й length - зміна items[3] = x теж реактивна;
  • Map, Set - через перехоплення їхніх методів;
  • не треба заздалегідь обходити весь об'єкт: вкладені об'єкти обгортаються ліниво, при доступі.

Інші застосування: валідація записів, значення за замовчуванням для відсутніх ключів, логування й налагодження, «віртуальні» об'єкти (клієнт API, де api.users.list() будує запит з назв властивостей), моки в тестах.

Обмеження:

  • тотожність: proxy !== target. Порівняння реактивного об'єкта з оригіналом (toRaw у Vue) - часте джерело плутанини;
  • приватні поля # і внутрішні слоти (Map, Date) не проходять крізь проксі: методи, викликані на проксі, отримують this = проксі й падають. Тому для вбудованих колекцій Vue має окремі обробники;
  • продуктивність: кожна операція йде через пастку - для гарячих циклів по великих структурах це відчутно (звідси shallowRef, markRaw);
  • інваріанти: пастка не може «збрехати» про незмінювану властивість (наприклад, заморожену) - рушій кине TypeError.

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

Декоратор - функція, що отримує елемент класу (метод, поле, аксесор чи весь клас) і змінює чи доповнює його поведінку. Записується через @ перед оголошенням:

function logged(method, context) {
  return function (...args) {
    console.log(`виклик ${String(context.name)}`, args);
    return method.apply(this, args);
  };
}

class OrderService {
  @logged
  place(order) { /* ... */ }
}

Стан стандарту. Декоратори - пропозиція TC39 на стадії 3: синтаксис і семантика узгоджені, але рушії браузерів і Node.js нативно їх ще не виконують. Використовуються через компіляцію:

  • TypeScript 5.0+ підтримує стандартні декоратори без прапорців;
  • Babel - через плагін.

Дві несумісні версії. Це головне джерело плутанини:

  • «експериментальні» (legacy) декоратори - TypeScript з experimentalDecorators: true. На них побудовані Angular, NestJS, TypeORM, MobX (старі версії). Вони отримують дескриптор властивості й підтримують декоратори параметрів;
  • стандартні декоратори (стадія 3) - інший API: отримують значення й об'єкт context (kind, name, addInitializer, access, metadata). Декораторів параметрів у них немає.

Декоратор, написаний для однієї версії, не працює з іншою. Перехід фреймворків на стандартні декоратори - поступовий.

Що можна декорувати:

  • методи - обгортки: логування, кешування, повтор при помилці, перевірка прав;
  • поля - перетворення початкового значення;
  • аксесори з accessor - нове ключове слово, що створює пару гетер/сетер з прихованим сховищем. Основа для реактивних полів (Lit, MobX);
  • класи - реєстрація, додавання поведінки.
class Counter {
  @reactive accessor count = 0;   // гетер/сетер, які декоратор може перехопити
}

Метадані (окрема пропозиція, теж стадія 3): context.metadata - об'єкт, куди декоратори записують інформацію про клас. Замінює reflect-metadata, на якому побудовані DI-контейнери NestJS і Angular.

Порівняння з PHP: атрибути PHP 8 (#[Route('/orders')]) - лише метадані, які читає фреймворк через рефлексію. Декоратори JavaScript - виконуваний код, що змінює елемент у момент визначення класу.

Чи варто використовувати в застосунку: якщо фреймворк побудований на них (Angular, NestJS, Lit) - так, у тій версії, яку він вимагає. У власному коді - обережно: компіляція обов'язкова, а стандарт ще не в рушіях. Звичайна функція вищого порядку (const place = logged(placeImpl)) дає той самий ефект без нового синтаксису.

Докладніше в документації: Пропозиція TC39: декоратори

Допоміжні методи ітераторів (ES2025) - map, filter, take, drop, flatMap, reduce, some, every, find, forEach, toArray - тепер є не лише в масивах, а й у будь-якому ітераторі.

Різниця з методами масиву - лінивість. Ланцюжок методів масиву на кожному кроці створює новий масив цілком:

const result = hugeArray
  .filter((n) => n % 2)     // новий масив на сотні тисяч елементів
  .map((n) => n * 10)       // ще один
  .slice(0, 2);             // а потрібно було лише 2 елементи

Методи ітератора нічого не обчислюють наперед. Кожен елемент проходить увесь ланцюжок по одному, і лише коли його запитали:

const result = hugeArray
  .values()                 // ітератор масиву
  .filter((n) => n % 2)
  .map((n) => n * 10)
  .take(2)                  // зупиниться після двох
  .toArray();

Оброблено рівно стільки елементів, скільки потрібно, щоб знайти два, - без проміжних масивів.

Нескінченні послідовності стають природними:

function* naturals() {
  let n = 1;
  while (true) yield n++;
}

naturals().filter((n) => n % 7 === 0).take(3).toArray();   // [7, 14, 21]

З масивом таке неможливо в принципі.

Ітератори всюди:

map.keys().filter((key) => key.startsWith('user:')).toArray();
set.values().map((tag) => tag.toLowerCase());
document.querySelectorAll('li').values().filter((li) => li.dataset.active).toArray();

Iterator.from(obj) перетворює будь-який ітерований об'єкт (чи об'єкт з методом next) на ітератор з цими методами.

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

  • ітератор одноразовий: після проходу він вичерпаний. Повторний toArray() поверне порожній масив. Масив можна перебирати скільки завгодно;
  • немає довжини й доступу за індексом - якщо потрібні length, sort, at, перетворюйте на масив;
  • на невеликих масивах виграшу немає, а ланцюжок методів масиву звичніший;
  • асинхронних версій (для асинхронних ітераторів) поки немає в стандарті - це окрема пропозиція.

Коли використовувати: великі чи потенційно нескінченні послідовності, ранній вихід (take, find), обробка Map/Set без проміжних масивів, генератори даних.

Підтримка: Chrome 122+, Firefox 131+, Safari 18.4+, Node.js 22+.

Аналогія з Laravel: LazyCollection проти Collection - те саме розрізнення між лінивою й «жадібною» обробкою, і cursor()/lazy() для великих вибірок з бази.

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

Знімкові тести (snapshot) зберігають результат при першому запуску у файл, а при наступних - порівнюють з ним.

it('серіалізує замовлення для API', () => {
  expect(toApiPayload(order)).toMatchSnapshot();
});

it('форматує адресу', () => {
  expect(formatAddress(address)).toMatchInlineSnapshot(`"м. Київ, вул. Хрещатик, 1"`);
});

Оновлення знімків після навмисної зміни: npx vitest -u.

Коли знімки корисні:

  • великі структури даних, які складно перевірити вручну: payload для API, згенерована конфігурація, результат парсера;
  • виявлення неочікуваних змін у серіалізації: додали поле - знімок це покаже;
  • inline-знімки для коротких значень - видно прямо в тесті, рев'ю бачить очікування.

Коли шкодять:

  • знімки всієї розмітки компонента змінюються від кожної правки верстки чи класу. Розробник звикає запускати -u не дивлячись - і знімок перестає щось перевіряти;
  • великі знімки ніхто не читає на рев'ю: diff на 300 рядків приймається «на віру»;
  • нестабільні дані (дати, випадкові id) змушують фіксувати чи маскувати значення, інакше тест падає щоразу;
  • знімок замість думки: тест не каже, що важливо в результаті. Явна перевірка expect(payload.total).toBe(1999) документує намір, знімок - ні.

Візуальна регресія порівнює знімки екрана:

await expect(page).toHaveScreenshot('checkout.png', {
  maxDiffPixelRatio: 0.01,
  mask: [page.getByTestId('current-date')],
});

Ловить те, що інші тести не бачать: зламаний CSS, зсунуту верстку, зниклі іконки, проблеми темної теми.

Складнощі візуальних тестів:

  • рендеринг шрифтів і згладжування відрізняються між ОС і навіть версіями браузера - знімки, зроблені на Mac, не збігаються з Linux у CI. Еталонні знімки генерують у тому самому середовищі (Docker-образ Playwright), де запускаються тести;
  • динамічний вміст (дати, реклама, аватари, анімації) - маскувати й вимикати;
  • поріг розбіжності - надто суворий дає хибні падіння, надто м'який пропускає реальні зміни;
  • зберігання еталонів у репозиторії збільшує його розмір.

Практичний підхід: небагато візуальних тестів для ключових сторінок і компонентів дизайн-системи, явні перевірки поведінки - для логіки, знімки даних - для великих структур з обов'язковим переглядом diff на рев'ю.

Докладніше в документації: Playwright: візуальні порівняння

У Laravel-проєкті фронтенд (Blade з Alpine, Livewire, Inertia з Vue чи React) тісно пов'язаний з бекендом. Браузерний тест має перевіряти обидві частини разом - і питання в тому, з якого боку його писати.

Pest browser tests (плагін pestphp/pest-plugin-browser, працює на Playwright):

it('користувач підписується на розсилку', function () {
    $page = visit('/');

    $page->fill('email', 'olena@example.com')
        ->click('Підписатися')
        ->assertSee('Дякуємо за підписку')
        ->assertNoJavaScriptErrors();

    expect(Subscriber::where('email', 'olena@example.com')->exists())->toBeTrue();
});

it('сторінки відкриваються без помилок', function () {
    visit(['/', '/blog', '/jobs'])->assertNoSmoke();
});
  • той самий процес і база, що в PHP-тестах: фабрики, RefreshDatabase, actingAs(), Mail::fake() працюють як у звичайних feature-тестах;
  • перевірка бази поруч з перевіркою інтерфейсу - без API для підготовки даних;
  • smoke-тести (assertNoSmoke, assertNoJavaScriptErrors) дешево ловлять зламані сторінки й помилки в консолі;
  • inDarkMode(), мобільні пристрої, знімки екрана для візуальних порівнянь.

Playwright напряму (тести на JavaScript/TypeScript):

  • окремий застосунок запущений як справжній сервер (webServer у конфігурації), тести бачать його лише через HTTP;
  • підготовка даних - через сидери, спеціальні тестові ендпойнти чи API, що ускладнює ізоляцію;
  • повний API Playwright: перехоплення мережі, кілька вкладок і контекстів, трасування, компонентні тести;
  • природно для команди, що пише фронтенд на TypeScript, і для SPA, де бекенд - окремий сервіс.

Як обрати:

Ситуація Що зручніше
Blade, Livewire, Inertia в одному репозиторії з Laravel Pest browser tests
команда пише переважно на PHP Pest
окремий SPA-фронтенд, бекенд лише API Playwright
складна клієнтська логіка: офлайн, кілька вкладок, мережеві умови Playwright

У CI:

  • браузери треба встановити (npx playwright install --with-deps chromium) і закешувати;
  • ассети зібрати (npm run build), інакше сторінки без JavaScript і CSS;
  • паралельний запуск і шардування для довгих наборів;
  • знімки екрана й траси як артефакти збирання - щоб розбиратися з падіннями без локального відтворення.

Розподіл рівнів: браузерні тести - для критичних сценаріїв і smoke-перевірок сторінок, Livewire- і feature-тести - для логіки компонентів і контролерів, Vitest - для складної клієнтської логіки окремо від браузера.

Докладніше в документації: Pest: браузерне тестування

На старті будь-які тести швидкі. Через рік набір з тисячі тестів може йти 15 хвилин, падати випадково й гальмувати кожен pull request. Структура з самого початку визначає, чи станеться це.

1. Правильний рівень для кожної перевірки:

  • логіка (розрахунки, перетворення, валідація) - винесена в чисті функції й перевіряється unit-тестами в середовищі node, без DOM;
  • поведінка компонентів - інтеграційні тести з Testing Library;
  • критичні сценарії - кілька E2E.

Найпоширеніша причина повільного набору - перевірка логіки через E2E: кожен варіант знижки перевіряється відкриттям браузера.

2. Середовище за потребою: jsdom у всіх файлах сповільнює й ті тести, яким DOM не потрібен. Середовище задають для конкретних файлів чи проєктів:

// vitest.config.js
export default defineConfig({
  test: {
    projects: [
      { test: { name: 'unit', environment: 'node', include: ['**/*.unit.test.js'] } },
      { test: { name: 'dom', environment: 'happy-dom', include: ['**/*.dom.test.js'] } },
    ],
  },
});

3. Ізоляція й детермінованість:

  • кожен тест готує свої дані й не залежить від порядку запуску;
  • моки й глобальні заглушки відновлюються після кожного тесту (restoreMocks, unstubGlobals у конфігурації);
  • час, випадкові значення й мережа - під контролем тесту (фейкові таймери, vi.setSystemTime, MSW).

4. Швидкий зворотний зв'язок:

  • vitest --changed - лише тести, пов'язані зі зміненими файлами;
  • у CI - паралельний запуск і шардування: vitest run --shard=1/4;
  • E2E - окремою задачею CI, що не блокує швидкі перевірки.

5. Боротьба з нестабільними тестами: тест, що падає випадково, швидко вчить команду ігнорувати червоний CI. Нестабільний тест або виправляють одразу, або тимчасово виключають (з задачею на виправлення), але не перезапускають мовчки.

6. Тести як код:

  • спільні фабрики тестових даних замість копіювання об'єктів у кожному тесті;
  • допоміжні функції для монтування з провайдерами;
  • назви тестів описують поведінку: «показує помилку для порожньої пошти», а не «test 3».

7. Що прибирати: тести, що перевіряють реалізацію (внутрішні виклики, точну розмітку), знімки, які оновлюють не дивлячись, і дублікати тієї самої перевірки на різних рівнях.

Метрики здоров'я набору: час прогону в CI, частка нестабільних тестів, кількість тестів, змінених при рефакторингу без зміни поведінки. Остання - найкращий індикатор того, що тести перевіряють деталі реалізації.

Докладніше в документації: Vitest: середовище тестів

Core Web Vitals - три метрики Google, що описують досвід реального користувача. Вони впливають на ранжування в пошуку й показуються в Search Console.

Метрика Що вимірює Добре
LCP (Largest Contentful Paint) коли відмалювався найбільший видимий елемент: головне зображення, заголовок до 2,5 с
INP (Interaction to Next Paint) затримка від взаємодії (клік, натискання клавіші) до наступного відмальовування - найгірша з типових за весь візит до 200 мс
CLS (Cumulative Layout Shift) наскільки вміст «стрибає» під час завантаження до 0,1

Оцінка - за 75-м перцентилем реальних відвідувань: сторінка «добра», якщо щонайменше 75% візитів укладаються в поріг. INP замінив FID у 2024 році: FID вимірював лише першу взаємодію й лише затримку до початку обробки.

Як вимірювати:

  • польові дані (від реальних користувачів): звіт CrUX, Search Console, PageSpeed Insights, власний збір через бібліотеку web-vitals:
import { onLCP, onINP, onCLS } from 'web-vitals';

const send = (metric) => navigator.sendBeacon('/vitals', JSON.stringify(metric));
onLCP(send);
onINP(send);
onCLS(send);
  • лабораторні дані: Lighthouse, вкладка Performance у DevTools - відтворювані, але не замінюють польові (інший пристрій і мережа). INP у лабораторії майже не виміряти - немає реальних взаємодій.

Типові причини й виправлення:

LCP:

  • головне зображення завантажується пізно - fetchpriority="high", без loading="lazy" для першого екрана, preload;
  • повільна відповідь сервера (TTFB) - кешування сторінок, CDN;
  • блокуючі CSS і JavaScript - менше критичного CSS, defer для скриптів;
  • шрифти, що затримують текст, - font-display: swap.

INP:

  • довгі задачі в головному потоці - розбити роботу (scheduler.yield(), setTimeout), перенести обчислення у Web Worker;
  • важкі обробники подій - оновити інтерфейс одразу, а повільну роботу виконати після відмальовування;
  • великий DOM і дорогі перерахунки стилів;
  • сторонні скрипти (аналітика, чати, реклама).

CLS:

  • зображення й відео без розмірів - width/height чи aspect-ratio;
  • вміст, що вставляється над наявним (банери, рекламні блоки), - зарезервувати місце;
  • зміна шрифту після завантаження - size-adjust для запасного шрифту.

Для Laravel-сайту: Vite з розділенням коду, атрибути fetchpriority, width, height у Blade-компонентах зображень, Livewire wire:navigate для швидких переходів і кешування HTML на краю мережі для гостей.

Докладніше в документації: web.dev: Web Vitals

Питання з реальних технічних співбесід - 114 питань у 10 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.

Рівні
Junior 37 Middle 41 Senior 36

Готуєтесь до співбесіди не просто так: зараз на сайті 145 відкритих вакансій Laravel і PHP. Переглянути вакансії