Відповіді на запитання під час співбесід з React та JavaScript, що показують справжню глибину знань.
Дослідіть сильніші, більш деталізовані відповіді на поширені запитання під час співбесід з React та JavaScript — від Virtual DOM до проектування систем, які демонструють глибші інженерні навички.
Вступ: Чому більшість кандидатів говорять однаково
Якщо ви присутні на достатньій кількості інтерв’ю з React, ви помітите певну закономірність: одні й ті самі стандартні фрази знову і знову звучать. „Virtual DOM працює швидше“. „useEffect використовується для побічних ефектів“. „JavaScript виконується на одному потоці“. Жодна з цих тверджень не є хибною, але це такі речі, які будь-хто може запам’ятати після швидкого прочитання кількох постів у блогах, і вони нічого не розповідають інтерв’юеру про те, як ви насправді мислите.
Те, що відрізняє сильного кандидата від того, хто отримує ввічливий лист про відмову, — це не те, чи знає він текстове визначення, а те, чи може він заглибитися на один рівень глибше. Чи можете ви пов’язати певну концепцію з компромісами, які за нею стоять? Чи можете ви пояснити, чому було прийнято певне рішення щодо дизайну, а не просто що воно робить? Саме такий судження насправді шукають інтерв’юери.
Цей посібник розглядає запитання, які дійсно виникають під час співбесід з фронтенд-розробниками середнього та вищого рівня, разом із достатньо детальними відповідями, які змушують співбесідника припинити швидко переглядати свої нотатки та по-справжньому прислухатися.
Розділ 1: Основні концепції React
Запитання 1: „Поясніть, що таке Virtual DOM. Як він працює?“
Забутня відповідь: „Virtual DOM — це легка копія справжнього DOM. React порівнює їх та оновлює лише те, що змінилося, що робить процес швидшим.“
Краща відповідь: Virtual DOM — це абстракція, але описувати його виключно як „швидший“ означає пропустити справжню суть. Реальна перевага у продуктивності походить не від самого Virtual DOM, а від логіки групування операцій та їх узгодження, створеної навколо нього.
React внутрішньо веде облік двох дерев: того, яке наразі відображається на екрані, та дерева у процесі створення, яке представляє те, що ось-ось буде відрендеровано. Коли змінюється стан, React не поспішає відразу міняти справжній DOM. Натомість він створює нове дерево Virtual DOM, проводить процедуру порівняння, щоб визначити найменший набір необхідних змін, а потім застосовує всі ці зміни до справжнього DOM у одному пакетному операції. Саме цей крок пакетування запобігає повторним обчисленням макету, що часто називається проблемою «layout thrashing».
Є ще одна нюанс, яку варто згадати: Virtual DOM — це не щось безкоштовне. Його алгоритм порівняння навмисно підтримується на рівні складності O(n) завдяки використанню евристик — наприклад, припущенню про те, що елементи різних типів створюватимуть абсолютно різні піддрева — замість виконання повністю універсального алгоритму порівняння дерев O(n³). Цей компроміс забезпечує швидку роботу у звичайних ситуаціях, але означає, що певні сценарії, як-от величезний список, де змінюється лише один елемент, все ще можуть бути ресурсоємними. Саме тому існують такі інструменти, як React.memo, useMemo, та бібліотеки для віртуалізації списків.
Варто також визнати, що Віртуальний DOM втрачає частину своєї привабливості як унікальна перевага. Компілятори на кшталт Svelte повністю пропускають етап Віртуального DOM, а сам React рухається у напрямку функцій одночасного відображення, які змінюють спосіб врегулювання даних у фоновому режимі. Віртуальний DOM вирішував певну проблему близько 2013 року. Розуміння причин його появи має більше значення, ніж здатність перерахувати його механізми.
Така відповідь є ефективною, тому що вона демонструє обізнаність з історією розвитку технологій, чесне визнання компромісів та розуміння того, як еволюціонує загальна ситуація у фронтенд-розробці — ви не просто відповідаєте на буквальне запитання, а й демонструєте, що розумієте місце цієї ідеї в загальній картині.
Запитання 2: „У чому різниця між useEffect, useLayoutEffect та коли варто використовувати кожен з них?“
Забутня відповідь: «useEffect виконується після відображення. useLayoutEffect виконується перед тим, як браузер намалює екран. Використовуйте useLayoutEffect, коли вам потрібно виміряти структуру DOM».
Краща відповідь: різниця у часу виконання є загальновідомою, але саме пояснення того, чому цей момент часу має значення, робить відповідь кращою. useEffect виконується асинхронно після того, як браузер вже намалював екран. useLayoutEffect, навпаки, виконується синхронно одразу після того, як React завершив обчислення змін у DOM, але ще до того, як браузер встигне намалювати екран.
Ця різниця означає, що useLayoutEffect фактично блокує візуальне оновлення. Якщо всередину нього помістити витратні обчислення, користувач побачить заморожений екран. Саме тому документація React рекомендує використовувати useEffect за замовчуванням — непотрібне блокування процесу малювання є поширеною проблемою продуктивності.
Проте існують обґрунтовані причини використовувати useLayoutEffect не лише для „вимірювання DOM“. Одним із хороших прикладів є запобігання помітному мерехтінню. Уявіть собі відображення підказки, положення якої залежить від розмірів цільового елемента — виконання розрахунку положення всередині useEffect спричиняє помітний спалах, коли підказка на мить з’являється не в тому місці, перш ніж опинитися на своєму місці. Виконання того самого розрахунку всередині useLayoutEffect повністю усуває цей спалах, оскільки він відбувається до того, як браузер намалює щось на екрані.
У цій родині існує ще один хук, який багато розробників ігнорують: useInsertionEffect. Його мета — дозволити інструментам CSS-in-JS вставляти правила стилів у документ до того моменту, коли ефекти макетування інакше отримали б застарілу інформацію про стилі з DOM. Більшості розробників він ніколи не знадобиться безпосередньо, але сам факт того, що він є частиною життєвого циклу ефектів у React, свідчить про глибше розуміння того, як React 18 в цілому керує ефектами.
Інтерв’юер може запитати детальніше, що станеться, якщо використовувати useLayoutEffect під час серверного рендерингу. Відповідь: React попередить вас, оскільки на сервері немає DOM для вимірювань. Цей хук просто не виконується під час SSR, тож будь-яка логіка, яка залежить від DOM, потребує або захисту лише для клієнта, або має бути переміщена до useEffect.
Запитання 3: „Поясніть поведінку відображення у React. Коли компонент переробляється?“
Проста відповідь: „Компонент переробляється щоразу, коли змінюється його стан або параметри.“
Більш детальна відповідь: це лише поверхневий погляд. Цікавішим є питання про те, що саме вважається „зміною“ та що робить React після виявлення такої зміни.
Компонент переробляється за трьох умов:
- Змінюється його власний локальний стан, зазвичай через функцію для зміни стану
- Переробляється його батьківський компонент, незалежно від того, чи справді змінилися передані параметри
- Змінюється значення контексту, яке використовує компонент
Ключова ідея полягає у другому пункті: React не порівнює пропси перед тим, як вирішити, чи потрібно перерендрувати дочірній компонент. Якщо рендрується батьківський компонент, за принципом роботи рендруються також його дочірні компоненти. Порівняння пропсів є обчислювально затратним, а у більшості реальних випадків дочірній компонент все одно потребує оновлення, тому відмова від цього порівняння за замовчуванням є розумним компромісом.
Саме тут розробники часто і неправильно звертаються до React.memo. Сам процес мемоізації також має свою вартість — React все одно мусить виконувати порівняння пропсів під час кожного рендру. Якщо ці пропси є складними об’єктами або якщо сам компонент, який обгортається, і так легко рендрується, то його обгортання в React.memo насправді може погіршити продуктивність замість того, щоб покращити її.
Справжня майстерність полягає у тому, щоб знати, коли оптимізація дійсно необхідна. Корисним правилом є уникання використання механізмів кешування, поки ви не виміряєте реальну проблему. Спочатку використовуйте інструмент Profiler у React DevTools, щоб знайти справжні вузькі місця, а лише потім цілеспрямовано застосовувати React.memo, useMemo чи useCallback. Передчасна оптимізація в React зазвичай означає працю проти концепцій фреймворку, а не разом з ним.
Функція паралельного відображення у React 18 додає ще один рівень складності. Тепер процеси відображення можна переривати, надавати їм пріоритет чи навіть відкидати під час виконання. Розуміння того, що процес відображення не завжди безпосередньо призводить до синхронної зміни DOM, є ключовим для написання коду, який правильно функціонує під час паралельного відображення.
Питання 4: Як слід керувати станом у великому додатку на React?
Слабка відповідь розглядає це як питання інструментарію: використовуйте Redux для всього глобального та useState для всього локального.
Сильніша відповідь починається з запитання про те, який саме стан задіяний та хто насправді від нього залежить, перш ніж обирати будь-яку бібліотеку.
Стан у великому додатку зазвичай поділяється на чотири групи:
- Локальний стан UI: значення форм, перемикачі, чи відкрито модальне вікно. Тут достатньо
useState. - Стан сервера: дані, отримані з бекенду. Для цього створені React Query чи SWR, адже вони вже реалізують функції кешування, усунення дублікатів, фонового оновлення та оптимістичних оновлень — функції, які Redux ніколи не був призначений забезпечувати.
Поширеною помилкою є використання Redux з самого початку проекту. Redux ідеально підходить для справді складної логіки на клієнтській стороні з багатьма взаємозалежними оновленнями, але більшість додатків насправді не мають такої проблеми — те, що здається станом клієнта, часто є лише прихованим станом сервера. Зберігання відповідей API всередині Redux схоже на використання величезного молотка для підвішування картини: це можливо, але це значно більше зусиль, ніж вимагає завдання.
Коли використання Redux справді є доцільним, хорошим підходом є поєднання Redux Toolkit з RTK Query. RTK Query бере на себе обов’язки щодо стану сервера, залишаючи Redux керувати лише тією клієнтською логікою, яка справді є складною. Розділення цих функцій робить загальну архітектуру більш зрозумілою.
Розділ 2: Глибоке дослідження JavaScript
Запитання 5: Поясніть замикання в JavaScript. Наведіть практичний приклад.
Поверхнева відповідь описує замикання як просто функцію, яка зберігає змінні з її навколишнього області видимості.
Глибша відповідь пов’язує це з лексичною областю видимості: коли створюється функція, вона зафіксовує посилання на змінні навколо неї у той момент і зберігає доступ до них навіть після того, як виконання переходить за межі цієї початкової області видимості.
Те, що вирізняє хорошого кандидата, — це здатність пояснити, чому ця поведінка має значення саме в React. Розгляньмо паттерн, який часто спричиняє помилки:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // Always logs 0
setCount(count + 1); // Resets to 1 every time
}, 1000);
}, []); // Empty deps = closure over initial count
}
Змінна count, на яку посилається функція setInterval, залишається незмінною на тому значенні, яке існувало під час початкового відображення. Оскільки цей ефект виконується лише один раз завдяки порожній масці залежностей, ця закрита функція ніколи не оновлюється новими значеннями. Просте додавання count до списку залежностей також не є правильним рішенням, адже це призведе до розбирання та повторної створення інтервалу після кожної зміни. Правильним рішенням є використання функціональної форми оновлення setCount(c => c + 1), яка повністю усуває необхідність використання застарілої закритої функції.
Замикання також спричиняють конфлікти з обробниками подій усередині користувацьких хуків. Кожного разу, коли обробник підключається всередині useEffect та читає дані з стану, задіюється замикання. Поширеною технікою для хука у стилі useEventListener є зберігання функції-обробника у ref, щоб обробник завжди міг читати найновішу версію даних без необхідності її повторного підключення.
Усе це не робить замикання чимось, що слід уникати — це основний механізм, який варто опанувати. Модульні патерни, приватні змінні, функції-фабрики та курірування — усе це ґрунтується на замиканнях. Важливою навичкою є точне визначення моменту утворення замикання та переконання в тому, що воно захоплює саме ту значення, яка була задумана.
Запитання 6: Що таке цикл подій? Поясніть мікрозавдання та макрозавдання.
Проста відповідь полягає у тому, що цикл подій керує асинхронною роботою, а мікрозавдання виконуються раніше за макрозавдання.
Більш детальна відповідь пояснює, що JavaScript виконується на одній потоці, а браузер імітує конкурентність за допомогою циклу подій. Синхронний код виконується у стеку викликів; коли зустрічається асинхронна операція, вона передається Web API — setTimeout, fetch, подіям DOM — і після завершення цієї роботи її функція-відповідь додається до черги.
Варто зазначити тонку деталь: існує не одна черга. Макрозавдання — setTimeout, setInterval, операції вводу-виводу — потрапляють у одну чергу, тоді як мікрозавдання — Promise.then, queueMicrotask, MutationObserver — у іншу. Як тільки стек викликів спорожніє, цикл подій спочатку очищує всю чергу мікрозавдань, перш ніж обробити хоча б одне макрозавдання.
Це створює справжню небезпеку: мікрозавдання можуть «загнобити» решту програми. Якщо нові мікрозавдання продовжують додаватися до черги рекурсивно, заплановані обробники setTimeout ніколи не отримають своєї черги. Така ситуація може заблокувати інтерфейс користувача, коли обіцянки поєднуються у циклі, не повертаючи керування браузеру.
Це безпосередньо пов’язано з тим, як React групує оновлення стану. У React 18 оновлення стану автоматично групуються незалежно від того, де вони виникають — всередині setTimeout, усередині Promise чи усередині нативного обробника подій. Раніше, до React 18, оновлення, запущені всередині setTimeout, виконувалися по одному, замість того щоб об’єднуватися. Розуміння циклу подій пояснює, чому автоматичне групування в React 18 є значущим: воно підключається до черги мікрозавдань, щоб усі заплановані оновлення були виконані разом перед наступним відображенням.
Вас також можуть попросити спрогнозувати результат короткого фрагмента коду, подібного до цього:
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
Очікувана відповідь: 1, 4, 3, 2 — синхронні оператори виконуються спочатку, потім йдуть мікрозавдання, такі як калебек функції Promise, і лише після цього виконується макрозавдання, заплановане за допомогою setTimeout.
Запитання 7: «Поясніть this у JavaScript. Якою є різниця між ним та іншими мовами?»
Слабка відповідь: «this вказує на об’єкт, який викликав функцію».
Сильна відповідь: «У JavaScript this діє за принципом динамічного обмеження області видимості, а не лексичного. У більшості мов значення self або this визначається моменту оголошення функції. У JavaScript же воно визначається під час виклику, залежно від способу її запуску, а не від місця розташування у коді».
«Існує порядок пріоритету чотирьох правил прив’язки:
- Нова прив’язка:
new Foo()встановлюєthisна щойно створений екземпляр - Явна прив’язка:
foo.call(obj),foo.apply(obj)абоfoo.bind(obj)змушуютьthisбути об’єктом, який ви передаєте
obj.foo() робить this рівним objfoo() залишає this як undefined у режимі strict mode, а в інших випадках звертається до globalThis""Функції-стрілки навмисно порушують цю схему — вони успадковують this лексично від того контексту, який їх оточує. Саме тому розробники використовували функції-стрілки всередині компонентів React на основі класів ще до появи хуків: це дозволяло уникнути необхідності викликати .bind(this) у конструкторі."
«Код у React, заснований на хуках, рідко безпосередньо використовує this, оскільки компоненти більше не є класами. Проте ця концепція знову з’являється під час технічного обслуговування старих класових компонентів, інтеграції бібліотек третіх сторін або під час співбесіди, коли інтерв’юер хоче перевірити ваші знання основ JavaScript. Поширеною проблемою в реальних умовах є передача методу об’єкта як колбека — наприклад, передача obj.handleClick до слухача подій — що усуває його неявне прив’язування та змушує this вказувати на непередбачуване місце».
Питання 8: «Що таке обіцянки JavaScript? Поясніть async/await».
Слабка відповідь: «Обіцянки керують асинхронною роботою, а async/await — це лише синтаксичний елемент для їхнього використання».
Сильна відповідь: «Обіцянка є заміною значенню, якого поки немає, але яке зрештою буде отримано. Вона замінює заплутані ланцюги калебеків на інтерфейс, який можна поєднувати, та послідовний спосіб визначення успіху чи невдачі».
«Те, що робить обіцянки справді корисними, — це не синтаксис, а гарантії, які стоять за ними. Як тільки обіцянка досягає свого стану — чи то виконана, чи відхилена — цей результат фіксується та більше не може змінитися. Саме ця незмінність робить їх здатними до комбінування та прогнозованими для аналізу».
«Називати async/await „лише штучним елементом“ — це применшувати їх значення; вони фундаментально змінюють спосіб написання асинхронної логіки, дозволяючи їй виглядати як синхронний код та значно полегшуючи її розуміння. Проте це створює кілька підводних каменів, на які варто звернути увагу».
// This runs sequentially - 6 seconds total
async function sequential() {
const a = await fetch('/a'); // 3s
const b = await fetch('/b'); // 3s
}
// This runs in parallel - 3 seconds total
async function parallel() {
const [a, b] = await Promise.all([fetch('/a'), fetch('/b')]);
}
"Частою помилкою серед менш досвідчених розробників є розміщення await всередині циклу, що ненавмисно змушує операції виконуватися одна за одною замість паралельно. Рішенням є використання Promise.all, коли операції не залежать одна від одної, а цикл for...of із await — у випадках, коли сувора послідовність дій справді необхідна."
"Обробка помилок — ще одна сфера, де трапляються проблеми. Обгортання виклику await у блоки try/catch дозволяє зафіксувати відмову, але якщо цього не зробити, неконтрольована відмова може призвести до зупинки процесу Node.js. На фронтенді обгортання асинхронних викликів у механізми обробки помилок або використання бібліотек, таких як React Query, які декларативно керують станами помилок, допомагає уникнути таких ситуацій."
Розділ 3: Проектування системи та архітектура
Запитання 9: «Спроєктуйте редактор документів у реальному часі для спільної роботи, подібний до Google Docs».
Це запитання повністю змінює фокус інтерв’ю. Інтерв’юер більше не допитує про ваші знання React — він хоче побачити, як ви мислите щодо архітектури системи.
Надійний підхід виглядає так:
«Перш ніж написати жодної рядка коду, я визначуся з вимогами:
- Скільки людей буде редагувати одночасно? Проектування для 10 користувачів зовсім не схоже на проектування для 10 000.
- Яка затримка є прийнятною — справжній реальний час чи щось ближче до майже реального часу?
- Чи потрібно, щоб додаток працював офлайн?
- Яку стратегію вирішення конфліктів ми оберемо?»
«Щодо фронтенду:
- Управління станом: кожен клієнт зберігає власну локальну копію документа. Зміни спочатку застосовуються оптимістично на клієнті, а потім надсилаються на сервер, який розповсюджує їх серед усіх інших підключених клієнтів.
- Оперативна трансформація або CRDT: це механізм для вирішення конфліктних змін. OT була початковою технологією Google, але вона залежить від центрального сервера для визначення порядку дій. CRDT (Conflict-free Replicated Data Types) можуть функціонувати в режимі peer-to-peer, і інструменти на кшталт Yjs роблять їх все більш поширеними.
requestAnimationFrame та обмеження частоти відправки запитів під час синхронізації, щоб уникнути надсилання запиту після кожного натискання клавіші."На рівні синхронізації:
- WebSocket забезпечує передачу даних у реальному часі
- Server-Sent Events або long-polling використовуються як альтернатива, якщо з’єднання WebSocket не може бути встановлене
"Справді складною частиною цієї проблеми є не процес відображення в React — це модель послідовності дій. Що має статися, коли двоє людей одночасно та в точній тій самій позиції курсора починають вводити текст? Відповідь повністю залежить від того, чи було обрано OT чи CRDTs, і саме це рішення впливає майже на кожен інший архітектурний вибір, який робиться далі."
Питання 10: "Як оптимізувати React-додаток, який повільно завантажується та неефективно функціонує?"
Незадовільна відповідь: перелік різних технік — мемоїзація, затримане завантаження, розділення пакетів — без будь-якого контексту.
Відповідь, яка вирізняється: «Я б почав з вимірювань, а не з припущень. React DevTools Profiler та панель Performance у Chrome DevTools показують, чи проблема полягає у часі завантаження, часі відображення або в обох — і немає сенсу оптимізувати сліпо».
Щодо завантаження:
- Розділення коду: розділення за маршрутами за допомогою React.lazy та Suspense є базовим підходом, але на цьому не варто зупинятися. Важкі компоненти, які не є відразу видимими — модали, контент під скроллом — також потребують окремих точок розділення.
- Попереднє завантаження: використовуйте
<link rel="preload">для критично важливих ресурсів, а також поєднуйте React.lazy з функцією prefetching для маршрутів, які користувач, ймовірно, відвідає далі.
import lodash from 'lodash' замість import debounce from 'lodash/debounce' може призвести до різниці у 100 КБ у кінцевому пакеті.Щодо взаємодії з користувачем:
- Віртуалізація: як тільки кількість елементів у списку перевищує приблизно 50, використовуйте react-window або react-virtualized. Відображення 10 000 елементів DOM одночасно ніколи не буде швидким, незалежно від ефективності решти коду.
- Дисциплінована стратегія мемоїзації: спочатку проаналізуйте ситуацію, потім дійте. Обгортайте дорогі обчислення у
useMemo, дорогі калебули — уuseCallback, а компоненти, які зайвий раз оновлюються, — уReact.memo. Мемоїзація всього за замовчуванням без оцінки реального впливу зазвичай лише збільшує навантаження, а не зменшує його. - Розміщення стану: тримайте стан якомога ближче до компонента, який його фактично використовує. Перенесення стану до спільного предка лише через те, що це здається більш організованим, спричиняє додаткові оновлення щоразу, коли змінюється цей стан.
- Розділення контекстів: коли один контекст поєднує оновлення високої частоти — наприклад, положення миші — з оновленнями низької частоти — наприклад, статус автентифікації — розділіть його на два. Інакше кожен рух миші змусить оновитися кожен компонент, який використовує цей контекст, навіть ті, яким важливий лише статус автентифікації.
Щодо сприйнятної продуктивності:
- Інтерфейс з екранами-скелетами замість індикаторів завантаження створює враження швидкості, оскільки контент здається наповнюваним поступово, а не одночасно.
- Поступове завантаження за допомогою механізму
Suspenseу React 18 дозволяє спочатку завантажувати критично важливий контент, а другорядні розділи — пізніше. - Варто стежити за показником Interaction to Next Paint, новішим метриком Core Web Vital від Google. Він замінює First Input Delay, оскільки вимірює швидку реакцію сайту протягом усього життєвого циклу сторінки, а не лише під час першої взаємодії. Ціль — забезпечити виконання обробників подій протягом менше ніж 200 мілісекунд.
Пов’язана література
- Помилки архітектури бекенду, які заважають командам React, що працюють переважно з фронтендом — пояснюється п’ять типових недоліків проектування бекенду, поширених у проєктах на React, від неправильного використання парадигми API до нестабільних розгортань, а також архітектурні рішення для забезпечення надійності на рівні продакшну.
- Керування станами інтерфейсу у реальних умовах за допомогою умовного відображення в React — дізнайтеся, як створювати інтерфейси для автентифікації, ролей, дозволів, стану завантаження, помилок та порожнього стану в React за допомогою практичних шаблонів умовного відображення.