Питання на співбесіді з 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')));
}
});
Життєвий цикл:
- install - підготовка: завантажити й закешувати потрібні файли;
- waiting - новий воркер чекає, доки закриються всі вкладки зі старим. Це захищає від ситуації, коли одна сторінка працює зі старими файлами, а запити обслуговує новий воркер;
- activate - новий воркер бере керування; тут видаляють старі кеші;
- fetch - перехоплення запитів.
self.skipWaiting() і clients.claim() прискорюють перемикання, але ціною можливої невідповідності версій.
Стратегії кешування:
- cache first - для хешованих статичних файлів, що не змінюються;
- network first - для HTML і API: свіже, а кеш лише як запас при відсутності мережі;
- stale-while-revalidate - миттєво з кешу, паралельно оновити кеш з мережі.
Бібліотека Workbox реалізує ці стратегії й генерує воркер під час збирання (у Vite - vite-plugin-pwa).
Чим небезпечна помилка:
- воркер живе довше за сторінку. Він встановлений у браузері користувача й перехоплює запити навіть після того, як ви виправили сайт. Якщо воркер помилково кешує HTML «назавжди», користувачі бачитимуть стару версію сайту, і деплой цього не виправить;
- оновлення самого воркера - браузер перевіряє, чи змінився
sw.js, при навігації на сайт (за замовчуванням в обхід HTTP-кешу, і щонайменше раз на 24 години). URLsw.jsмає лишатися незмінним: якщо перейменувати файл, старий воркер про новий так і не дізнається; - план відступу: заздалегідь мати «вимикач» - новий
sw.js, що видаляє кеші й знімає реєстрацію (self.registration.unregister()).
Обмеження: лише HTTPS (виняток - localhost), без доступу до DOM, область дії (scope) - каталог, з якого віддано скрипт, і нижче.
Для звичайного сайту (не PWA) Service Worker часто зайвий: HTTP-кешування з хешованими іменами файлів дає більшу частину вигоди без ризиків.
Дві суперечливі вимоги: статичні файли мають кешуватися якомога довше (швидкість, трафік), а після деплою користувачі мають одразу отримати нову версію. Розв'язок - різна політика для 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) чи розмір, а заголовки відповіді - реальну політику.
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.
Декоратор - функція, що отримує елемент класу (метод, поле, аксесор чи весь клас) і змінює чи доповнює його поведінку. Записується через @ перед оголошенням:
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)) дає той самий ефект без нового синтаксису.
Допоміжні методи ітераторів (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() для великих вибірок з бази.
Знімкові тести (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 на рев'ю.
У 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 - для складної клієнтської логіки окремо від браузера.
На старті будь-які тести швидкі. Через рік набір з тисячі тестів може йти 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, частка нестабільних тестів, кількість тестів, змінених при рефакторингу без зміни поведінки. Остання - найкращий індикатор того, що тести перевіряють деталі реалізації.
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 на краю мережі для гостей.
Питання з реальних технічних співбесід - 114 питань у 10 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.
- Теми
- Браузерні API й мережа 12 Сучасний синтаксис 12 Асинхронність 12 Типи й приведення 12 DOM і події 12 Масиви й об'єкти 12 Функції й замикання 12 Тестування 10
Готуєтесь до співбесіди не просто так: зараз на сайті 145 відкритих вакансій Laravel і PHP. Переглянути вакансії