Питання на співбесіді Front-End розробника: 37 питань із відповідями
Коротка відповідь: на співбесіді Junior Front-End розробника перевіряють не знання рідкісних фактів, а розуміння основ — семантики HTML, блокової моделі та розкладок CSS, типів, замикань і this у JavaScript, циклу подій, роботи браузера та базових речей у TypeScript і React. Нижче — 37 питань у тих формулюваннях, у яких їх ставлять, із короткими відповідями: спершу те, що варто сказати вголос, далі деталі й код. Питання згруповано в сім блоків; після них — практичні задачі з розв'язками, типові помилки та план підготовки на два тижні.
Я у фронтенд-розробці з 2014 року і понад дев'ять років викладаю. За моїми спостереженнями, початківці частіше спотикаються не на екзотиці, а на базі: можуть зібрати застосунок на React, але не можуть пояснити, чому setTimeout із нульовою затримкою спрацьовує пізніше за проміс. Тому в добірці найбільше місця відведено JavaScript, а кожен приклад коду можна запустити й перевірити самостійно.
Усі приклади на JavaScript запущено в Node.js v22.14.0, і вивід у коментарях — справжній. Приклади на TypeScript перевірено компілятором TypeScript 5.9.3 у режимі strict. Фрагменти HTML, CSS, React і роботи з DOM — мінімальні стандартні приклади, у межах статті їх не запускали. Добір питань — вибір автора з досвіду викладання, а не статистика: у конкретній компанії перелік буде іншим.
Як проходить співбесіда Front-End розробника?
Зазвичай відбір складається з чотирьох етапів: коротка розмова з рекрутером, технічне інтерв'ю, практичне завдання і фінальна зустріч із командою або керівником. Порядок і кількість етапів залежать від компанії: десь тестове завдання надсилають до технічного інтерв'ю, десь його замінює live coding. Запитайте про це рекрутера вже на першому дзвінку.
| Етап | Що перевіряють | Як готуватися |
|---|---|---|
| Скринінг із рекрутером | Мотивацію, очікування, англійську, відповідність резюме вакансії | Підготуйте розповідь про себе на 2–3 хвилини і відповідь на питання, чому фронтенд і чому ця компанія |
| Технічне інтерв'ю | Теорію з HTML, CSS, JavaScript, браузера та фреймворку з вакансії | Відповідайте на питання з цієї статті вголос: спершу коротка відповідь, потім приклад |
| Практичне завдання або live coding | Уміння писати код, читати чужий і пояснювати свої рішення | Розв'язуйте невеликі задачі з таймером і проговорюйте хід думки |
| Фінальна зустріч | Як ви працюєте в команді, реагуєте на зауваження і вчитеся | Пригадайте два-три приклади з навчальних проєктів: що не виходило і як ви це розв'язали |
Якщо ви лише плануєте шлях у професію, почніть із матеріалу Як стати Front-End розробником, а медіанні зарплати за рівнями зібрано на сторінці зарплати Front-End розробників.
Які теми перевіряють на Junior і Middle?
Від джуна чекають упевненої бази з HTML, CSS і JavaScript та робочого знання одного фреймворку; від Middle — розуміння, чому все влаштовано саме так, і вміння обирати між рішеннями. Таблиця показує, де проходить межа, без оцінок частоти питань.
| Тема | Що перевіряють | Рівень |
|---|---|---|
| HTML і доступність | Семантичні теги, форми, керування з клавіатури, текстові альтернативи | Junior |
| CSS | Блокова модель, специфічність, Flexbox і Grid, позиціювання, адаптивність | Junior |
| Основи JavaScript | Типи та приведення, область видимості, замикання, this, прототипи | Junior |
| Асинхронність | Проміси, async/await, порядок виконання в циклі подій | Junior; деталі черг — Middle |
| DOM і браузер | Події, делегування, сховища, CORS, шлях від URL до пікселів | Junior; рендеринг у деталях — Middle |
| TypeScript | Типи та інтерфейси, unknown, звуження типів, дженерики | Junior — основи; Middle — дженерики та службові типи |
| React | Компоненти, props і state, хуки, ключі, ефекти, повторний рендер | Junior; оптимізація рендеру — Middle |
| Інструменти та продуктивність | Git, збирач, Core Web Vitals | Junior — Git і загальне розуміння; Middle — налаштування та вимірювання |
Питання з HTML і доступності
Що таке семантична верстка і навіщо вона потрібна?
Семантична верстка — це вибір тегів за змістом вмісту, а не за зовнішнім виглядом: <header>, <nav>, <main>, <article>, <section>, <aside>, <footer>, заголовки <h1>–<h6>, <button> для дій і <a> для переходів.
Вона потрібна з трьох причин. Програми екранного доступу будують за такими тегами карту сторінки й дають змогу стрибати між областями та заголовками. Пошукові системи краще розуміють структуру документа. І код читається простіше: <nav> зрозуміліший, ніж <div class="menu-wrapper">. Перелік елементів із призначенням кожного є в довіднику MDN.
Типова помилка: обирати рівень заголовка за розміром шрифту. Рівні задають структуру, а розмір налаштовується в CSS.
Чим кнопка button відрізняється від div з обробником кліку?
<button> одразу отримує фокус із клавіатури, спрацьовує на Enter і пробіл, оголошується скринрідером як кнопка і вміє бути вимкненою через disabled. <div> нічого з цього не вміє: доведеться вручну додавати tabindex="0", role="button" та обробники клавіш.
Правило просте: дія на сторінці — <button>, перехід на іншу адресу — <a href>. Окрема деталь, яку люблять уточнювати: усередині форми кнопка без атрибута type має тип submit і надсилає форму, тому звичайним кнопкам пишуть type="button".
Як пов'язати підпис із полем форми і яку перевірку дає сам HTML?
Підпис пов'язують із полем через пару for та id або вкладають поле всередину <label>. Тоді клік по підпису ставить фокус у поле, а скринрідер зачитує підпис під час переходу до нього. Атрибут placeholder підпису не замінює: він зникає під час введення.
<form action="/subscribe" method="post">
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" required>
<button type="submit">Subscribe</button>
</form>
Вбудовану перевірку дають атрибути required, type (email, url, number), min, max, minlength, maxlength і pattern; тип поля ще й підказує мобільному браузеру потрібну клавіатуру. Докладніше — у посібнику MDN з валідації форм.
Що хочуть почути: перевірка в браузері — це зручність для користувача, а не захист. Дані однаково перевіряються на сервері.
Що таке доступність і що ви перевіряєте насамперед?
Доступність (accessibility, a11y) — це можливість користуватися сайтом людям із різними обмеженнями: без миші, зі скринрідером, зі слабким зором, на маленькому екрані. Вимоги описано у стандарті WCAG 2.2.
Мінімальна перевірка, яку можна зробити самостійно:
- увесь сценарій проходиться з клавіатури, і фокус завжди видно;
- змістовні зображення мають
alt, у декоративних він порожній (alt=""); - кожне поле форми має підпис;
- контраст звичайного тексту і тла не нижчий за 4,5:1 — це рівень AA у WCAG;
- заголовки йдуть по порядку і відображають структуру сторінки.
Про ARIA достатньо знати головне правило з настанов W3C: спершу нативний елемент HTML, а атрибути role та aria-* — лише коли відповідного елемента немає. Неправильна ARIA-розмітка гірша, ніж її відсутність.
Питання з CSS
Що таке блокова модель і що змінює box-sizing: border-box?
Кожен елемент — це прямокутник із чотирьох шарів: вміст, внутрішній відступ (padding), рамка (border) і зовнішній відступ (margin). Типово діє box-sizing: content-box: властивість width задає ширину лише вмісту, а padding і border додаються зверху. За border-box вони входять у задану ширину.
*,
*::before,
*::after {
box-sizing: border-box;
}
.card {
width: 300px;
padding: 20px;
border: 2px solid currentColor;
}
/* border-box: the card is 300px wide; content-box: 300 + 40 + 4 = 344px */
Варто згадати і схлопування відступів: вертикальні margin сусідніх блокових елементів не додаються, а береться більший із них. Опис моделі — у MDN: box-sizing.
Як обчислюється специфічність селекторів?
Специфічність — це вага селектора з трьох чисел: кількість ідентифікаторів, далі класів, атрибутів і псевдокласів, далі тегів і псевдоелементів. Порівняння йде зліва направо, тому один #id сильніший за будь-яку кількість класів. За однакової ваги перемагає правило, оголошене нижче.
p { color: black; } /* 0-0-1 */
.note { color: blue; } /* 0-1-0 */
p.note:hover { color: green; } /* 0-2-1 */
#intro { color: red; } /* 1-0-0 */
Інлайновий атрибут style сильніший за будь-який селектор, а !important порівнюється окремо і виграє у звичайних оголошень. Універсальний селектор і комбінатори ваги не додають, :where() завжди має нульову вагу, а :is() та :not() беруть вагу найспецифічнішого аргументу. Правила — у MDN: Specificity.
Що хочуть почути: війну специфічності не виграють через !important; стилі тримають пласкими — на класах, без ідентифікаторів і глибокої вкладеності.
Чим Flexbox відрізняється від Grid і коли що обирати?
Flexbox розкладає елементи вздовж однієї осі — у рядок або в колонку, а Grid керує рядками й колонками одночасно. Flexbox зручний, коли розміри диктує вміст: меню, панель кнопок, вирівнювання іконки з текстом. Grid — коли спершу є сітка, а вміст у неї вписується: макет сторінки, галерея карток.
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 24px;
}
Це не конкуренти: в одному макеті Grid зазвичай задає каркас сторінки, а Flexbox вирівнює вміст усередині комірок. Довідкові матеріали — MDN: Flexible box layout і MDN: Grid layout.
Які значення має position і чим вони відрізняються?
Значень п'ять: static, relative, absolute, fixed і sticky.
static— типове значення: елемент у звичайному потоці, властивостіtop,leftіz-indexна нього не діють.relative— елемент зсувається відносно свого звичайного місця, а місце за ним зберігається.absolute— елемент випадає з потоку і відраховує координати від найближчого предка зposition, відмінним відstatic.fixed— елемент прив'язаний до вікна браузера і не прокручується разом зі сторінкою.sticky— елемент поводиться як звичайний, доки не дійде до заданого відступу (наприклад,top: 0), після чого «прилипає» в межах свого контейнера прокручування.
Типова помилка: поставити absolute і дивуватися, що елемент полетів у кут сторінки, — батьківському елементу забули вказати position: relative. Повний опис — у MDN: position.
Як ви робите адаптивну верстку?
Я починаю зі стилів для вузького екрана і розширюю їх медіазапитами min-width (підхід mobile-first), використовую відносні одиниці та розкладки, які перебудовуються самі. У <head> обов'язково стоїть <meta name="viewport" content="width=device-width, initial-scale=1">, інакше мобільний браузер відмалює сторінку на ширину настільного екрана і зменшить її.
.layout {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
}
@media (min-width: 768px) {
.layout {
grid-template-columns: 240px 1fr;
}
}
h1 {
font-size: clamp(1.75rem, 4vw, 3rem);
}
img {
max-width: 100%;
height: auto;
}
Додаткові інструменти: rem замість пікселів для шрифтів, функція clamp() для плавних розмірів, контейнерні запити (@container), коли компонент має реагувати на ширину батьківського елемента, а не вікна. Основа — MDN: Using media queries.
Питання з основ JavaScript
Які типи даних є в JavaScript?
У мові сім примітивних типів — string, number, bigint, boolean, undefined, null, symbol — і один тип для всього іншого, object. Масиви, функції, дати — це об'єкти. Примітиви незмінні й порівнюються за значенням, об'єкти — за посиланням.
console.log(typeof 42); // number
console.log(typeof 'text'); // string
console.log(typeof true); // boolean
console.log(typeof undefined); // undefined
console.log(typeof 10n); // bigint
console.log(typeof Symbol('id')); // symbol
console.log(typeof null); // object
console.log(typeof {}); // object
console.log(typeof []); // object
console.log(typeof function () {}); // function
console.log(Array.isArray([])); // true
Що хочуть почути: typeof null повертає 'object' — це історична особливість мови, а не ознака того, що null є об'єктом; масив перевіряють через Array.isArray(). Таблиця значень — у MDN: typeof.
Чим відрізняється == від ===?
Оператор === порівнює без перетворень: якщо типи різні, результат одразу false. Оператор == спершу приводить операнди до спільного типу, через що результат буває неочевидним. У робочому коді використовують ===.
console.log(1 == '1'); // true
console.log(1 === '1'); // false
console.log(0 == ''); // true
console.log(null == undefined); // true
console.log(null === undefined); // false
console.log(null == 0); // false
console.log(NaN === NaN); // false
console.log(Object.is(NaN, NaN)); // true
console.log('5' + 2); // 52
console.log('5' - 2); // 3
Два останні рядки — про неявне приведення: + із рядком склеює рядки, а - працює лише з числами і перетворює рядок на число. Єдиний загальноприйнятий виняток для == — перевірка value == null, яка ловить одразу null та undefined. Алгоритми порівняння описано в MDN: Equality comparisons and sameness.
Чим відрізняються var, let і const?
var видима в усій функції, і її можна оголосити повторно; let і const видимі лише всередині блоку {}, де оголошені. const забороняє присвоювати змінній нове значення, але не робить об'єкт незмінним.
function demo() {
if (true) {
var a = 1;
let b = 2;
}
console.log(a); // 1
console.log(typeof b); // undefined
}
demo();
const user = { name: 'Olena' };
user.name = 'Ira'; // allowed: the binding is constant, not the object
console.log(user.name); // Ira
try {
user = {};
} catch (e) {
console.log(e.name + ': ' + e.message); // TypeError: Assignment to constant variable.
}
Що хочуть почути: типово — const, let — коли значення справді змінюється, var у новому коді не використовують.
Що таке hoisting (підняття)?
Підняття — це те, що оголошення обробляються до виконання коду у своїй області видимості. Оголошення функції доступне повністю ще до рядка, де воно написане. Змінна var існує від початку функції, але до присвоєння дорівнює undefined. Змінні let, const і класи теж «підняті», але до рядка оголошення перебувають у тимчасовій мертвій зоні, і звернення до них кидає ReferenceError.
console.log(a); // undefined
var a = 1;
console.log(sum(2, 3)); // 5
function sum(x, y) {
return x + y;
}
try {
console.log(b);
} catch (e) {
console.log(e.name + ': ' + e.message); // ReferenceError: Cannot access 'b' before initialization
}
let b = 2;
Функціональні вирази та стрілкові функції підпорядковуються правилам змінної, у яку записані: викликати їх до оголошення не можна. Термін розібрано в глосарії MDN.
Що таке замикання?
Замикання — це функція разом зі змінними зовнішньої області видимості, у якій її було створено. Функція зберігає доступ до цих змінних, навіть коли зовнішня функція вже завершилася.
function createCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const next = createCounter();
console.log(next()); // 1
console.log(next()); // 2
const other = createCounter();
console.log(other()); // 1
Змінна count недоступна ззовні, але живе, доки жива функція next; other має власну копію. На замиканнях побудовано приватний стан, обробники подій, debounce і хуки React.
Класичне уточнювальне питання — що виведе цикл із таймером:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log('var:', i), 0);
}
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log('let:', j), 0);
}
var: 3
var: 3
var: 3
let: 0
let: 1
let: 2
Із var змінна одна на весь цикл, і на момент спрацювання таймерів вона вже дорівнює 3. Із let на кожній ітерації створюється нова прив'язка, і кожна функція замикає свою. Докладніше — у MDN: Closures.
Як визначається значення this?
У звичайної функції this визначається тим, як її викликано, а не де оголошено. Виклик як методу (user.hello()) — this дорівнює об'єкту перед крапкою. Виклик через call, apply або bind — тому, що передали явно. Виклик із new — новому об'єкту. Простий виклик fn() у суворому режимі — undefined. Стрілкова функція власного this не має: вона бере його з навколишнього коду.
'use strict';
const user = {
name: 'Olena',
tags: ['js', 'css'],
hello() {
return 'Hi, ' + this.name;
},
describe() {
return this.tags.map((tag) => this.name + ':' + tag);
},
};
console.log(user.hello()); // Hi, Olena
const fn = user.hello;
try {
fn();
} catch (e) {
console.log(e.name); // TypeError
}
const bound = user.hello.bind(user);
console.log(bound()); // Hi, Olena
console.log(user.hello.call({ name: 'Ira' })); // Hi, Ira
console.log(user.describe()); // [ 'Olena:js', 'Olena:css' ]
Типова помилка: передати метод як колбек (button.addEventListener('click', user.hello)) і втратити контекст. Рішення — bind або стрілкова функція-обгортка. Усі випадки перелічено в MDN: this.
Як працює прототипне успадкування і що таке класи?
Кожен об'єкт має приховане посилання на інший об'єкт — прототип. Якщо властивості немає в самому об'єкті, рушій шукає її в прототипі, потім у прототипі прототипа — і так до null. Класи — це зручніший синтаксис поверх того самого механізму: методи класу лежать у prototype, а extends зв'язує прототипи в ланцюжок.
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return this.name + ' makes a sound';
}
}
class Dog extends Animal {
speak() {
return super.speak() + ': woof';
}
}
const dog = new Dog('Rex');
console.log(dog.speak()); // Rex makes a sound: woof
console.log(Object.getPrototypeOf(dog) === Dog.prototype); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
console.log(Object.hasOwn(dog, 'name')); // true
console.log(Object.hasOwn(dog, 'speak')); // false
console.log(typeof Dog); // function
Властивість name належить самому об'єкту, а метод speak знайдено за ланцюжком прототипів. Докладно — у MDN: Inheritance and the prototype chain.
Чим поверхнева копія об'єкта відрізняється від глибокої?
Поверхнева копія ({ ...obj }, Object.assign, [...arr]) копіює лише верхній рівень: вкладені об'єкти залишаються спільними з оригіналом. Глибока копія дублює всю структуру; вбудований спосіб — structuredClone().
const original = { name: 'Olena', tags: ['js'], address: { city: 'Kyiv' } };
const shallow = { ...original };
shallow.address.city = 'Lviv';
console.log(original.address.city); // Lviv
const deep = structuredClone(original);
deep.address.city = 'Odesa';
deep.tags.push('ts');
console.log(original.address.city); // Lviv
console.log(original.tags); // [ 'js' ]
try {
structuredClone({ run() {} });
} catch (e) {
console.log(e.name); // DataCloneError
}
structuredClone розуміє дати, Map, Set і циклічні посилання, але не копіює функції — на них він кидає помилку. Питання важливе для React: стан не можна змінювати на місці, його замінюють копією. Опис — у MDN: structuredClone().
Питання з асинхронного JavaScript
Що таке event loop (цикл подій)?
JavaScript виконує код в одному потоці: у кожен момент працює одна функція зі стека викликів. Цикл подій — це механізм, який, коли стек порожній, бере наступне завдання з черги і запускає його. Завдяки йому таймери, мережеві запити й події користувача не блокують сторінку: очікування відбувається поза потоком JavaScript, а в чергу потрапляє лише колбек.
Один оберт циклу в браузері має такий вигляд:
- взяти з черги одне завдання (скрипт, колбек таймера, обробник події) і виконати його до кінця;
- виконати всі накопичені мікрозавдання, зокрема ті, що додалися в процесі;
- за потреби перемалювати сторінку;
- перейти до наступного завдання.
Що хочуть почути: довга синхронна операція блокує все — і обробку кліків, і відмальовування, бо цикл не може перейти до наступного кроку. Модель описано у стандарті HTML.
Чим мікрозавдання відрізняються від макрозавдань?
Макрозавдання (у стандарті — просто завдання) — це колбеки setTimeout і setInterval, обробники подій, мережеві колбеки. Мікрозавдання — обробники промісів (then, catch, finally), продовження функції після await і queueMicrotask(). Після кожного макрозавдання черга мікрозавдань виконується повністю, тому проміс завжди випереджає setTimeout(fn, 0).
console.log('1: sync start');
setTimeout(() => console.log('2: timeout (macrotask)'), 0);
Promise.resolve().then(() => console.log('3: promise (microtask)'));
queueMicrotask(() => console.log('4: queueMicrotask (microtask)'));
console.log('5: sync end');
1: sync start
5: sync end
3: promise (microtask)
4: queueMicrotask (microtask)
2: timeout (macrotask)
Другий приклад показує, що мікрозавдання виконуються між двома таймерами, а не після всіх:
setTimeout(() => {
console.log('timeout 1');
Promise.resolve().then(() => console.log('microtask inside timeout 1'));
}, 0);
setTimeout(() => console.log('timeout 2'), 0);
Promise.resolve()
.then(() => console.log('microtask 1'))
.then(() => console.log('microtask 2'));
console.log('sync');
sync
microtask 1
microtask 2
timeout 1
microtask inside timeout 1
timeout 2
Розбір черг — у MDN: Using microtasks.
Що таке проміс і які в нього стани?
Проміс — це об'єкт, який представляє результат асинхронної операції. Він має три стани: очікування (pending), виконано (fulfilled) і відхилено (rejected). Вийти з очікування можна лише один раз, після цього стан і значення не змінюються.
Для кількох промісів є чотири статичні методи, і про різницю між ними питають часто:
const ok = (value, ms) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
const fail = (message, ms) => new Promise((_, reject) => setTimeout(() => reject(new Error(message)), ms));
async function main() {
console.log(await Promise.all([ok('a', 20), ok('b', 10)])); // [ 'a', 'b' ]
try {
await Promise.all([ok('a', 20), fail('boom', 10)]);
} catch (e) {
console.log('all rejected:', e.message); // all rejected: boom
}
const settled = await Promise.allSettled([ok('a', 20), fail('boom', 10)]);
console.log(settled.map((r) => r.status)); // [ 'fulfilled', 'rejected' ]
console.log(await Promise.race([ok('slow', 30), ok('fast', 10)])); // fast
console.log(await Promise.any([fail('boom', 10), ok('first success', 30)])); // first success
}
main();
Promise.all— чекає всі і падає на першій помилці; результати йдуть у порядку вихідного масиву, а не в порядку завершення.Promise.allSettled— чекає всі і ніколи не падає, повертає статус кожного.Promise.race— повертає перший завершений, успішно чи з помилкою.Promise.any— повертає перший успішний.
Довідник — MDN: Promise.
Як працює async/await і як обробляти в ньому помилки?
Функція з async завжди повертає проміс, а await призупиняє її до завершення проміса, не блокуючи потік: решта коду продовжує виконуватися. Помилку відхиленого проміса ловлять звичайним try/catch.
const load = (name, ms) => new Promise((resolve) => setTimeout(() => resolve(name), ms));
async function sequential() {
const start = Date.now();
const user = await load('user', 100);
const posts = await load('posts', 100);
return [user, posts, Math.round((Date.now() - start) / 100) * 100];
}
async function parallel() {
const start = Date.now();
const [user, posts] = await Promise.all([load('user', 100), load('posts', 100)]);
return [user, posts, Math.round((Date.now() - start) / 100) * 100];
}
async function withError() {
try {
await Promise.reject(new Error('network error'));
} catch (e) {
return 'caught: ' + e.message;
}
}
async function main() {
console.log(await sequential()); // [ 'user', 'posts', 200 ]
console.log(await parallel()); // [ 'user', 'posts', 100 ]
console.log(await withError()); // caught: network error
}
main();
Третє число — час виконання в мілісекундах, округлений до сотень.
Типова помилка: ставити await поспіль для незалежних запитів. Вони виконуються по черзі й забирають удвічі більше часу; незалежні операції запускають разом через Promise.all. Довідник — MDN: async function.
Ті самі теми — з тренером і code review
Курс Front-End у Prog Academy складається з модулів AI Start, «Верстка HTML/CSS» і JavaScript та триває 4,5 місяця. У програмі — Flexbox і Grid, прототипи, події та DOM, Promise і Fetch, React та/або Vue.js, збірка проєктів і дипломний проєкт для портфоліо з code review від викладача.
Програма курсу Front-End →Питання з DOM і браузера
Що таке спливання і перехоплення подій?
Подія в DOM проходить три фази: перехоплення (від window вниз до елемента), ціль (сам елемент) і спливання (від елемента назад угору по предках). Типово обробники, додані через addEventListener, спрацьовують на фазі спливання; щоб упіймати подію на перехопленні, третім аргументом передають { capture: true }.
Що зазвичай уточнюють:
event.target— елемент, на якому подія сталася;event.currentTarget— елемент, на якому висить обробник, що виконується;event.stopPropagation()зупиняє подальше поширення, аevent.preventDefault()скасовує типову дію браузера — перехід за посиланням або надсилання форми. Це різні речі;- спливають не всі події:
focusіblurне спливають, для них є спливні аналогиfocusinтаfocusout.
Наочне пояснення — у MDN: Event bubbling.
Що таке делегування подій?
Делегування — це один обробник на спільному предку замість окремого обробника на кожному дочірньому елементі. Працює завдяки спливанню: клік по кнопці всередині списку дійде до самого списку, а потрібний елемент знаходять через event.target.
const list = document.querySelector('#todo-list');
list.addEventListener('click', (event) => {
const button = event.target.closest('button[data-action="remove"]');
if (!button || !list.contains(button)) return;
button.closest('li').remove();
});
Переваг дві: менше обробників у пам'яті й автоматична робота з елементами, які додали до списку пізніше. Метод closest() потрібен, бо клік може припасти на іконку всередині кнопки, а не на саму кнопку.
Чим відрізняються localStorage, sessionStorage і cookie?
Усі три зберігають рядки в браузері, але живуть і передаються по-різному. localStorage зберігає дані безстроково, доки їх не видалять. sessionStorage живе до закриття вкладки, і кожна вкладка має власне. Cookie мають термін життя і, на відміну від сховищ, автоматично надсилаються на сервер із кожним запитом.
| Властивість | localStorage | sessionStorage | Cookie |
|---|---|---|---|
| Термін життя | Доки не видалять | До закриття вкладки | Задається через Expires або Max-Age; без них — до кінця сесії браузера |
| Надсилання на сервер | Ні | Ні | Автоматично з кожним запитом |
| Доступ із JavaScript | Так | Так | Так, якщо не виставлено прапорець HttpOnly |
| Обсяг | Кілька мегабайтів на джерело, залежить від браузера | Кілька мегабайтів на джерело, залежить від браузера | Близько 4 КБ на одну cookie |
Що хочуть почути: усе, що лежить у localStorage, може прочитати будь-який скрипт на сторінці, тому за XSS-вразливості токен звідти вкрадуть. Cookie з прапорцями HttpOnly, Secure і SameSite з JavaScript недоступна. Опис API — MDN: Web Storage API.
Що таке CORS і чому браузер блокує запит?
За правилом одного джерела (same-origin policy) скрипт сторінки не може читати відповіді з іншого джерела, а джерело — це поєднання протоколу, домену і порту. CORS — механізм, яким сервер дозволяє такі запити: він додає у відповідь заголовок Access-Control-Allow-Origin з адресою сторінки, і браузер віддає відповідь скрипту.
Для «непростих» запитів — наприклад, із методом PUT чи DELETE, з тілом у форматі JSON або з нестандартними заголовками — браузер спершу надсилає попередній запит OPTIONS (preflight) і лише після дозвільної відповіді робить основний.
Що хочуть почути: CORS — це обмеження браузера, а не сервера: той самий запит із curl або з іншого сервера пройде. Тому помилку CORS виправляють налаштуванням сервера або проксі, а не кодом на фронтенді. Деталі — у MDN: Cross-Origin Resource Sharing.
Що відбувається після того, як користувач увів адресу й натиснув Enter?
Браузер знаходить IP-адресу сервера через DNS, встановлює з'єднання (TCP і TLS для HTTPS), надсилає HTTP-запит і отримує HTML. Далі починається відмальовування:
- з HTML будується дерево DOM, з CSS — дерево стилів CSSOM;
- з них збирається дерево рендерингу — лише видимі елементи з обчисленими стилями;
- layout — обчислюються розміри та положення кожного елемента;
- paint — елементи відмальовуються в шари;
- composite — шари збираються в підсумкову картинку на екрані.
CSS блокує відмальовування: доки стилі не завантажено, сторінку не показують. Звичайний <script> блокує розбір HTML, тому скрипти підключають із defer (виконуються після розбору документа, по порядку) або async (виконуються одразу після завантаження, у довільному порядку); скрипти з type="module" відкладені типово. Докладніше — web.dev: Critical rendering path.
Питання з TypeScript і React
Чим type відрізняється від interface у TypeScript?
Обидва способи описують форму об'єкта, і здебільшого вони взаємозамінні. Різниця в можливостях: interface розширюється через extends і допускає злиття оголошень з однаковим іменем, а type уміє те, чого інтерфейс не може, — об'єднання, перетини, псевдоніми примітивів і кортежів.
interface User {
id: number;
name: string;
email?: string;
}
interface Admin extends User {
permissions: string[];
}
type Status = 'idle' | 'loading' | 'error';
type WithTimestamps<T> = T & { createdAt: Date };
const admin: Admin = { id: 1, name: 'Olena', permissions: ['users:edit'] };
const status: Status = 'loading';
const stored: WithTimestamps<User> = { id: 2, name: 'Ira', createdAt: new Date(0) };
console.log(admin.permissions.length, status, stored.createdAt.getTime()); // 1 loading 0
Що хочуть почути: головне — одноманітність у проєкті. Рекомендація з TypeScript Handbook — використовувати interface, доки не знадобляться можливості type. І варто додати, що типи існують лише на етапі компіляції: у підсумковому JavaScript їх немає, тому дані із сервера потрібно перевіряти окремо.
Чим any відрізняється від unknown і навіщо потрібен never?
any вимикає перевірку типів: зі значенням можна робити що завгодно, і помилка проявиться лише під час виконання. unknown теж приймає будь-яке значення, але не дає нічого з ним зробити, доки ви не звузите тип перевіркою. never — тип, який не має значень: його повертають функції, що завжди кидають помилку, і ним перевіряють, що у switch розібрано всі варіанти.
function parse(json: string): unknown {
return JSON.parse(json);
}
const data = parse('{"name":"Olena"}');
// console.log(data.name); // error TS18046: 'data' is of type 'unknown'.
if (typeof data === 'object' && data !== null && 'name' in data) {
console.log(data.name); // Olena
}
function assertNever(value: never): never {
throw new Error('Unexpected value: ' + String(value));
}
type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; side: number };
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'square':
return shape.side ** 2;
default:
return assertNever(shape);
}
}
console.log(area({ kind: 'square', side: 3 })); // 9
Якщо до Shape додати третій варіант і забути про нього у switch, компілятор вкаже на рядок з assertNever. Звуження типів описано в TypeScript Handbook: Narrowing.
Що таке дженерики і навіщо вони потрібні?
Дженерик — це параметр типу: функція або тип описуються один раз, а конкретний тип підставляється під час використання. Так код залишається універсальним і водночас типобезпечним, без any.
function first<T>(items: T[]): T | undefined {
return items[0];
}
const n = first([1, 2, 3]); // number | undefined
const s = first(['a', 'b']); // string | undefined
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map((item) => item[key]);
}
const users = [
{ id: 1, name: 'Olena' },
{ id: 2, name: 'Ira' },
];
console.log(n, s, pluck(users, 'name')); // 1 a [ 'Olena', 'Ira' ]
// pluck(users, 'age'); // error TS2345: Argument of type '"age"' is not assignable to parameter of type '"name" | "id"'.
Запис K extends keyof T — обмеження: другим аргументом можна передати лише ім'я наявної властивості. Від джуна зазвичай достатньо вміння читати такі сигнатури; писати складні — рівень Middle. Розділ довідника — TypeScript Handbook: Generics.
Чим props відрізняються від state у React?
Props — це дані, які компонент отримує від батьківського компонента; змінювати їх усередині компонента не можна. State — це власні дані компонента, які зберігаються між рендерами; їх змінюють лише функцією-сетером, і це спричиняє новий рендер.
import { useState } from 'react';
function Counter({ step = 1 }) {
const [count, setCount] = useState(0);
return (
<button type="button" onClick={() => setCount((current) => current + step)}>
Clicked: {count}
</button>
);
}
Тут step — props, count — state. Два уточнення, які варто назвати самому. Стан не змінюють на місці (items.push(...)), а замінюють новим значенням, інакше React не побачить зміни. І значення стану всередині одного рендеру не змінюється одразу після виклику сетера, тому, коли нове значення залежить від попереднього, у сетер передають функцію. Пояснення — у react.dev: State: A Component's Memory.
Навіщо потрібні ключі (key) у списках?
Ключ дає React змогу зрозуміти, який елемент списку якому відповідає між рендерами: що додали, що видалили, що переставили. Ключ має бути стабільним та унікальним серед сусідів — зазвичай це ідентифікатор із даних.
function TodoList({ todos }) {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
Типова помилка: використовувати індекс масиву як ключ. Якщо елементи вставляються, видаляються або сортуються, індекси зсуваються, і стан компонента — наприклад, текст у полі введення — опиняється в іншого елемента. Ключ із Math.random() ще гірший: елементи створюються заново на кожному рендері. Правила — у react.dev: Rendering Lists.
Як працює useEffect і навіщо потрібна функція очищення?
useEffect синхронізує компонент із чимось зовнішнім щодо React: підпискою, таймером, мережевим з'єднанням, стороннім віджетом. Ефект запускається після того, як React оновив екран. Масив залежностей визначає, коли запускати його знову: без масиву — після кожного рендеру, з порожнім масивом — один раз після появи компонента, з переліком значень — коли хоча б одне з них змінилося.
import { useEffect, useState } from 'react';
function OnlineStatus() {
const [online, setOnline] = useState(navigator.onLine);
useEffect(() => {
const update = () => setOnline(navigator.onLine);
window.addEventListener('online', update);
window.addEventListener('offline', update);
return () => {
window.removeEventListener('online', update);
window.removeEventListener('offline', update);
};
}, []);
return <p>{online ? 'Online' : 'Offline'}</p>;
}
Функція, яку повертає ефект, — очищення: React викликає її перед повторним запуском ефекту і під час видалення компонента. Без неї залишаються «завислі» підписки й таймери. У режимі розробки зі Strict Mode React один раз додатково запускає ефект і очищення, щоб такі помилки були помітні.
Що хочуть почути: ефект не потрібен, щоб обчислити щось із props або state, — це рахують просто під час рендеру. Довідник — react.dev: useEffect.
Коли компонент React рендериться заново?
Причин три: змінився стан самого компонента, заново відрендерився його батьківський компонент або змінилося значення контексту, який компонент читає. Важливо: під час рендеру батьківського компонента дочірні рендеряться типово, навіть якщо їхні props не змінилися.
Рендер — це виклик функції компонента, а не оновлення сторінки. Після нього React порівнює результат із попереднім і вносить у DOM лише відмінності, тому «зайвий» рендер не завжди проблема. Якщо він став дорогим, є інструменти: memo пропускає рендер компонента за тих самих props, useMemo запам'ятовує результат обчислення, useCallback — саму функцію. Проєкти з React Compiler отримують таку мемоізацію автоматично на етапі збірки.
Що хочуть почути: спершу виміряти в React DevTools Profiler, потім оптимізувати. Процес описано в react.dev: Render and Commit.
Які правила мають хуки і чому?
Правил два: хуки викликають лише на верхньому рівні компонента або власного хука — не в умовах, циклах і вкладених функціях, — і лише з React-функцій, а не зі звичайних. Причина в тому, що React зіставляє стан із хуком за порядком викликів: якщо на одному рендері хук викликано, а на іншому пропущено, значення зсунуться.
Виняток у сучасному React — функція use: її можна викликати всередині умов і циклів. Власний хук — це звичайна функція з іменем на use, яка викликає інші хуки; так виносять повторювану логіку на кшталт підписки з прикладу вище. Дотримання правил перевіряє лінтер — плагін eslint-plugin-react-hooks. Джерело — react.dev: Rules of Hooks.
Питання з інструментів і продуктивності
Навіщо потрібен збирач і що він робить?
Збирач перетворює вихідний код проєкту на файли, які швидко завантажуються в браузері. Він проходить графом імпортів, перетворює TypeScript і JSX на JavaScript, об'єднує модулі, видаляє невикористаний код (tree shaking), мініфікує результат і додає до імен файлів хеш вмісту, щоб браузер міг довго їх кешувати.
Ще два завдання: розділення коду (code splitting) — сторінка завантажує лише те, що потрібно їй зараз, а решта довантажується на вимогу через динамічний import(), — і сервер розробки з миттєвим оновленням модулів без перезавантаження сторінки. Найвідоміші збирачі — Vite і webpack. Мотивацію добре описано в документації Vite.
Що таке Core Web Vitals?
Core Web Vitals — це три метрики Google, які описують, як користувач відчуває швидкість сторінки:
| Метрика | Що вимірює | Добре значення |
|---|---|---|
| LCP (Largest Contentful Paint) | Коли відмальовано найбільший елемент у видимій області — швидкість завантаження | До 2,5 секунди |
| INP (Interaction to Next Paint) | Скільки минає від дії користувача до наступного відмальовування — чуйність | До 200 мілісекунд |
| CLS (Cumulative Layout Shift) | Наскільки елементи зсуваються під час завантаження — візуальна стабільність | До 0,1 |
Пороги оцінюються за 75-м перцентилем завантажень сторінки. Що допомагає: стиснені зображення в сучасних форматах і пріоритет для головного зображення (LCP), короткі обробники та розбиття довгих завдань (INP), атрибути width і height у зображень та зарезервоване місце під банери (CLS). Визначення і пороги — на web.dev: Web Vitals.
Чим git merge відрізняється від git rebase?
git merge з'єднує дві гілки і, якщо історії розійшлися, створює окремий коміт злиття: історія зберігається такою, якою була. git rebase переносить ваші коміти поверх іншої гілки, створюючи їх заново з новими хешами: історія виходить лінійною, але переписаною.
Що хочуть почути: правило з книжки Pro Git — не робити rebase комітів, які вже надіслано до спільного репозиторію і на яких хтось міг побудувати свою роботу. Власну локальну гілку перед pull request перебазувати можна, спільну — ні. Пояснення з рисунками — у Pro Git: Rebasing, а базові команди розібрано у статті Git для початківців.
Практичні задачі з розв'язками
На практичній частині джуну зазвичай дають невеликі задачі на функції, масиви та асинхронність — такі, що розв'язуються за 10–15 хвилин. Нижче чотири типові задачі; усі розв'язки запущено в Node.js v22.14.0.
Задача 1. Напишіть функцію debounce
Потрібно повернути функцію, яка викликає вихідну лише після того, як виклики припинилися на заданий час. Так роблять пошук під час введення: запит іде, коли користувач перестав друкувати.
function debounce(fn, delay) {
let timerId;
return function (...args) {
clearTimeout(timerId);
timerId = setTimeout(() => fn.apply(this, args), delay);
};
}
const search = debounce((query) => console.log('search:', query), 100);
search('r');
search('re');
search('react'); // only this call reaches the function
search: react
Що варто проговорити: timerId живе в замиканні, кожен новий виклик скасовує попередній таймер, а fn.apply(this, args) зберігає контекст та аргументи.
Задача 2. Розгорніть вкладений масив без методу flat
function flatten(items) {
const result = [];
for (const item of items) {
if (Array.isArray(item)) {
result.push(...flatten(item));
} else {
result.push(item);
}
}
return result;
}
console.log(flatten([1, [2, [3, [4]], 5]])); // [ 1, 2, 3, 4, 5 ]
console.log([1, [2, [3, [4]], 5]].flat(Infinity)); // [ 1, 2, 3, 4, 5 ]
Розв'язок рекурсивний: якщо елемент — масив, функція викликає сама себе. Після ручного розв'язку назвіть вбудований спосіб — flat(Infinity): інтерв'юеру важливо, що ви знаєте обидва.
Задача 3. Згрупуйте масив об'єктів за полем
function groupBy(items, getKey) {
return items.reduce((groups, item) => {
const key = getKey(item);
(groups[key] ??= []).push(item);
return groups;
}, {});
}
const users = [
{ name: 'Olena', role: 'dev' },
{ name: 'Ira', role: 'qa' },
{ name: 'Taras', role: 'dev' },
];
console.log(groupBy(users, (user) => user.role));
{
dev: [ { name: 'Olena', role: 'dev' }, { name: 'Taras', role: 'dev' } ],
qa: [ { name: 'Ira', role: 'qa' } ]
}
Оператор ??= створює масив для ключа, лише якщо його ще немає. У сучасних середовищах є вбудований Object.groupBy() з ES2024 — згадайте його після власного розв'язку.
Задача 4. Що виведе цей код і в якому порядку?
async function first() {
console.log('A');
await second();
console.log('B');
}
async function second() {
console.log('C');
}
console.log('D');
setTimeout(() => console.log('E'), 0);
first();
Promise.resolve().then(() => console.log('F'));
console.log('G');
D
A
C
G
B
F
E
Пояснення за кроками. D — синхронний код. Виклик first() виконується синхронно до першого await, тому одразу друкуються A і C. Решта first після await іде в чергу мікрозавдань, слідом туди ж потрапляє обробник then. Синхронний код закінчується рядком G, далі по порядку виконуються мікрозавдання — B і F, і лише потім макрозавдання таймера — E.
Типові помилки кандидатів
Більшість відмов на рівні Junior пов'язана не з прогалиною в одній темі, а з тим, як кандидат відповідає і міркує. Це спостереження автора, а не статистика:
- Завчені визначення без прикладу. «Замикання — це функція та її лексичне оточення» нічого не доводить, доки ви не покажете лічильник або
debounce. - Фреймворк замість мови. Знання хуків React не рятує, якщо незрозуміло, як працює
thisчи цикл подій. - Мовчання під час live coding. Інтерв'юер оцінює міркування; тишу він оцінити не може.
- Розв'язок без уточнень. Що на вході? Чи можуть бути порожні значення? Поставити два питання перед початком — ознака досвіду, а не слабкості.
- Вигадана відповідь замість «не знаю». Наступне уточнювальне питання це покаже.
- Проєкт, який кандидат не може пояснити. Особливо якщо код писав AI-асистент, а ви його не розбирали.
- Ігнорування HTML і доступності. «Я все роблю на div» — швидкий спосіб втратити бали вже на перших питаннях.
- Відсутність питань до інтерв'юера. Це сприймається як відсутність інтересу до роботи.
Які питання поставити інтерв'юеру?
Співбесіда — двостороння розмова: вам теж потрібно зрозуміти, як тут працюють і чи будете ви зростати. Оберіть три-чотири питання зі списку:
- Над яким продуктом або проєктом я працюватиму і який у нього стек?
- Як влаштована команда: скільки фронтенд-розробників, хто буде моїм наставником?
- Як проходить code review і скільки часу зазвичай триває?
- Якими будуть мої завдання в перший місяць?
- Який вигляд має процес від завдання до релізу: гілки, тести, деплой?
- Як у команді ставляться до використання AI-асистентів у роботі?
- За якими критеріями оцінюють результат випробувального терміну?
- Яким буде наступний етап відбору і коли чекати на відповідь?
План підготовки на два тижні
План розрахований на тих, хто вже вивчив основи і має хоча б один власний проєкт: по півтори-дві години на день на повторення і тренування відповідей уголос. Якщо якась тема для вас нова, двох тижнів на неї не вистачить — спершу вивчіть її за планом зі статті Як стати Front-End розробником.
| Дні | Тема | Що зробити |
|---|---|---|
| 1–2 | HTML і доступність | Зверстати форму з підписами та перевіркою, пройти її з клавіатури, відповісти вголос на питання блоку |
| 3–4 | CSS | Зібрати макет на Grid і Flexbox з однією контрольною точкою, пояснити вагу трьох своїх селекторів |
| 5–7 | Основи JavaScript | Запустити кожен приклад із блоку, змінити його і передбачити вивід до запуску |
| 8–9 | Асинхронність | Намалювати схему циклу подій, розв'язати п'ять задач «що виведе код» |
| 10 | DOM і браузер | Написати список із делегуванням подій, розповісти шлях від URL до відмальовування |
| 11 | TypeScript | Типізувати функції зі свого проєкту, замінити кожен any на точний тип або unknown |
| 12 | React | Пояснити вголос кожен useEffect у своєму проєкті: навіщо він, які залежності, чи потрібне очищення |
| 13 | Практика та інструменти | Розв'язати задачі з розділу вище з таймером; повторити Git: гілки, конфлікт, rebase |
| 14 | Пробна співбесіда | Попросити знайомого поставити питання або записати відповіді на диктофон; підготувати розповідь про себе і проєкт |
Для щоденної практики з автоматичною перевіркою завдань підійдуть безплатний курс із JavaScript і безплатний курс із Git. Якщо ви ще вагаєтеся щодо напряму, подивіться порівняння Frontend чи Backend: що обрати.
Часті запитання
Скільки часу потрібно на підготовку до співбесіди на Junior Front-End?
Якщо основи вже вивчено і є власні проєкти, на повторення теорії та тренування відповідей уголос зазвичай вистачає двох тижнів по півтори-дві години на день. Це орієнтир автора, а не норма: якщо тема зі списку питань для вас нова, її спершу треба вивчити й закріпити практикою, а на це йдуть тижні, а не вечори.
Чи буде на співбесіді live coding?
Найімовірніше, так, у тому чи іншому вигляді: написати невелику функцію, передбачити вивід коду або виправити верстку, коментуючи свої дії. Формат залежить від компанії, тому запитайте про нього рекрутера заздалегідь. Оцінюють не швидкість набору, а хід міркувань: проговорюйте, що робите і чому.
Чи потрібна англійська на співбесіді Front-End розробника?
Читати англійською потрібно в будь-якому разі: нею написані документація, повідомлення про помилки та назви в коді. Розмовну англійську перевіряють, якщо команда або замовник спілкуються нею, і потрібний рівень зазвичай зазначено у вакансії. Частину співбесіди можуть провести англійською, тому підготуйте розповідь про себе і про свій проєкт двома мовами.
Чи можна користуватися AI-асистентами на співбесіді та в тестовому завданні?
На співбесіді — лише якщо інтерв'юер прямо дозволив; якщо правила не названо, запитайте. У тестовому завданні асистенти зазвичай допустимі, але за кожен рядок відповідаєте ви: на розборі попросять пояснити рішення і змінити його на ходу. Код, який ви не можете пояснити, шкодить більше, ніж його відсутність.
Що робити, якщо не знаєш відповіді на питання?
Сказати про це прямо і показати, як ви міркуєте: що знаєте із суміжної теми, як перевірили б припущення, де подивилися б у документації. Вигадувати відповідь гірше, ніж зізнатися: наступне уточнювальне питання це покаже. Після співбесіди запишіть питання і розберіть його — воно трапиться знову.
Чи питають алгоритми на співбесіді Junior Front-End?
Складних алгоритмів від джуна у фронтенді вимагають рідко, але базові задачі на масиви, рядки та об'єкти трапляються часто: згрупувати, відфільтрувати, розгорнути вкладений масив, прибрати дублікати. Потрібно впевнено володіти методами масивів і розуміти, скільки разів ваш код проходить по даних.
Як виконувати тестове завдання?
Спершу зробіть те, що написано в умові, і лише потім покращення. Додайте README з інструкцією запуску і переліком ухвалених рішень, перевірте верстку на мобільній ширині та керування з клавіатури, робіть осмислені коміти. Якщо термін або умова незрозумілі, поставте питання до початку роботи — це нормальна робоча практика.
Чи потрібно знати React, щоб пройти співбесіду на Junior?
Дивіться на вакансію: якщо в ній зазначено React, питання про компоненти, стан і хуки будуть обов'язково. Але фреймворк не замінює основ: кандидата, який пише на React і не може пояснити замикання чи цикл подій, відсіюють на блоці з JavaScript.
Що розповідати про себе, якщо комерційного досвіду немає?
Розкажіть про проєкти: яку задачу розв'язували, які технології обрали і чому, що було найважчим і що ви переробили б зараз. Двох-трьох хвилин достатньо. Не видавайте навчальні проєкти за комерційні — це з'ясовується за кілька уточнювальних питань.
Джерела
- WHATWG: HTML Standard — Event loops
- MDN: HTML elements reference
- MDN: Client-side form validation
- W3C: Web Content Accessibility Guidelines (WCAG) 2.2
- W3C WAI: ARIA Authoring Practices Guide — Read Me First
- MDN: Specificity
- MDN: position
- MDN: Equality comparisons and sameness
- MDN: Closures
- MDN: this
- MDN: Inheritance and the prototype chain
- MDN: Using microtasks in JavaScript with queueMicrotask()
- MDN: Promise
- MDN: Web Storage API
- MDN: Cross-Origin Resource Sharing (CORS)
- web.dev: Critical rendering path
- web.dev: Web Vitals
- TypeScript Handbook: Everyday Types
- TypeScript Handbook: Narrowing
- TypeScript Handbook: Generics
- react.dev: Rendering Lists
- react.dev: useEffect
- react.dev: Render and Commit
- react.dev: Rules of Hooks
- Vite: Why Vite
- Scott Chacon, Ben Straub: Pro Git — Rebasing
