Подготовка к frontend-собеседованию
Тренажёр учит не угадывать ответ, а объяснять его своими словами: коротко, по существу и с готовностью к уточняющим вопросам. Вопросы охватывают HTML, CSS, JavaScript, TypeScript, браузер, сети и доступность, а также архитектуру фронтенда, тестирование, React, управление состоянием, сборку проектов и производительность. Два уровня: Junior и Middle и выше.
Как пользоваться
Подготовка — круг из случайных вопросов по выбранной теме или по всем сразу: 10, 20, 30 или 40, по умолчанию 10. Ответьте вслух или запишите тезисы, затем откройте разбор: короткий ответ, подробное объяснение, уточняющие вопросы и частые ошибки.
Собеседование — вопросы из разных тем с таймером, ориентир — 2 минуты на ответ: 20 вопросов займут около 40 минут, как настоящее собеседование. Разборы откроются после завершения, там же итог по темам.
Повторение — вопросы, к которым пора вернуться. После разбора отметьте в чек-листе, что прозвучало в вашем ответе. Если отмечено 80% пунктов и больше, вопрос снова появится в этом режиме через 3 дня, потом через 7, 14 и 30 дней; если меньше — уже завтра. Так вопросы, которые вы знаете хуже, попадаются чаще.
Ответы автоматически не проверяются: чек-лист — это ваша самооценка. Записи и расписание повторения хранятся только в этом браузере.
Для вашего удобства рекомендую использовать полноэкранный режим.
Выберите режим и уровень.
Итог подхода
Напишите функцию и нажмите «Проверить»: решение выполнится в изолированной песочнице на ваших тестах. Код хранится только в этом браузере.
Убрать повторы
Напишите функцию unique(items), которая возвращает новый массив без повторяющихся значений. Порядок — как у первого появления, исходный массив не меняется.
Подсказка
- Есть встроенная коллекция, которая хранит только уникальные значения.
Setсохраняет порядок добавления, а массив из него можно получить через[...set].
Теория по теме
Нужно убрать повторы из списка тегов и посчитать, сколько раз встречается каждое слово в тексте. Когда для этого подходят Set и Map и чем Map лучше обычного объекта?
Set хранит только уникальные значения: [...new Set(tags)] убирает повторы. Map — пары «ключ — значение» с любыми ключами, своим размером size и порядком добавления. Для частоты слов: counts.set(word, (counts.get(word) ?? 0) + 1).
Решение и разбор
function unique(items) {
return [...new Set(items)];
}
Set хранит значения без повторов и помнит порядок добавления. Он сравнивает по алгоритму SameValueZero, поэтому NaN равен NaN.
Вариант items.filter((x, i) => items.indexOf(x) === i) работает за O(n²) и теряет NaN: indexOf(NaN) всегда возвращает -1. Если нужно убрать повторы объектов по полю, используют Map с этим полем в качестве ключа.
Сложность: O(n) по времени и O(n) по памяти.
Сгруппировать по полю
Напишите groupBy(items, key): функция возвращает объект, где ключ — значение поля key, а значение — массив элементов с этим значением в исходном порядке.
Подсказка
- Пройдите массив один раз и складывайте элементы в объект-результат.
- Если группы ещё нет, создайте пустой массив:
groups[value] ??= []. Объект-результат создайте черезObject.create(null): у{}уже есть ключиconstructorи__proto__, и группы с такими именами сломаются.
Теория по теме
Нужно убрать повторы из списка тегов и посчитать, сколько раз встречается каждое слово в тексте. Когда для этого подходят Set и Map и чем Map лучше обычного объекта?
Set хранит только уникальные значения: [...new Set(tags)] убирает повторы. Map — пары «ключ — значение» с любыми ключами, своим размером size и порядком добавления. Для частоты слов: counts.set(word, (counts.get(word) ?? 0) + 1).
Решение и разбор
function groupBy(items, key) {
const groups = Object.create(null);
for (const item of items) {
const value = item[key];
(groups[value] ??= []).push(item);
}
return groups;
}
Один проход по массиву: для каждого элемента берём значение поля и добавляем элемент в соответствующую группу.
В современных браузерах есть встроенный Object.groupBy(items, (item) => item[key]), а для ключей-объектов — Map.groupBy. Обычный объект превращает ключи в строки, а значение "__proto__" ведёт себя странно, поэтому надёжнее Object.create(null) или Map.
Эталон создаёт объект без прототипа: у него нет унаследованных ключей constructor и toString, а __proto__ работает как обычное имя группы.
Сложность: O(n) по времени и O(n) по памяти.
Частота слов
Напишите countWords(text): функция возвращает объект «слово → сколько раз встретилось». Регистр не важен, знаки препинания и лишние пробелы не считаются словами.
Подсказка
- Приведите текст к нижнему регистру и выделите слова регулярным выражением.
- Класс
\p{L}с флагомuнаходит буквы любого алфавита, включая кириллицу;\wеё не находит. - Счётчики храните в
Object.create(null)илиMap: у{}уже есть свойствоconstructor, и одноимённое слово посчитается неверно.
Теория по теме
Нужно убрать повторы из списка тегов и посчитать, сколько раз встречается каждое слово в тексте. Когда для этого подходят Set и Map и чем Map лучше обычного объекта?
Set хранит только уникальные значения: [...new Set(tags)] убирает повторы. Map — пары «ключ — значение» с любыми ключами, своим размером size и порядком добавления. Для частоты слов: counts.set(word, (counts.get(word) ?? 0) + 1).
Решение и разбор
function countWords(text) {
const counts = Object.create(null);
const words = text.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
for (const word of words) {
counts[word] = (counts[word] ?? 0) + 1;
}
return counts;
}
match с флагом g возвращает все совпадения или null, поэтому нужен запасной пустой массив. \w в JavaScript — только латиница, цифры и подчёркивание, для русского текста нужны классы Unicode.
Разбиение через split(' ') оставляет знаки препинания и пустые строки. Для частот удобен и Map: counts.set(word, (counts.get(word) ?? 0) + 1).
Объект без прототипа позволяет считать любое слово, включая constructor, без столкновения с унаследованными свойствами.
Сложность: O(n) по длине текста.
Проверка скобок
Напишите isBalanced(text): функция возвращает true, если скобки (), [] и {} в строке правильно открыты и закрыты. Остальные символы не важны.
Подсказка
- Счётчика открытых скобок мало:
([)]сбалансирована по количеству, но неверна. - Храните открытые скобки в стеке; закрывающая должна соответствовать последней открытой.
Решение и разбор
function isBalanced(text) {
const pairs = { ')': '(', ']': '[', '}': '{' };
const stack = [];
for (const char of text) {
if ('([{'.includes(char)) {
stack.push(char);
} else if (char in pairs) {
if (stack.pop() !== pairs[char]) return false;
}
}
return stack.length === 0;
}
Стек хранит ещё не закрытые скобки. Закрывающая скобка должна совпасть с верхней в стеке, иначе порядок нарушен. В конце стек должен быть пуст.
Классическая ошибка — считать количество скобок каждого вида: так ([)] и )( пройдут проверку.
Сложность: O(n) по времени, O(n) по памяти в худшем случае.
Развернуть вложенный массив
Напишите flatten(items), которая превращает массив любой вложенности в плоский. Не используйте Array.prototype.flat: на собеседовании его обычно просят написать самостоятельно.
Подсказка
- Если элемент — массив, его нужно развернуть тем же способом, что и весь массив.
- Проверка на массив —
Array.isArray(item); рекурсивный вызов возвращает плоский массив, который добавляют к результату.
Решение и разбор
function flatten(items) {
const result = [];
for (const item of items) {
if (Array.isArray(item)) {
result.push(...flatten(item));
} else {
result.push(item);
}
}
return result;
}
Рекурсия повторяет одну и ту же операцию для вложенных массивов. Для очень глубокой вложенности рекурсия может переполнить стек вызовов, тогда используют явный стек: берут элемент, если это массив — кладут его элементы обратно в стек.
push(...array) с массивом в сотни тысяч элементов тоже может упасть из-за лимита аргументов; там надёжнее цикл. В рабочем коде — items.flat(Infinity).
Сложность: O(n) по общему числу элементов.
Debounce
Напишите debounce(fn, ms). Она возвращает функцию, которая вызывает fn только через ms миллисекунд после последнего вызова — с последними аргументами и тем же this.
Подсказка
- Каждый вызов отменяет запланированный таймер и ставит новый.
- Чтобы сохранить
this, верните обычную функциюfunction (...args), а внутри таймера вызовитеfn.apply(this, args)из стрелочной функции.
Теория по теме
Поле поиска отправляет запрос на каждый символ, а обработчик прокрутки пересчитывает положение элементов сотни раз в секунду. Что такое debounce и throttle, что выбрать в каждом случае и как устроен debounce?
Debounce вызывает функцию, когда пауза после последнего события длится заданное время — подходит для поиска. Throttle вызывает её не чаще раза в заданный интервал — подходит для прокрутки и изменения размера. Debounce — таймер, который сбрасывается при каждом событии.
Каковы три результата вызовов и почему два счётчика не делят одно значение?
Будет 1, 2, 1. Каждая функция-счётчик запоминает переменную value из своего вызова makeCounter. Это замыкание: функция сохраняет доступ к окружению, где была создана.
Решение и разбор
function debounce(fn, ms) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), ms);
};
}
Переменная timer живёт в замыкании и общая для всех вызовов возвращённой функции. Каждый вызов сбрасывает прошлый таймер, поэтому fn выполнится только после паузы.
Возвращаемая функция обычная, а не стрелочная: только так она получает this от вызова counter.add(). Стрелка внутри setTimeout берёт этот this из окружения. В библиотечных версиях есть ещё cancel() и вызов в начале паузы (leading).
Сложность: O(1) на вызов.
Разбить массив на части
Напишите chunk(items, size): функция делит массив на части длиной size и возвращает массив этих частей. Последняя часть может быть короче. Исходный массив не меняется.
Подсказка
- Идите по массиву с шагом
size, а не по одному элементу. items.slice(i, i + size)возвращает очередную часть и не меняет исходный массив; конец за пределами массиваsliceобрезает сам.
Теория по теме
Функция сортирует список товаров для вывода, и после этого «ломается» исходный порядок в другом месте приложения. Почему? Какие методы массива меняют его, а какие возвращают новый?
sort меняет исходный массив, а массивы передаются по ссылке — все, у кого есть ссылка, видят изменения. Меняют массив: push, pop, shift, unshift, splice, sort, reverse, fill. Возвращают новый: map, filter, slice, concat, toSorted, toReversed, with.
Решение и разбор
function chunk(items, size) {
const result = [];
for (let i = 0; i < items.length; i += size) {
result.push(items.slice(i, i + size));
}
return result;
}
Цикл с шагом size и slice дают части по порядку, последняя получается короче сама: slice не выходит за конец массива.
Частая ошибка — splice в цикле: он режет исходный массив, а вызывающий код этого не ждёт. На собеседовании стоит спросить, что делать при size меньше 1: такой вызов зациклит наивное решение, поэтому в рабочем коде его отклоняют ошибкой.
Сложность: O(n) по времени и O(n) по памяти.
Выбрать поля объекта
Напишите pick(object, keys): функция возвращает новый объект только с перечисленными собственными полями. Ключей, которых нет в объекте, в результате быть не должно. Исходный объект не меняется.
Подсказка
- Создайте пустой объект и пройдите по списку ключей.
- Копируйте поле, только если оно действительно есть у объекта:
Object.hasOwn(object, key). Проверкаobject[key] !== undefinedпотеряет поле со значениемundefined.
Теория по теме
Есть тип User с полями id, name, email и password. Как без копирования полей получить тип для формы редактирования, где всё необязательно, и тип для ответа API без пароля?
Через встроенные утилиты: Partial<User> делает все поля необязательными, Omit<User, 'password'> убирает пароль, Pick<User, 'id' | 'name'> оставляет только нужные поля. Новые типы обновятся сами, если поменяется User.
Решение и разбор
function pick(object, keys) {
const result = {};
for (const key of keys) {
if (Object.hasOwn(object, key)) {
result[key] = object[key];
}
}
return result;
}
Проход по ключам и Object.hasOwn отличают отсутствующее поле от поля со значением undefined и не копируют унаследованные методы вроде toString. Оператор in здесь не подходит: он видит и прототип.
Обратная задача — omit: пройти по Object.keys(object) и пропустить лишние. Для коротких записей используют Object.fromEntries(keys.filter(...).map(...)), а в TypeScript у этих функций есть готовые типы Pick и Omit.
Сложность: O(k) по числу ключей.
Анаграммы
Напишите isAnagram(first, second): функция возвращает true, если строки состоят из одних и тех же букв в одинаковом количестве. Регистр и пробелы не важны.
Подсказка
- Сначала приведите обе строки к одному виду: нижний регистр, без пробелов.
- Посчитайте, сколько раз встречается каждая буква в первой строке, и вычтите буквы второй. Все счётчики должны вернуться к нулю. Сортировка букв тоже работает, но медленнее.
Теория по теме
Нужно убрать повторы из списка тегов и посчитать, сколько раз встречается каждое слово в тексте. Когда для этого подходят Set и Map и чем Map лучше обычного объекта?
Set хранит только уникальные значения: [...new Set(tags)] убирает повторы. Map — пары «ключ — значение» с любыми ключами, своим размером size и порядком добавления. Для частоты слов: counts.set(word, (counts.get(word) ?? 0) + 1).
Решение и разбор
function isAnagram(first, second) {
const normalize = (text) => text.toLowerCase().replace(/\s+/g, '');
const a = normalize(first);
const b = normalize(second);
if (a.length !== b.length) return false;
const counts = new Map();
for (const char of a) counts.set(char, (counts.get(char) ?? 0) + 1);
for (const char of b) {
const left = counts.get(char);
if (!left) return false;
counts.set(char, left - 1);
}
return true;
}
Счётчик в Map проходит строки по одному разу. Проверка длины в начале отсекает разные строки сразу, а после неё достаточно убедиться, что ни одна буква второй строки не ушла в минус.
Вариант [...a].sort().join('') === [...b].sort().join('') короче и подходит для собеседования, но работает за O(n log n). Ошибочный вариант — проверить только, что каждая буква одной строки есть в другой: так 'aab' и 'abb' окажутся анаграммами.
Сложность: O(n) по времени и O(k) по памяти, где k — число разных букв.
Два числа с заданной суммой
Напишите twoSum(numbers, target): функция возвращает индексы двух разных элементов, сумма которых равна target, в порядке возрастания, например [0, 2]. Если пары нет, верните null. Решение должно работать за O(n): массив может содержать сотни тысяч чисел.
Подсказка
- Для каждого числа вы знаете, какое второе число ему нужно:
target - number. - Храните уже просмотренные числа в
Map«число → индекс». Если нужное число уже там — пара найдена.
Теория по теме
Нужно убрать повторы из списка тегов и посчитать, сколько раз встречается каждое слово в тексте. Когда для этого подходят Set и Map и чем Map лучше обычного объекта?
Set хранит только уникальные значения: [...new Set(tags)] убирает повторы. Map — пары «ключ — значение» с любыми ключами, своим размером size и порядком добавления. Для частоты слов: counts.set(word, (counts.get(word) ?? 0) + 1).
Решение и разбор
function twoSum(numbers, target) {
const seen = new Map();
for (let i = 0; i < numbers.length; i++) {
const index = seen.get(target - numbers[i]);
if (index !== undefined) return [index, i];
seen.set(numbers[i], i);
}
return null;
}
Один проход: для числа ищем в Map его «пару» среди уже просмотренных. Поиск в Map в среднем O(1), поэтому всё решение линейное. Так как число добавляется в Map после проверки, элемент не составит пару сам с собой, а найденный индекс всегда меньше текущего.
Перебор всех пар двумя циклами — O(n²): на 200 000 чисел это 20 млрд сравнений. Если массив отсортирован, работают два указателя с концов массива и память O(1). Проверка if (index) вместо !== undefined — частая ошибка: индекс 0 ложный.
Сложность: O(n) по времени и O(n) по памяти.
Memoize
Напишите memoize(fn) для функции одного аргумента. Она возвращает функцию, которая при повторном вызове с тем же аргументом возвращает сохранённый результат и не вызывает fn снова. Сохранённым должен быть и результат undefined.
Подсказка
- Кеш нужно хранить в замыкании: он общий для всех вызовов возвращённой функции.
- Обычный объект превращает ключи в строки, и
1совпадёт с'1'.Mapхранит ключи любого типа, аhasотличает отсутствие записи от сохранённогоundefined.
Теория по теме
Каковы три результата вызовов и почему два счётчика не делят одно значение?
Будет 1, 2, 1. Каждая функция-счётчик запоминает переменную value из своего вызова makeCounter. Это замыкание: функция сохраняет доступ к окружению, где была создана.
Решение и разбор
function memoize(fn) {
const cache = new Map();
return function (arg) {
if (!cache.has(arg)) {
cache.set(arg, fn.call(this, arg));
}
return cache.get(arg);
};
}
Map в замыкании хранит пары «аргумент → результат». Проверка cache.has(arg), а не cache.get(arg) !== undefined, нужна, чтобы не пересчитывать функции, которые законно возвращают undefined.
Для нескольких аргументов ключ собирают, например, через JSON.stringify(args), но тогда объекты сравниваются по содержимому и ключ дорого строить. Кеш без ограничений растёт вечно: в рабочем коде его ограничивают (LRU) или берут WeakMap для ключей-объектов. Мемоизировать стоит только чистые функции.
Сложность: O(1) на вызов в среднем, O(k) по памяти для k разных аргументов.
Throttle
Напишите throttle(fn, ms). Она возвращает функцию, которая вызывает fn сразу при первом вызове, а затем не чаще одного раза в ms миллисекунд. Если за время ожидания были вызовы, после паузы fn выполняется ещё раз с последними аргументами.
Подсказка
- Храните время последнего вызова
fnи таймер отложенного вызова в замыкании. - Если интервал прошёл — вызывайте сразу. Иначе запомните последние аргументы и, если таймера ещё нет, поставьте его на оставшееся время.
Теория по теме
Поле поиска отправляет запрос на каждый символ, а обработчик прокрутки пересчитывает положение элементов сотни раз в секунду. Что такое debounce и throttle, что выбрать в каждом случае и как устроен debounce?
Debounce вызывает функцию, когда пауза после последнего события длится заданное время — подходит для поиска. Throttle вызывает её не чаще раза в заданный интервал — подходит для прокрутки и изменения размера. Debounce — таймер, который сбрасывается при каждом событии.
Решение и разбор
function throttle(fn, ms) {
let last = 0;
let timer = null;
let pending = null;
return function (...args) {
const wait = ms - (Date.now() - last);
if (wait <= 0 && !timer) {
last = Date.now();
fn.apply(this, args);
return;
}
pending = { self: this, args };
if (!timer) {
timer = setTimeout(() => {
timer = null;
last = Date.now();
fn.apply(pending.self, pending.args);
pending = null;
}, Math.max(wait, 0));
}
};
}
Throttle ограничивает частоту, а debounce ждёт паузы. Throttle подходит для прокрутки и перемещения мыши, когда реакция нужна во время действия, debounce — для поиска при вводе.
Без завершающего вызова теряется последнее состояние: например, позиция прокрутки после остановки. Частая ошибка — ставить новый таймер на каждый вызов: тогда вызовы снова идут чаще, чем задано. В lodash у throttle есть настройки leading и trailing и метод cancel().
Сложность: O(1) на вызов.
Развернуть строку с эмодзи
Напишите reverseText(text), которая разворачивает строку так, чтобы видимые символы не ломались: эмодзи с оттенком кожи и буквы с комбинируемым ударением должны остаться целыми.
Подсказка
split('')делит строку на кодовые единицы UTF-16, а[...text]— на кодовые точки. Ни то, ни другое не равно видимому символу.- Видимые символы (графемы) выделяет
Intl.Segmenter.
Теория по теме
Поле ограничено двадцатью видимыми символами, но эмодзи расходует несколько единиц length. Что считает JavaScript и как считать символы для пользователя?
length считает кодовые единицы UTF-16, а spread строки — кодовые точки. Видимый символ может состоять из нескольких кодовых точек; для такого лимита используют сегментацию графем через Intl.Segmenter.
Решение и разбор
function reverseText(text) {
const segments = new Intl.Segmenter().segment(text);
return Array.from(segments, (part) => part.segment).reverse().join('');
}
Строки в JavaScript хранятся в UTF-16. Эмодзи 😀 — две кодовые единицы, и split('') разрывает пару. [...text] исправляет это, но 👍🏽 — две кодовые точки (жест и оттенок), а «é» может быть буквой и отдельным знаком ударения.
Intl.Segmenter по умолчанию делит текст на графемы — то, что пользователь видит как один символ. Тот же подход нужен для подсчёта длины в полях с ограничением и для обрезки текста.
Сложность: O(n) по длине строки.
Свой Promise.all
Напишите promiseAll(values), которая ведёт себя как Promise.all для массива: результаты в исходном порядке, обычные значения принимаются как готовые, первая ошибка сразу отклоняет общий промис. Promise.all и Promise.allSettled не используйте.
Подсказка
- Верните
new Promiseи подпишитесь на каждое значение черезPromise.resolve(value).then(...). - Записывайте результат по индексу и считайте, сколько осталось; когда счётчик дошёл до нуля —
resolve. Пустой массив обработайте отдельно.
Теория по теме
Страница загружает три независимых виджета. Ошибка одного не должна скрывать остальные. Что выбрать: Promise.all или Promise.allSettled? Отменит ли all остальные запросы при ошибке?
Promise.allSettled: он дождётся всех запросов и вернёт статус каждого, ошибка одного не скроет остальные. Promise.all отклоняется при первой ошибке, но уже запущенные запросы не отменяет.
Нужно загрузить 200 картинок для обработки. В чём разница между await в цикле for, Promise.all(items.map(…)) и загрузкой не больше пяти одновременно? Как написать последний вариант?
await в цикле — по одному, медленно. Promise.all запускает все 200 сразу — перегружает сеть и сервер, а первая ошибка отклоняет весь результат. Ограничение параллельности — пул из пяти «работников», каждый берёт следующий элемент, когда закончил свой.
Решение и разбор
function promiseAll(values) {
return new Promise((resolve, reject) => {
const results = new Array(values.length);
let left = values.length;
if (left === 0) {
resolve(results);
return;
}
for (let index = 0; index < values.length; index++) {
const value = values[index];
Promise.resolve(value).then((result) => {
results[index] = result;
left--;
if (left === 0) resolve(results);
}, reject);
}
});
}
Результат записывают по индексу, а не через push: промисы завершаются в любом порядке. Счётчик показывает, когда готовы все. Promise.resolve(value) превращает обычное значение в выполненный промис.
Повторные вызовы resolve и reject после первого игнорируются, поэтому первая ошибка выигрывает. Остальные операции при этом не отменяются. Настоящий Promise.all принимает любой итерируемый объект, а не только массив.
Цикл по индексам обрабатывает и пустые позиции массива как undefined. forEach пропускает их, из-за чего счётчик незавершённых операций никогда не обнулится.
Сложность: O(n) операций плюс время самого медленного промиса.
Глубокое сравнение
Напишите deepEqual(a, b) для примитивов, обычных объектов и массивов. Объекты равны, если у них одинаковые собственные ключи с глубоко равными значениями; порядок ключей не важен, массив не равен объекту, NaN равен NaN.
Подсказка
- Сначала быстрый случай:
Object.is(a, b)верен для одинаковых примитивов,NaNи одной и той же ссылки. - Дальше оба значения должны быть объектами не
null, одного вида (массив или нет), с одинаковым числом ключей — и каждое значение сравнивается рекурсивно.
Теория по теме
Чем отличается == от ===? Что вернут эти сравнения?
=== — строгое сравнение: значения равны, только если совпадают и тип, и значение. == сначала приводит операнды к общему типу по сложным правилам. Результаты: true, true, true, false, false. В коде используют ===, а == допускают разве что для проверки x == null.
Решение и разбор
function deepEqual(a, b) {
if (Object.is(a, b)) return true;
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) {
return false;
}
if (Array.isArray(a) !== Array.isArray(b)) return false;
const keys = Object.keys(a);
if (keys.length !== Object.keys(b).length) return false;
return keys.every((key) => Object.hasOwn(b, key) && deepEqual(a[key], b[key]));
}
Object.is отличается от === для NaN и -0. Проверка typeof null === 'object' — частая ловушка, поэтому null исключают явно.
Проверка через Object.hasOwn отличает отсутствующий ключ от ключа со значением undefined. В реальной библиотеке ещё сравнивают Date, Map, Set, прототипы и защищаются от циклических ссылок. JSON.stringify для сравнения не подходит: он зависит от порядка ключей и теряет undefined.
Сложность: O(n) по общему числу значений в структурах.
Не больше N запросов одновременно
Напишите асинхронную mapLimit(items, limit, fn): она вызывает асинхронную fn(item, index) для каждого элемента, одновременно выполняет не больше limit операций и возвращает результаты в исходном порядке. Новая операция должна начинаться сразу, как только освободилось место, а не после завершения всей «пачки».
Подсказка
- Запустите
limit«работников»: каждый в цикле берёт следующий необработанный индекс и ждёт его результата. - Общий счётчик
next++безопасен: JavaScript однопоточный, и между чтением и увеличением никто не вклинится. Дождитесь всех работников и верните массив.
Теория по теме
Нужно загрузить 200 картинок для обработки. В чём разница между await в цикле for, Promise.all(items.map(…)) и загрузкой не больше пяти одновременно? Как написать последний вариант?
await в цикле — по одному, медленно. Promise.all запускает все 200 сразу — перегружает сеть и сервер, а первая ошибка отклоняет весь результат. Ограничение параллельности — пул из пяти «работников», каждый берёт следующий элемент, когда закончил свой.
Решение и разбор
async function mapLimit(items, limit, fn) {
const results = new Array(items.length);
let next = 0;
async function worker() {
while (next < items.length) {
const index = next++;
results[index] = await fn(items[index], index);
}
}
const workers = Array.from({ length: Math.min(limit, items.length) }, worker);
await Promise.all(workers);
return results;
}
Пул работников держит ровно limit операций в работе: закончив свою, работник сразу берёт следующую. Деление на пачки по limit с Promise.all на каждую проще, но медленнее: вся пачка ждёт самую долгую операцию.
Результаты пишут по индексу. Если нужно продолжать после ошибок, оборачивают fn в try/catch и собирают статусы, как allSettled. В рабочем коде используют проверенные библиотеки вроде p-limit.
Сложность: O(n) операций; время — примерно сумма длительностей, делённая на limit.
EventEmitter
Напишите класс EventEmitter с методами on(name, listener), off(name, listener), once(name, listener) и emit(name, ...args). Обработчики вызываются в порядке подписки. on возвращает функцию отписки. Отписка во время emit не должна ломать текущий вызов остальных обработчиков.
Подсказка
- Храните обработчики в
Map«имя события → массив функций». Обычный объект столкнётся с унаследованными ключами вродеconstructor. - В
emitпроходите по копии массива:[...listeners]. Тогда отписка внутри обработчика не сдвинет индексы.once— это обёртка, которая отписывает себя и вызывает исходную функцию.
Теория по теме
При каждом открытии панели добавляют обработчик, а при закрытии вызывают removeEventListener с новой стрелкой. Почему после трёх открытий действие выполняется трижды?
Снятие слушателя требует того же объекта функции, а новая стрелка — другая функция. Сохраните обработчик или объедините слушатели одного открытия через AbortController.
Решение и разбор
class EventEmitter {
#listeners = new Map();
on(name, listener) {
if (!this.#listeners.has(name)) this.#listeners.set(name, []);
this.#listeners.get(name).push(listener);
return () => this.off(name, listener);
}
off(name, listener) {
const list = this.#listeners.get(name);
if (!list) return;
const index = list.findIndex((item) => item === listener || item.original === listener);
if (index !== -1) list.splice(index, 1);
}
once(name, listener) {
const wrapper = (...args) => {
this.off(name, wrapper);
listener(...args);
};
wrapper.original = listener;
return this.on(name, wrapper);
}
emit(name, ...args) {
for (const listener of [...(this.#listeners.get(name) ?? [])]) {
listener(...args);
}
}
}
Map хранит списки обработчиков по имени события без конфликтов с прототипом. emit обходит копию списка: если обработчик отписывается во время вызова, splice в исходном массиве иначе сдвинул бы индексы, и следующий обработчик был бы пропущен.
once оборачивает функцию; поле original позволяет снять её через off с исходной функцией. В Node.js есть готовый EventEmitter, в браузере — EventTarget с addEventListener и опцией { once: true }. На собеседовании стоит обсудить, что делать с ошибкой в одном из обработчиков: сейчас она прерывает emit.
Сложность: emit — O(k) по числу обработчиков события, off — O(k).
Повтор с задержкой
Напишите асинхронную retry(fn, attempts, delay). Она вызывает асинхронную fn() и при ошибке повторяет вызов, пока не исчерпает attempts попыток всего. Между попытками ждёт delay мс, и каждое следующее ожидание вдвое длиннее предыдущего. Возвращает первый успешный результат или отклоняется последней ошибкой.
Подсказка
- Цикл по попыткам с
try { return await fn(); } catch (error) { ... }. Безawaitвнутриtryотклонение промиса не попадёт вcatch. - Перед следующей попыткой ждите
delay * 2 ** attempt, но только если попытка не последняя. После цикла бросьте сохранённую ошибку.
Теория по теме
Функция возвращает save() из try, но её catch не ловит отклонение промиса. Что изменит return await save()?
Возврат промиса передаёт его результат вызывающему коду, но не удерживает локальный try до отклонения. return await позволяет локальному catch обработать асинхронную ошибку.
POST создания заказа завершился тайм-аутом. Можно ли автоматически повторить его? Как избежать двух заказов?
Тайм-аут не значит, что заказ не создан: мог потеряться только ответ. Слепой повтор POST создаст дубль. Нужен ключ идемпотентности: клиент шлёт один и тот же ключ при повторах, а сервер не создаёт второй заказ с тем же ключом.
Поиск получил 429 и Retry-After: 30. Нужно ли сразу повторить запрос и как избежать лавины повторов от клиентов?
429 сообщает о превышении лимита запросов. Клиент учитывает Retry-After, ограничивает повторы и отменяет устаревшие запросы; без указаний сервера применяет задержку с увеличением и случайным разбросом.
Решение и разбор
async function retry(fn, attempts, delay) {
let lastError;
for (let attempt = 0; attempt < attempts; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (attempt < attempts - 1) {
await new Promise((resolve) => setTimeout(resolve, delay * 2 ** attempt));
}
}
}
throw lastError;
}
return await внутри try важен: без await функция вернёт отклонённый промис мимо catch, и повтора не будет. Ожидание после последней попытки бессмысленно и только задерживает ошибку.
Экспоненциальная задержка снижает нагрузку на упавший сервер. В рабочем коде к ней добавляют случайный разброс (jitter), чтобы тысячи клиентов не повторяли запросы одновременно, повторяют только временные ошибки (сеть, 503, 429 с учётом Retry-After) и не повторяют неидемпотентные запросы вроде создания заказа без ключа идемпотентности.
Сложность: O(attempts) вызовов; общее ожидание — delay × (2^(attempts−1) − 1).
LRU-кеш
Напишите класс LRUCache: new LRUCache(capacity), методы get(key) и set(key, value). get возвращает значение или undefined. Когда записей становится больше capacity, удаляется та, к которой дольше всего не обращались. Чтение через get и перезапись через set считаются обращением. Оба метода — O(1).
Подсказка
Mapпомнит порядок добавления ключей, а первый ключ — самый старый:map.keys().next().value.- Чтобы отметить обращение, удалите ключ и добавьте его снова — он переедет в конец. При переполнении удалите первый ключ.
Теория по теме
Нужно убрать повторы из списка тегов и посчитать, сколько раз встречается каждое слово в тексте. Когда для этого подходят Set и Map и чем Map лучше обычного объекта?
Set хранит только уникальные значения: [...new Set(tags)] убирает повторы. Map — пары «ключ — значение» с любыми ключами, своим размером size и порядком добавления. Для частоты слов: counts.set(word, (counts.get(word) ?? 0) + 1).
Решение и разбор
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.map = new Map();
}
get(key) {
if (!this.map.has(key)) return undefined;
const value = this.map.get(key);
this.map.delete(key);
this.map.set(key, value);
return value;
}
set(key, value) {
this.map.delete(key);
this.map.set(key, value);
if (this.map.size > this.capacity) {
this.map.delete(this.map.keys().next().value);
}
}
}
Map хранит ключи в порядке вставки, поэтому «удалить и вставить заново» переносит ключ в конец за O(1), а самый давний ключ всегда первый. Это решение принимают на собеседовании, но часто просят объяснить классическую схему: хеш-таблица плюс двусвязный список, где узел переносится в голову за O(1).
Ошибочные варианты — массив ключей с indexOf и splice (O(n) на операцию) или поиск самой старой записи по меткам времени. LRU используют в кешах браузера, ORM и библиотеках вроде lru-cache.
Сложность: O(1) на get и set, O(capacity) по памяти.
Значение по пути
Напишите get(object, path, fallback): функция возвращает значение по пути вида 'user.address.city' или 'items.0.title'. Если на пути встретился null, undefined или поля нет, функция возвращает fallback и не падает. Если значение по пути есть и равно undefined, тоже верните fallback; остальные значения, включая 0, '', false и null, возвращаются как есть.
Подсказка
- Разбейте путь через
split('.')и спускайтесь по ключам по одному. - Перед каждым шагом проверяйте
current == null— это ловит иnull, иundefined. В конце вернитеfallback, только если результатundefined:??заменит иnull, а||— ещё и0.
Теория по теме
API может вернуть пользователя без адреса. Как безопасно прочитать город, не получив ошибку Cannot read properties of undefined?
Использовать опциональную цепочку: user.address?.city вернёт undefined, а не ошибку, если address нет. С оператором ?? можно задать запасное значение: user.address?.city ?? 'Город не указан'. Ошибка возникает потому, что обращаться к свойству у undefined нельзя.
Нулевой лимит допустим и означает «ничего не показывать». Почему первый вариант ошибочен для limit=0 и какой вариант подходит?
|| заменит и ноль: limit || 10 при limit = 0 даст 10. ?? заменяет только null и undefined, поэтому limit ?? 10 сохранит 0.
Решение и разбор
function get(object, path, fallback) {
let current = object;
for (const key of path.split('.')) {
if (current == null) return fallback;
current = current[key];
}
return current === undefined ? fallback : current;
}
Проверка current == null перед шагом защищает от «Cannot read properties of undefined». Примитивы тоже работают: у строки есть length, а у числа нет поля value, и результатом станет undefined.
Возврат через || — классическая ошибка: 0, пустая строка и false заменятся на запасное значение. В современном коде при известном пути пишут user?.address?.city ?? 'нет', а get из lodash нужен для путей, которые приходят строкой, и понимает ещё запись items[0].title.
Сложность: O(k) по длине пути.
Решение на JavaScript
Редактор загружается…
Вывод console.log
Какие пары попадут в new FormData(form) для этой формы? Объясните роль name, disabled, readonly и состояния checkbox.
<form>
<input name="user" value="Ира">
<input value="без имени">
<input name="role" value="admin" disabled>
<input name="city" value="Пермь" readonly>
<input type="checkbox" name="agree" value="yes">
</form>
Интервьюер может уточнить
- Как отправить значение поля, которое нельзя редактировать?
- Как сервер узнает, что галочка снята?
Разбор
user=Ира и city=Пермь. Значение без name не имеет ключа для отправки. disabled исключает поле из данных формы, а readonly лишь запрещает редактирование поддерживаемого поля. Неотмеченный checkbox не добавляет пару, поэтому отсутствие agree нельзя автоматически трактовать как строку «false». На сервере отсутствие поля обрабатывают явно.Уточняющие вопросы
Как отправить значение поля, которое нельзя редактировать?
Сделать его readonly, а не disabled, или продублировать значение в <input type="hidden">. Сервер всё равно должен проверять такое значение: пользователь может его изменить.
Как сервер узнает, что галочка снята?
По отсутствию поля в данных. Если нужно явное значение, сервер считает отсутствие как «нет», а не ждёт строку false.
- Названы
user=Ираиcity=Пермь; другие поля исключены. nameопределяет ключ,readonlyне исключает поле,disabledисключает.- Неотмеченный
checkboxне передаёт значение.
Почитать на сайте
Спецификация: WHATWG: данные и проверка формы
Форма проверяет email через type="email" и required. Можно ли убрать проверку на сервере? Что делать с серверной ошибкой после отправки?
Интервьюер может уточнить
- Как обойти проверку браузера?
- Что делать, если почта корректная по формату, но уже занята?
Разбор
Уточняющие вопросы
Как обойти проверку браузера?
Удалить атрибут required в DevTools, добавить форме novalidate или отправить запрос напрямую, без формы, например через curl.
Что делать, если почта корректная по формату, но уже занята?
Это может проверить только сервер. Он возвращает ошибку для конкретного поля, а интерфейс показывает её рядом с полем почты и переводит туда фокус.
- Проверка браузера помогает пользователю, но обходится.
- Сервер проверяет данные и правила операции независимо.
- Ошибка показывается рядом с полем; введённые значения сохраняются.
Почитать на сайте
Спецификация: WHATWG: проверка формы на клиенте
Зачем в начале документа нужен <!DOCTYPE html> и что обязательно должно быть в <head>?
Интервьюер может уточнить
- Что будет, если не указать кодировку?
- Чем
titleотличается отh1? - Можно ли подключать скрипты в
head?
Разбор
DOCTYPE — не тег, а указание браузеру, по каким правилам разбирать страницу. Строка <!DOCTYPE html> включает стандартный режим. Без неё браузер имитирует поведение старых версий: например, иначе считает высоту строк и размеры блоков, и одна и та же вёрстка выглядит по-разному.
В <head> находятся сведения о документе, которые не выводятся как содержимое. Минимум: <meta charset="utf-8"> в самом начале, чтобы кириллица не превратилась в «кракозябры», и <title> — название вкладки и заголовок в поисковой выдаче. Почти всегда нужны viewport для мобильных, описание description, подключение стилей и иконка сайта. Атрибут lang у <html> сообщает язык страницы скринридерам и переводчикам.
Уточняющие вопросы
Что будет, если не указать кодировку?
Кодировка может быть объявлена HTTP-заголовком или BOM. Если этих сведений нет, важен <meta charset="utf-8">, полностью размещённый в первых 1024 байтах. Полагаться на угадывание не стоит.
Чем title отличается от h1?
<title> — название документа: вкладка, закладки, поисковая выдача. На самой странице он не виден. <h1> — главный заголовок содержимого, который видит читатель. Тексты могут совпадать, но title обычно короче и часто включает название сайта.
Можно ли подключать скрипты в head?
Можно, но обычный <script> в <head> остановит разбор страницы, пока не загрузится и не выполнится. Поэтому скрипты либо ставят в конец <body>, либо подключают в <head> с атрибутом defer.
DOCTYPEвключает стандартный режим, без него браузер работает в режиме совместимости.- В
headнаходятся сведения о документе, а не видимое содержимое. - Назван
meta charset(utf-8) в началеhead. - Назван
titleи его назначение: вкладка и поисковая выдача. - Упомянут
meta viewportдля мобильных устройств.
Почитать на сайте
Что такое семантическая разметка? Зачем писать <header>, <nav>, <main> и <article>, если всё можно сделать на <div>?
Интервьюер может уточнить
- Чем
articleотличается отsection? - Сколько может быть
headerиmainна странице? - Как семантика связана с заголовками?
Разбор
Внешне <nav> и <div> ничем не отличаются: оба блочные и без оформления. Разница в смысле. Семантические теги создают ориентиры (landmarks): скринридер позволяет сразу перейти к основному содержимому <main> или к навигации <nav>. Поисковики лучше понимают, какой текст главный. Разработчик видит структуру страницы, не разбираясь в классах.
Основные теги: <header> и <footer> — шапка и подвал страницы или раздела; <nav> — основные блоки ссылок навигации; <main> — уникальное содержимое страницы, один на странице; <article> — самостоятельный материал, который имеет смысл и отдельно: статья, карточка товара, комментарий; <section> — тематический раздел, обычно с заголовком; <aside> — дополнительное содержимое сбоку. <div> остаётся для обёрток, которые нужны только для оформления.
Уточняющие вопросы
Чем article отличается от section?
<article> — законченный материал, который можно вынести на другую страницу или в RSS, и он останется понятным. <section> — часть чего-то большего, тематический раздел со своим заголовком. Статья может состоять из секций, а секция «Новости» — из статей.
Сколько может быть header и main на странице?
<main> — один видимый на странице. <header> и <footer> могут быть у всей страницы и у отдельных статей или разделов.
Как семантика связана с заголовками?
Структуру документа задают в первую очередь заголовки <h1>–<h6>: скринридеры позволяют переходить по ним. Разделы <section> обычно начинаются с заголовка, а уровни идут по порядку, без пропусков ради размера шрифта.
- Семантика — выбор тега по смыслу содержимого, а не по виду.
- Названа польза для скринридеров: ориентиры и навигация по областям.
- Названа польза для поисковиков и читаемости кода.
- Правильно описаны хотя бы три тега:
header,nav,main,article,section,aside,footer. divостаётся для обёрток без смысловой роли.
Почитать на сайте
Чем блочные элементы отличаются от строчных? Приведите примеры и объясните, почему у <span> не работает width.
Интервьюер может уточнить
- Что даёт
display:inline-block? - Можно ли вложить блочный элемент в ссылку?
Разбор
Блочные по умолчанию: <div>, <p>, заголовки, списки, <section>. Они выстраиваются сверху вниз, занимают всю доступную ширину, принимают width, height и все отступы.
Строчные: <span>, <a>, <strong>, <em>, <code>. Они живут внутри строки текста и переносятся вместе с ним. Ширина строчного элемента — это ширина его текста, поэтому width игнорируется. Горизонтальные padding и margin работают, а вертикальные padding рисуются, но не раздвигают соседние строки.
Это поведение задаёт CSS-свойство display, и его можно поменять. display: inline-block оставляет элемент в строке, но позволяет задать размеры — так делают кнопки и бейджи. Элементы <img> и <input> строчные, но у них есть собственные размеры, поэтому width для них работает.
Уточняющие вопросы
Что даёт display: inline-block?
Элемент стоит в строке рядом с текстом, как inline, но принимает width, height и вертикальные отступы, как блок. Между такими элементами появляется небольшой зазор от пробелов в HTML-коде.
Можно ли вложить блочный элемент в ссылку?
Да, в HTML5 ссылка <a> может оборачивать блоки, например целую карточку. Нельзя только вкладывать интерактивные элементы: ссылку в ссылку или кнопку в ссылку. А вот <p> не может содержать блоки вроде <div>.
- Блочный элемент начинается с новой строки и занимает всю ширину.
- Строчный идёт внутри текста, размер по содержимому.
widthиheightне действуют на обычный строчный элемент.- Тип определяется свойством
displayи меняется в CSS. - Назван
inline-blockкак промежуточный вариант.
Почитать на сайте
Как правильно расставлять заголовки <h1>–<h6>? Можно ли взять <h4>, потому что он подходит по размеру?
Интервьюер может уточнить
- Можно ли несколько
h1на странице? - Логотип в шапке — это
h1?
Разбор
Уровень заголовка — это его место в иерархии. Страница обычно начинается с одного <h1> с основной темой. Разделы — <h2>, их подразделы — <h3> и так далее. Переходить вниз можно только на один уровень: после h2 идёт h3, а не h5. Вверх можно возвращаться на любой уровень.
Пользователи скринридеров часто просматривают страницу по списку заголовков, как по оглавлению. Если уровни выбраны по размеру шрифта, структура для них ломается. Внешний вид заголовка меняют в CSS: h2 { font-size: 1.25rem; }. А если текст лишь выглядит крупным, но не открывает раздел, это не заголовок, а абзац с классом.
Уточняющие вопросы
Можно ли несколько h1 на странице?
Технически HTML это не запрещает, но на практике рекомендуют один <h1> с главной темой страницы: так понятнее и людям, и поисковикам. Браузеры не меняют уровни заголовков внутри section автоматически.
Логотип в шапке — это h1?
Обычно нет: логотип повторяется на каждой странице и не описывает её тему. h1 — заголовок конкретной страницы: название статьи, товара, раздела. Исключение иногда делают для главной страницы.
- Заголовки задают иерархию документа, как оглавление.
- Один
h1с темой страницы. - Уровни идут по порядку, без пропусков вниз.
- Размер меняется в CSS, а не выбором другого уровня.
- Упомянуты скринридеры: навигация по заголовкам.
Почитать на сайте
Какие атрибуты стоит указывать у <img> и зачем? Зачем писать width и height, если размер всё равно задаётся в CSS?
Интервьюер может уточнить
- Что писать в
altу фотографии товара? - Когда картинку лучше сделать фоном в CSS?
Разбор
src — адрес изображения. alt — текст, который прочитает скринридер и который покажется, если картинка не загрузилась. Он описывает смысл изображения в контексте, а у чисто декоративной картинки должен быть пустым: alt="". Отсутствие alt совсем — ошибка: скринридер может прочитать имя файла.
width и height в HTML не мешают CSS: правило img { max-width: 100%; height: auto; } по-прежнему делает картинку резиновой. Зато браузер ещё до загрузки файла вычисляет пропорции и резервирует место. Без них текст сначала подтягивается вверх, а потом резко сдвигается вниз, когда картинка загрузилась. Это раздражает и ухудшает метрику CLS.
loading="lazy" откладывает загрузку картинок, до которых пользователь ещё не долистал. Для главной картинки первого экрана его не ставят, чтобы она появилась как можно раньше.
Уточняющие вопросы
Что писать в alt у фотографии товара?
То, что важно в контексте: «Красный рюкзак Urban 20 л, вид спереди». Не нужно начинать с «Картинка…» — скринридер и так сообщит, что это изображение.
Когда картинку лучше сделать фоном в CSS?
Когда она чисто декоративная и не несёт смысла: узор, фоновая текстура. Если изображение — часть содержимого (фото товара, схема), его вставляют через <img> с alt.
srcиaltназваны обязательными, объяснено назначениеalt.- У декоративной картинки
altпустой. widthиheightрезервируют место и задают пропорции до загрузки.- Упомянут сдвиг содержимого при загрузке (CLS).
- Назван
loading="lazy"и когда его не ставить.
Почитать на сайте
Зачем у поля формы нужен <label>? Как связать подпись с полем и почему placeholder её не заменяет?
Интервьюер может уточнить
- Как подписать группу переключателей
radio? - Что делать, если по дизайну подписи нет — только иконка лупы в поиске?
Разбор
Два способа связи: <label for="email">Почта</label> <input id="email"> или поле внутри подписи: <label>Почта <input></label>. Значение for должно совпадать с id поля, а id — быть уникальным на странице.
Что это даёт: скринридер объявляет «Почта, поле ввода»; по подписи можно кликнуть, и это особенно важно для маленьких чекбоксов и переключателей на телефоне; голосовое управление находит поле по названию.
placeholder — пример или подсказка внутри поля. Он исчезает, как только пользователь начинает печатать, и человек забывает, что нужно было ввести. Обычно он ещё и бледный и плохо читается. Поэтому подпись должна быть постоянной и видимой, а placeholder — необязательным дополнением.
Уточняющие вопросы
Как подписать группу переключателей radio?
У каждого переключателя свой <label>, а у всей группы — общий вопрос в <fieldset> с <legend>. Тогда скринридер прочитает и вопрос, и вариант.
Что делать, если по дизайну подписи нет — только иконка лупы в поиске?
Дать полю невидимое имя: label, скрытый визуально классом sr-only, или атрибут aria-label="Поиск". Но видимая подпись удобнее для всех, поэтому её отсутствие стоит обсудить с дизайнером.
labelдаёт полю доступное имя для скринридера.- Клик по подписи переводит фокус или отмечает чекбокс.
- Названы оба способа связи:
for + idи вложенность. - Placeholder исчезает при вводе и не заменяет подпись.
Почитать на сайте
Чем отличается отправка формы методом GET от POST? Какой выбрать для поиска по сайту, а какой — для регистрации?
Интервьюер может уточнить
- Почему после отправки POST-формы браузер при обновлении страницы спрашивает «Отправить данные повторно?»
- Какие поля не попадут в отправку?
Разбор
Форма отправляет пары «name=значение» по адресу из атрибута action методом из атрибута method. По умолчанию метод GET.
GET добавляет данные в URL: /search?q=кот&page=2. Плюсы: результат можно добавить в закладки, поделиться ссылкой, вернуться кнопкой «Назад». Минусы: данные видны в адресной строке, истории браузера и журналах сервера, а длина адреса ограничена. GET по смыслу только получает данные и не должен ничего менять.
POST передаёт данные в теле запроса. Он подходит для создания и изменения: регистрация, комментарий, заказ, загрузка файлов (вместе с enctype="multipart/form-data"). Важно: POST не шифрует данные — защищает их только HTTPS. Пароль в POST по HTTP так же уязвим, как в GET, но хотя бы не остаётся в адресе и истории.
Уточняющие вопросы
Почему после отправки POST-формы браузер при обновлении страницы спрашивает «Отправить данные повторно?»
Обновление повторяет последний запрос, а повтор POST может создать второй заказ или комментарий. Поэтому сервер после успешного POST обычно делает перенаправление на страницу результата, которая открывается обычным GET.
Какие поля не попадут в отправку?
Поля без атрибута name, отключённые disabled и неотмеченные чекбоксы и переключатели.
- GET передаёт данные в URL, POST — в теле запроса.
- GET-адрес можно сохранить и передать, он остаётся в истории.
- POST — для действий, которые меняют данные на сервере.
- Для поиска выбран GET, для регистрации — POST.
- Упомянуто, что защиту данных даёт HTTPS, а не метод.
Почитать на сайте
Зачем указывать у поля type="email", type="tel" или type="number", если можно везде оставить type="text"?
Интервьюер может уточнить
- Если у поля
type="email"иrequired, нужна ли проверка на сервере? - Что делает
inputmode?
Разбор
Что даёт правильный тип:
- Клавиатура на телефоне: для
email— с символом @, дляtel— цифровая, дляurl— с «.com» и косой чертой. - Проверка:
emailиurlпроверяют формат при отправке,number— что введено число в пределахminиmax. - Элементы управления:
dateоткрывает календарь,range— ползунок,color— палитру. - Автозаполнение: вместе с атрибутом
autocompleteбраузер подставляет сохранённые почту, телефон и адрес.
type="number" подходит для количества, но не для номера карты, телефона или индекса: у них могут быть ведущие нули, а стрелки увеличения и колесо мыши там только мешают. Для таких полей берут type="text" с inputmode="numeric" — цифровая клавиатура без поведения числа.
Уточняющие вопросы
Если у поля type="email" и required, нужна ли проверка на сервере?
Да. Браузерную проверку легко обойти: отключить в инструментах разработчика или отправить запрос в обход формы. Она помогает пользователю, а защищает данные только проверка на сервере.
Что делает inputmode?
Подсказывает, какую экранную клавиатуру показать, но не меняет тип и проверку поля. Например, inputmode="numeric" у текстового поля для кода из SMS.
- Тип меняет экранную клавиатуру на телефоне.
- Тип включает встроенную проверку формата.
- Названы специальные элементы управления: дата, ползунок и т. п.
- number не подходит для телефона и номеров с ведущими нулями.
- Проверка на сервере всё равно нужна.
Почитать на сайте
Чем атрибут id отличается от class? Для чего нужны атрибуты data-*?
Интервьюер может уточнить
- Почему не стоит стилизовать по
id? - Можно ли хранить в data-атрибутах секретные данные?
Разбор
id должен быть уникальным в документе. Его используют там, где нужна ссылка на конкретный элемент: якорь <a href="#contacts">, связь <label for>, атрибуты aria-describedby, быстрый поиск getElementById. Дубли id ломают эти связи: браузер найдёт только первый элемент.
class — повторяемая метка: class="button button--primary". Стили обычно пишут по классам: у селектора по id слишком высокая специфичность, и его потом трудно переопределить.
data-* — пользовательские атрибуты: <button data-product-id="42">. В JavaScript они доступны через element.dataset.productId, в CSS — через селектор [data-state="open"]. Так данные для скрипта не смешиваются с классами для оформления.
Уточняющие вопросы
Почему не стоит стилизовать по id?
У селектора с id специфичность намного выше, чем у класса. Чтобы потом переопределить такой стиль, придётся писать ещё более сильный селектор или !important. Кроме того, стиль по id нельзя переиспользовать для второго такого же элемента.
Можно ли хранить в data-атрибутах секретные данные?
Нет: всё, что есть в разметке, видно любому пользователю в коде страницы. Там хранят только то, что нужно интерфейсу.
idуникален на странице,classможет повторяться.- У элемента может быть несколько классов.
- Названы применения
id: якорь,labelfor, ARIA. data-*хранят данные для скриптов, доступны черезdataset.
Почитать на сайте
Два внешних классических скрипта зависят друг от друга и обращаются к DOM. Чем различаются обычный script, async и defer? Какой вариант выбрать при размещении в head?
Интервьюер может уточнить
- Когда уместен
async? - Нужен ли
deferуtype="module"?
Разбор
defer у обоих внешних классических скриптов. Они загружаются параллельно разбору HTML, затем выполняются в порядке документа. async удобен независимому скрипту, но не цепочке зависимостей. Обычный script блокирует дальнейший разбор на время загрузки и выполнения. У модульных скриптов другая модель зависимостей; без async они и так отложены, defer для них не нужен.Уточняющие вопросы
Когда уместен async?
Для независимых скриптов, которым не важен порядок и готовый DOM: счётчики аналитики, реклама.
Нужен ли defer у type="module"?
Нет: модульные скрипты и так откладываются до разбора документа и выполняются по порядку.
- Обычный скрипт без атрибутов останавливает разбор HTML.
asyncвыполняется по готовности, порядок зависимых файлов не гарантирован.deferсохраняет порядок, выполняется после разбора HTML доDOMContentLoaded.
Почитать на сайте
Спецификация: WHATWG: загрузка скриптов
Карточка занимает половину окна на десктопе и всю ширину на телефоне. Как помочь браузеру выбрать подходящий файл изображения? Когда нужен picture?
Интервьюер может уточнить
- Чем дескриптор w отличается от x?
- Как отдать WebP с запасным JPEG?
Разбор
srcset и согласованный с макетом sizes, например «(max-width: 600px) 100vw, 50vw», если отступов нет. Браузер выбирает ресурс с учётом размера и плотности пикселей. Саму ширину задаёт CSS. picture нужен, например, для крупного плана на телефоне вместо общего кадра. Атрибуты width и height помогают заранее зарезервировать место; это отдельная задача от выбора файла.Уточняющие вопросы
Чем дескриптор w отличается от x?
2x указывает плотность пикселей и подходит для картинок фиксированного размера, например логотипа. 800w указывает ширину файла и работает вместе с sizes для резиновых картинок.
Как отдать WebP с запасным JPEG?
Через <picture>: <source type="image/webp" srcset="…">, а внутри обычный <img> с JPEG. Браузер выберет первый поддерживаемый источник.
srcsetс w описывает реальные ширины файлов;sizesописывает ожидаемый размер изображения в макете.- Выбор учитывает плотность пикселей;
sizesне задаёт CSS-ширину. pictureприменим для другого кадрирования или формата;widthиheightрезервируют пропорции.
Почитать на сайте
Спецификация: WHATWG: адаптивные изображения
Главная страница медленно показывает первый экран: крупная картинка-обложка появляется последней. Какими средствами HTML можно ускорить её появление и не замедлить остальное?
Интервьюер может уточнить
- Чем
preloadотличается отprefetch? - Что такое
preconnect?
Разбор
Браузер узнаёт о картинке, только когда доходит до неё в HTML или в CSS, и сам решает, что важнее. Ему можно помочь:
- у обложки не должно быть
loading="lazy": ленивая загрузка откладывает её до раскладки; fetchpriority="high"поднимает приоритет загрузки картинки первого экрана;- если обложка задана фоном в CSS, браузер найдёт её поздно.
<link rel="preload" as="image" href="…">в<head>начнёт загрузку сразу; srcsetиsizesдают телефону файл поменьше;widthиheightрезервируют место, и страница не прыгает.
Чтобы не замедлить остальное: картинки ниже первого экрана — loading="lazy", скрипты — defer или type="module", шрифты — font-display: swap. preload нельзя ставить на всё подряд: всё «срочное» означает, что срочного нет.
Проверять результат стоит в Lighthouse или во вкладке Performance по метрике LCP — времени появления самого крупного элемента первого экрана.
Уточняющие вопросы
Чем preload отличается от prefetch?
preload — ресурс нужен для текущей страницы, загрузить сейчас. prefetch — пригодится на следующей странице, загрузить в свободное время.
Что такое preconnect?
Заранее установить соединение с другим доменом, например со шрифтовым сервисом или CDN: DNS, TCP и TLS пройдут, пока браузер ещё разбирает страницу.
- Обложку первого экрана не загружают лениво.
- Названы
fetchpriority="high"илиpreload. - Картинки ниже первого экрана —
loading="lazy", скрипты —defer. - Результат проверяют по метрике LCP.
Почитать на сайте
Спецификация: HTML: атрибут fetchpriority
Нужно окно подтверждения удаления и выпадающее меню профиля. Когда хватит нативного <dialog>, когда — атрибута popover, а когда понадобится свой код?
Интервьюер может уточнить
- Чем
dialog.show()отличается отshowModal()? - Зачем верхний слой, если есть
z-index?
Разбор
<dialog> с showModal() выводится в верхний слой, делает остальную страницу недоступной (inert), переводит фокус внутрь и закрывается по Escape с событием close. Для подтверждения удаления это почти всё, что нужно; остаётся выбрать начальный фокус и вернуть фокус на кнопку после закрытия.
popover="auto" — атрибут для неблокирующих всплывающих элементов: меню, подсказок, панелей. Кнопка с popovertarget открывает его без JavaScript, браузер выводит его поверх страницы, закрывает по щелчку мимо и по Escape и закрывает другие auto-панели, кроме связанных вложенных. У popover="manual" автоматического закрытия по щелчку мимо и Escape нет.
Свой код всё ещё нужен, чтобы:
- привязать панель к кнопке и не дать ей выйти за край экрана (anchor positioning поддерживается ещё не везде);
- сделать навигацию стрелками по пунктам меню;
- плавно анимировать появление и исчезновение;
- поддержать браузеры без этих возможностей, если это требуется.
Уточняющие вопросы
Чем dialog.show() отличается от showModal()?
show() открывает неблокирующее окно на месте в документе: фон остаётся доступным, Escape по умолчанию не закрывает. showModal() — модальное окно в верхнем слое.
Зачем верхний слой, если есть z-index?
Элемент в верхнем слое выше всего остального независимо от контекстов наложения. Окно с z-index: 9999 внутри родителя с transform может оказаться под соседней секцией, а верхний слой этой проблемы не имеет.
- Подтверждение — модальный
<dialog>черезshowModal(). - Меню —
popover, без блокировки страницы. - Названо, что дают элементы: верхний слой, Escape, закрытие по щелчку мимо или блокировка фона.
- Назван хотя бы один случай, когда нужен свой код.
Почитать на сайте
Спецификация: HTML: атрибут popover
Что такое веб-компоненты? Из каких частей они состоят и чем пользовательский элемент <user-card> отличается от компонента React?
Интервьюер может уточнить
- Как стилизовать компонент снаружи, если Shadow DOM его изолирует?
- Что с доступностью и формами у пользовательских элементов?
Разбор
Три части:
- Custom Elements — класс, наследник
HTMLElement, регистрируется черезcustomElements.define('user-card', UserCard). Имя обязательно с дефисом. МетодыconnectedCallback,disconnectedCallbackиattributeChangedCallbackвызываются при подключении, отключении и смене атрибутов из спискаobservedAttributes; - Shadow DOM — отдельное поддерево с границей инкапсуляции, а не защита секретных данных: стили внутри не утекают наружу, а внешние селекторы не ломают компонент;
<template>и<slot>— заготовка разметки и места, куда попадёт содержимое, переданное снаружи.
Отличие от React: React описывает интерфейс как функцию от состояния и сам обновляет DOM. Веб-компонент — это просто элемент DOM: обновлять его при изменении данных нужно вручную или через библиотеку вроде Lit. Зато он не зависит от фреймворка и подходит для дизайн-систем, которые используют разные команды.
Уточняющие вопросы
Как стилизовать компонент снаружи, если Shadow DOM его изолирует?
Через CSS-переменные — они проходят внутрь, через ::part() для явно открытых частей и через стили содержимого слотов.
Что с доступностью и формами у пользовательских элементов?
Роль и поведение элемент не получает сам. Нужно взять нативные элементы внутри или настроить роль и состояние; для участия в форме есть ElementInternals.
- Названы пользовательские элементы и
customElements.define. - Назван Shadow DOM и изоляция стилей.
- Названы
<template>или<slot>. - Отличие от React: нет встроенного обновления по состоянию, зато нет зависимости от фреймворка.
Спецификация: HTML: пользовательские элементы
Сайт выходит на русском, английском и арабском. Что нужно учесть в разметке, чтобы браузер, поисковики и скринридеры правильно работали с каждой версией?
Интервьюер может уточнить
- Можно ли определять язык по IP и сразу перенаправлять?
- Почему не стоит зашивать текст в картинки?
Разбор
Язык. <html lang="ru"> говорит скринридеру, каким голосом читать, браузеру — какие правила переносов и кавычек применять, поисковику — для кого страница. Вставку на другом языке размечают отдельно: <span lang="en">.
Направление. Для арабского и иврита — dir="rtl": текст и порядок элементов идут справа налево. Для текста неизвестного направления, например имени пользователя, — dir="auto" или <bdi>.
Связь версий. В <head> каждой версии — ссылки на остальные: <link rel="alternate" hreflang="en" href="…">. Поисковик покажет пользователю его версию.
Вёрстка. Логические свойства CSS (margin-inline-start, padding-inline, inset-inline-end) сами отражаются для RTL. Немецкий и русский переводы бывают на треть длиннее английского — кнопки фиксированной ширины ломаются. Даты, числа и валюты форматируют через Intl, а не склеивают строки.
Уточняющие вопросы
Можно ли определять язык по IP и сразу перенаправлять?
Лучше не делать этого принудительно: человек может быть в поездке или читать на другом языке. Предложить версию и запомнить выбор — удобнее.
Почему не стоит зашивать текст в картинки?
Его нельзя перевести, увеличить, найти поиском и прочитать скринридером без отдельного альтернативного текста на каждом языке.
- Назван
langу страницы и у вставок на другом языке. - Назван
dir="rtl"для арабского. - Названа связь версий через
hreflang. - Названы логические свойства CSS или запас места под длинный перевод.
Почитать на сайте
Спецификация: W3C: язык в HTML
Файл blog/post.html ссылается на index.html в корне сайта и на картинку blog/img/cat.jpg. Как записать оба пути? Что учесть, если ссылка открывается в новой вкладке?
Интервьюер может уточнить
- Почему путь
/img/cat.jpgне работает, если открыть страницу двойным щелчком по файлу? - Чем ссылка отличается от кнопки?
Разбор
Путь бывает трёх видов:
- абсолютный адрес с протоколом:
https://example.com/index.html— для других сайтов; - от корня сайта, начинается с
/:/blog/img/cat.jpg— одинаково работает с любой страницы, но только на сервере, а не при открытии файла с диска; - относительно документа:
img/cat.jpg— в папке рядом,../index.html— на уровень выше.
Для blog/post.html: <a href="../index.html"> и <img src="img/cat.jpg" alt="…">.
target="_blank" открывает новую вкладку. Это стоит делать редко и предупреждать текстом или значком: человек с экранным диктором может не понять, почему перестала работать кнопка «Назад». Современные браузеры для _blank сами ведут себя как с rel="noopener", но для старых его указывают явно: иначе открытая страница получит доступ к window.opener.
Уточняющие вопросы
Почему путь /img/cat.jpg не работает, если открыть страницу двойным щелчком по файлу?
Без сервера корнем считается корень диска, а не папка сайта. Нужен локальный сервер или относительные пути.
Чем ссылка отличается от кнопки?
Ссылка ведёт на другой адрес, кнопка выполняет действие на странице. Удаление, отправка, открытие окна — кнопка.
- Записан путь на уровень выше:
../index.html. - Записан путь в дочернюю папку:
img/cat.jpg. - Объяснено отличие пути от корня
/от относительного. - Названы
target="_blank"и предупреждение пользователя; упомянуто, чтоnoopenerбраузеры включают сами.
Почитать на сайте
Спецификация: HTML: ссылки
Когда для данных нужна таблица, а когда нет? Как разметить таблицу расписания, чтобы скринридер называл, к какому дню и времени относится ячейка?
Интервьюер может уточнить
- Что делать с широкой таблицей на телефоне?
- Зачем
colspanиrowspan?
Разбор
Если данные сравнивают по двум осям — расписание, цены по тарифам, результаты — это таблица. Колонки сайта, карточки товаров и формы раскладывают через CSS Grid и Flexbox: таблица для них заставляет скринридер объявлять «строка 2, столбец 3» там, где смысла нет.
<table>
<caption>Расписание занятий</caption>
<thead>
<tr><th scope="col">Время</th><th scope="col">Пн</th><th scope="col">Ср</th></tr>
</thead>
<tbody>
<tr><th scope="row">10:00</th><td>HTML</td><td>CSS</td></tr>
</tbody>
</table><caption> даёт таблице имя, <th> отличает заголовок от данных, а scope говорит, к чему он относится. На ячейке «CSS» скринридер прочитает «10:00, Ср». Для сложных таблиц с объединёнными заголовками есть атрибут headers.
Уточняющие вопросы
Что делать с широкой таблицей на телефоне?
Обернуть её в блок с overflow-x: auto, чтобы прокручивалась таблица, а не вся страница. Иногда данные лучше показать карточками, но тогда это уже не таблица.
Зачем colspan и rowspan?
Объединить ячейку на несколько столбцов или строк, например «Перерыв» на все дни. С ними важно проверить, что заголовки по-прежнему правильно связаны с ячейками.
- Таблица — для данных по строкам и столбцам, не для раскладки.
- Назван
<caption>. - Названы
<th>иscope. - Объяснено, как скринридер связывает ячейку с заголовками.
Почитать на сайте
Спецификация: HTML: таблицы
Чем <strong> отличается от <b>, а <em> — от <i>? Если выглядят одинаково, какая разница?
Интервьюер может уточнить
- Читают ли скринридеры
<strong>иначе? - Можно ли вложить
<strong>в<strong>?
Разбор
По умолчанию <strong> и <b> жирные, <em> и <i> курсивные, но назначение у них разное:
<strong>— важное: «Не выключайте компьютер во время обновления»;<em>— ударение: «Я просил чай» и «Я просил чай» — разный смысл;<b>— выделить без важности: ключевые слова в обзоре, имя товара в описании;<i>— другой голос текста: термин, мысль героя, латинское название, слово на другом языке (вместе сlang).
Если нужно только оформление — например, жирная подпись в карточке, — правильнее задать font-weight в CSS, а не брать тег ради вида.
Уточняющие вопросы
Читают ли скринридеры <strong> иначе?
Большинство по умолчанию не меняет интонацию. Но смысл важен для поиска, переводчиков, режимов чтения и будущих инструментов, поэтому тег выбирают по смыслу.
Можно ли вложить <strong> в <strong>?
Да, это повышает степень важности. На практике так делают редко.
<strong>— важность,<em>— ударение.<b>и<i>— выделение без важности, приведены примеры.- Вид задаёт CSS, тег выбирают по смыслу.
Почитать на сайте
Спецификация: HTML: семантика текста
В форме оформления заказа люди путаются в полях и бросают покупку на телефоне. Какие атрибуты HTML помогут браузеру заполнить форму за пользователя и показать удобную клавиатуру?
Интервьюер может уточнить
- Почему
autocomplete="off"на поле пароля плохая идея? - Где пригодится
autocomplete="off"?
Разбор
Автозаполнение. Браузер хранит имя, адрес, телефон и карты, но подставит их, только если понимает поле. autocomplete="given-name", "email", "tel", "street-address", "postal-code", "cc-number", "cc-exp" — стандартные значения. Для адреса доставки и оплаты разные — префиксы shipping и billing. Для пароля — "new-password" при регистрации и "current-password" при входе: так менеджер паролей предложит сгенерировать или подставить нужный.
Клавиатура. type="email" и type="tel" показывают нужную раскладку. Для кода из СМС — inputmode="numeric" и autocomplete="one-time-code": телефон предложит код из сообщения. enterkeyhint="next" или "done" меняет подпись клавиши.
Чего избегать. Разбивать телефон или карту на несколько полей, запрещать вставку, ставить autocomplete="off" на всё подряд и использовать type="number" для индекса и номера карты — у числового поля есть стрелки, и ведущие нули теряются.
Уточняющие вопросы
Почему autocomplete="off" на поле пароля плохая идея?
Менеджеры паролей часто его игнорируют, а если нет — человек вынужден придумывать простой пароль, который помнит. Это хуже для безопасности.
Где пригодится autocomplete="off"?
В полях, где подсказка мешает: поиск по таблице, ввод чужих данных оператором, одноразовые значения.
- Назван
autocompleteс конкретными значениями. - Названы
typeиinputmodeдля клавиатуры. - Назван
one-time-codeили паролиnew-password/current-password. - Названа хотя бы одна ошибка: разбиение полей, запрет вставки,
type="number".
Почитать на сайте
Спецификация: HTML: автозаполнение
Страница товара плохо выглядит в поиске и в мессенджерах: заголовок обрезан, описания нет, картинка случайная. Что поправить в разметке?
Интервьюер может уточнить
- Что делает
<meta name="robots" content="noindex">? - Помогает ли
<meta name="keywords">?
Разбор
<title>— главный текст ссылки в выдаче: уникальный, с главным словом в начале, примерно до 60 символов, например «Рюкзак городской 20 л — Магазин».<meta name="description">— описание под ссылкой. Поисковик может заменить его своим отрывком, но хорошее описание часто показывают как есть.<link rel="canonical">— основной адрес, если страница открывается с фильтрами, метками рекламы или по разным путям. Иначе поисковик видит дубли.- Open Graph —
<meta property="og:title">,og:description,og:imageс картинкой нужного размера: мессенджеры и соцсети строят по ним карточку. - JSON-LD —
<script type="application/ld+json">с описанием по словарю schema.org (Product, цена, наличие): поисковик может показать их в сниппете.
Разметка не заменяет содержимое: важный текст должен быть на странице, а не только в метатегах, и страница должна быстро загружаться и работать без ошибок.
Уточняющие вопросы
Что делает <meta name="robots" content="noindex">?
Просит поисковики не добавлять страницу в выдачу. Ставят на служебные страницы, результаты внутреннего поиска, тестовые копии сайта.
Помогает ли <meta name="keywords">?
Нет, крупные поисковики давно его не учитывают.
- Названы уникальный
<title>иdescription. - Назван
canonicalдля дублей адресов. - Названы теги Open Graph для превью.
- Названы структурированные данные JSON-LD.
Почитать на сайте
Спецификация: Протокол Open Graph
Нужно встроить на страницу видео с YouTube и карту. Как это сделать через <iframe> и какие атрибуты нужны, чтобы фрейм был понятным, безопасным и не тормозил загрузку?
Интервьюер может уточнить
- Можно ли ускорить страницу с несколькими видео ещё сильнее?
- Почему адрес
youtube-nocookie.com?
Разбор
<iframe
src="https://www.youtube-nocookie.com/embed/VIDEO_ID"
title="Урок: первая HTML-страница"
loading="lazy"
allow="fullscreen; picture-in-picture"
width="560" height="315">
</iframe>title— имя фрейма для скринридера: «фрейм, Урок: первая HTML-страница». Без него непонятно, что внутри;loading="lazy"— каждый фрейм загружает целую страницу со своими скриптами. Для фреймов ниже первого экрана это заметно ускоряет загрузку;allow— список разрешённых возможностей: полноэкранный режим, камера, геолокация. Лишнего не давать;sandbox— для недоверенного содержимого: запрещает скрипты, формы и переходы, пока их явно не разрешили;- адаптивность: в CSS
width: 100%; height: auto; aspect-ratio: 16 / 9;.
Не каждый сайт можно встроить: многие запрещают это заголовками, тогда во фрейме будет ошибка.
Уточняющие вопросы
Можно ли ускорить страницу с несколькими видео ещё сильнее?
Показывать превью-картинку с кнопкой воспроизведения и подставлять <iframe> только после щелчка.
Почему адрес youtube-nocookie.com?
В этом режиме YouTube не ставит отслеживающие cookie, пока пользователь не запустит видео.
- Названы
<iframe>иsrc. - Назван
titleдля доступности. - Назван
loading="lazy". - Названы
allow,sandboxили адаптивный размер.
Почитать на сайте
Спецификация: HTML: элемент iframe
Встроенные подсказки браузера при проверке формы не совпадают с дизайном, а нужно ещё проверить, что пароли совпадают. Как сохранить браузерную проверку, но показать свои сообщения?
Интервьюер может уточнить
- Когда показывать ошибку поля?
- Почему не убрать атрибуты и проверять всё в JavaScript?
Разбор
form.noValidate = true
form.addEventListener('submit', (event) => {
confirm.setCustomValidity(confirm.value === password.value ? '' : 'Пароли не совпадают')
if (!form.checkValidity()) {
event.preventDefault()
showErrors(form) // свой вывод сообщений
form.querySelector(':invalid').focus() // фокус к первой ошибке
}
})Что даёт API:
input.validity— флаги причин:valueMissing,typeMismatch,tooShort,patternMismatch,customError. По ним выбирают понятный текст: «Введите email в формате name@example.ru»;input.validationMessage— стандартный текст браузера, если своего нет;setCustomValidity(текст)помечает поле ошибочным, пустая строка снимает ошибку — её нужно сбрасывать при каждом вводе;- псевдоклассы
:invalidи:user-invalidдля оформления.
novalidate отключает только автоматическую остановку отправки и всплывающие подсказки — атрибуты и состояние полей продолжают работать. Сервер всё равно проверяет всё заново.
Уточняющие вопросы
Когда показывать ошибку поля?
При уходе из поля (blur) или при отправке, а исправленную ошибку убирать сразу при вводе.
Почему не убрать атрибуты и проверять всё в JavaScript?
Атрибуты дают правильную клавиатуру, состояние для скринридера и проверку, которая работает даже без скрипта.
- Атрибуты проверки остаются, подсказки отключает
novalidate. - Названы
validityилиcheckValidity(). - Своё правило через
setCustomValidityсо сбросом. - Ошибка выводится текстом рядом с полем, сервер проверяет заново.
Почитать на сайте
Спецификация: HTML: проверка ограничений
Нужно нарисовать интерактивную схему зала с 300 местами и график с 50 000 точек. Что выбрать для каждого: SVG или <canvas>, и почему?
Интервьюер может уточнить
- Как ускорить canvas, если тяжёлый рендер подвешивает страницу?
- Как сделать SVG-иконку доступной?
Разбор
SVG:
- каждая фигура — узел DOM: на неё можно повесить обработчик, задать
:hover, поставить фокус иaria-label; - чёткий при любом масштабе и на любом экране;
- при тысячах элементов DOM становится тяжёлым, и перерисовка замедляется.
<canvas>:
- одно полотно пикселей и API рисования (
getContext('2d')или WebGL); - хорошо держит десятки тысяч объектов и анимацию — рисуется только картинка;
- браузер не знает, где на полотне «место 12», — попадание мыши, фокус и состояние нужно считать самим;
- для экранов высокой плотности полотно масштабируют на
devicePixelRatio, иначе изображение размыто; - для скринридера нужна альтернатива: таблица данных или текстовое описание.
Схема зала — SVG: мест немного, каждое кликается и должно быть доступно с клавиатуры. График на 50 000 точек — canvas, а рядом — таблица или итоги текстом. Комбинировать тоже можно: данные на canvas, оси и подписи в SVG или HTML.
Уточняющие вопросы
Как ускорить canvas, если тяжёлый рендер подвешивает страницу?
Перерисовывать только изменившуюся часть, рисовать через requestAnimationFrame или вынести рисование в воркер через OffscreenCanvas.
Как сделать SVG-иконку доступной?
Декоративной — aria-hidden="true"; значимой — role="img" и aria-label или <title> внутри.
- SVG — элементы в DOM с событиями, CSS и доступностью.
- Canvas — растровое полотно, хорошо для большого числа объектов.
- Выбор обоснован для зала и графика.
- Названа доступность canvas: таблица или описание.
Почитать на сайте
Спецификация: HTML: элемент canvas
Нужен блок «Вопросы и ответы», где открыт только один ответ. Можно ли обойтись <details> и <summary>? Что они дают и где ограничения?
Интервьюер может уточнить
- Можно ли в
<summary>поставить заголовок? - Что будет со старыми браузерами без поддержки
name?
Разбор
<details name="faq" open>
<summary>Как оплатить заказ?</summary>
<p>Картой на сайте или при получении.</p>
</details>
<details name="faq">
<summary>Сколько идёт доставка?</summary>
<p>От одного до трёх дней.</p>
</details>Что даёт браузер:
<summary>фокусируется и раскрывается Enter и пробелом, скринридер объявляет «свёрнуто/развёрнуто»;- атрибут
open— состояние, событиеtoggle— изменение; - поиск по странице (Ctrl+F) находит текст в свёрнутом ответе и раскрывает его;
nameобъединяет блоки в группу «аккордеон» — открытие одного закрывает другие.
Ограничения: плавная анимация высоты требует дополнительных приёмов (::details-content, interpolate-size поддерживаются не везде); маркер оформляют через summary::marker или list-style: none со своим значком. Внутри <summary> не стоит размещать другие интерактивные элементы.
Свой виджет на <button aria-expanded> нужен, если требуется поведение, которого нет у <details>, например переход стрелками между заголовками.
Уточняющие вопросы
Можно ли в <summary> поставить заголовок?
Да, <summary><h3>…</h3></summary> — так вопросы попадут в навигацию по заголовкам. Но некоторые скринридеры читают такое сочетание по-разному, его стоит проверить.
Что будет со старыми браузерами без поддержки name?
Блоки будут открываться независимо. Это безопасный откат: содержимое остаётся доступным.
- Названы
<details>и<summary>и что они дают без JavaScript. - Назван атрибут
nameдля группы с одним открытым блоком. - Названо ограничение: анимация или оформление маркера.
Почитать на сайте
Спецификация: HTML: элемент details
Кнопка с disabled="false" всё равно недоступна. Почему и как включать её из JavaScript?
Интервьюер может уточнить
- Что произойдёт при
setAttribute("disabled", false)? - Как проверить текущее состояние флажка?
Разбор
disabled, checked и required — булевы атрибуты. Запись disabled="false" не означает ложь: атрибут присутствует, кнопка отключена. Используйте button.disabled = isBusy или button.toggleAttribute("disabled", isBusy).
Не переносите правило на все атрибуты: aria-expanded="false" — осмысленное состояние ARIA, а contenteditable имеет перечислимые значения.
Уточняющие вопросы
Что произойдёт при setAttribute("disabled", false)?
Значение преобразуется в строку, атрибут останется, кнопка будет отключена.
Как проверить текущее состояние флажка?
Через input.checked. Атрибут checked задаёт состояние по умолчанию, а пользователь меняет текущее свойство.
- Наличие
disabledозначает отключение независимо от строки. - Предложены свойство
disabledили удаление атрибута. - Булевы HTML-атрибуты отличены от строковых состояний ARIA.
Почитать на сайте
Спецификация: Первоисточник: html
Кнопка в панели вызывает form.submit(), но проверка обязательных полей и обработчик submit не работают. Как отправить форму программно с обычным поведением?
Интервьюер может уточнить
- Как различить «Сохранить» и «Опубликовать» в одной форме?
- Почему метод
form.submitиногда перестаёт быть функцией?
Разбор
Обработчик ставят на форму, а не только на клик кнопки: форму отправляют и клавишей Enter. requestSubmit(saveButton) учитывает ограничения и параметры кнопки, например formaction. Если проверка не пройдена, события submit не будет; novalidate и formnovalidate намеренно отключают проверку.
В обработчике доступна event.submitter. Вызов submit() нужен только при осознанном обходе этих механизмов.
Уточняющие вопросы
Как различить «Сохранить» и «Опубликовать» в одной форме?
По event.submitter и значениям отправляющих кнопок. Для FormData можно передать кнопку вторым аргументом: new FormData(form, button).
Почему метод form.submit иногда перестаёт быть функцией?
Поле с name="submit" или id="submit" может перекрыть одноимённый метод формы. Лучше избегать такого имени.
- Объяснён обход проверки и события при
submit(). - Предложен
requestSubmit()с учётом выбранной кнопки. - Обработчик отправки размещён на форме.
Почитать на сайте
Спецификация: Первоисточник: html
Форма принимает аватар с accept="image/*". Достаточно ли этого, чтобы сервер получил только безопасные изображения, и как отправить файл через fetch?
Интервьюер может уточнить
- Почему нельзя вручную поставить
Content-Type: multipart/form-data? - Как убрать временный адрес предпросмотра?
Разбор
У поля нужен name: <input type="file" name="avatar" accept="image/*">. Отправка: fetch("/avatar", { method: "POST", body: new FormData(form) }). Браузер сам создаёт multipart/form-data с разделителем boundary.
На клиенте проверяют наличие файла и размер для быстрой обратной связи. Имя, расширение и file.type не доказывают содержимое. Сервер ограничивает размер, проверяет допустимый формат и обрабатывает файл, не доверяя присланному имени.
Уточняющие вопросы
Почему нельзя вручную поставить Content-Type: multipart/form-data?
Без согласованного boundary сервер не сможет надёжно разобрать части. Заголовок формирует браузер вместе с телом.
Как убрать временный адрес предпросмотра?
После использования адреса из URL.createObjectURL(file) вызывают URL.revokeObjectURL(url), когда картинка больше не нужна.
acceptназван подсказкой, а не проверкой содержимого.- Выбрана отправка
FormDataбез ручного заголовка. - Разделены клиентская обратная связь и серверная проверка файла.
Почитать на сайте
Спецификация: Первоисточник: html
На странице есть загрузка файла и показатель «занято 7 из 10 ГБ». Для чего подойдут <progress> и <meter>?
Интервьюер может уточнить
- Когда нужен
progressбезvalue? - Как сообщить об окончании загрузки?
Разбор
Для загрузки: <progress id="upload" value="35" max="100">35%</progress> и связанный label. Если общий размер неизвестен, не придумывайте проценты: используйте progress без value.
Для диска: <meter id="disk" min="0" max="10" value="7">7 из 10 ГБ</meter>. Подпись объясняет единицы и смысл. Цвет шкалы не должен быть единственным способом понять состояние; текстовый результат остаётся рядом.
Уточняющие вопросы
Когда нужен progress без value?
Когда задача выполняется, но доля завершения неизвестна; value="0" означает известный нулевой прогресс.
Как сообщить об окончании загрузки?
Показать понятный результат и при необходимости обновить заранее существующую область role="status", не объявляя каждый процент.
- Выполнение задачи отличено от измерения в диапазоне.
- Объяснено отсутствие
valueпри неизвестном прогрессе. - Предусмотрены имя и текстовое состояние.
Почитать на сайте
Спецификация: Первоисточник: html
Какова ширина border box в обоих случаях? Что произойдёт с margin?
.box {
width: 200px;
padding: 20px;
border: 2px solid;
margin: 10px;
box-sizing: content-box; /* затем border-box */
}
Интервьюер может уточнить
- Почему часто пишут
*, *::before, *::after { box-sizing: border-box; }? - Что такое схлопывание
margin?
Разбор
content-box к 200px содержимого добавляются 40px padding и 4px границы: получается 244px. При border-box заданные 200px уже включают padding и границы, содержимому остаётся 156px. Margin не входит в border box ни в одном варианте. Это объясняет, почему поле шириной 100% может вылезать из контейнера при добавлении внутренних отступов.Уточняющие вопросы
Почему часто пишут *, *::before, *::after { box-sizing: border-box; }?
Так ширина включает внутренние отступы и рамку. Но если сумма padding и границ больше заданной ширины, содержимое сжимается только до нуля, а фактический блок будет шире: border-box не разрешает отрицательный размер содержимого.
Что такое схлопывание margin?
У соседних блоков в обычном потоке вертикальные отступы могут схлопываться. Для двух положительных берётся больший; для смешанных знаков складываются наибольший положительный и наиболее отрицательный. Между flex- и grid-элементами схлопывания нет.
- При
content-boxполучается244px, приborder-box200px. - Padding и
borderвключаются в заданную ширину только приborder-box. - Margin остаётся снаружи в обоих случаях.
Почитать на сайте
Спецификация: CSS Sizing: box-sizing
Два правила задают кнопке разный цвет. Как браузер решает, какое применить? Что такое специфичность и как она считается?
#form .button { color: green; }
.form .button.primary { color: blue; }
button { color: red; }
Интервьюер может уточнить
- Влияет ли на специфичность селектор *, комбинаторы > и +?
- Почему
!importantсчитают плохой практикой? - Что сильнее:
:where()или:is()?
Разбор
Сначала каскад учитывает происхождение, важность и слои. Здесь сравниваем обычные авторские правила в одном слое, без анимаций, переходов и @scope.
Специфичность записывают как (a, b, c): a — число id, b — число классов, атрибутов и псевдоклассов, c — число тегов и псевдоэлементов. Сравнение идёт слева направо, как у разрядов числа.
#form .button— (1, 1, 0);.form .button.primary— (0, 3, 0);button— (0, 0, 1).
Побеждает первое правило: один id перевешивает любое количество классов. Если специфичность одинаковая, применяется правило, которое стоит позже в CSS. Inline-стиль в атрибуте style сильнее любых селекторов, а !important перекрывает обычные объявления.
На практике стараются держать специфичность низкой и ровной: писать стили по одному классу. Тогда порядок правил предсказуем, и не приходится «перебивать» стили ещё более длинными селекторами или !important.
Уточняющие вопросы
Влияет ли на специфичность селектор *, комбинаторы > и +?
Нет: универсальный селектор * и комбинаторы (пробел, >, +, ~) специфичность не добавляют. Учитываются только id, классы, атрибуты, псевдоклассы, теги и псевдоэлементы.
Почему !important считают плохой практикой?
!important участвует в каскаде, а не выключает его. Среди важных объявлений тоже учитываются происхождение и слои (их порядок обратный), затем специфичность. Частое применение затрудняет переопределения; сначала стоит разобраться в структуре стилей.
Что сильнее: :where() или :is()?
:is() берёт специфичность самого сильного селектора из списка, а :where() всегда даёт нулевую специфичность. Поэтому :where() удобен для базовых стилей, которые легко переопределить.
- Специфичность считается по группам: id, классы/атрибуты/псевдоклассы, теги.
- Сравнение идёт по группам слева направо, один id сильнее любого числа классов.
- При равной специфичности побеждает правило, написанное позже.
- Упомянуты inline-стили и
!important. - Для примера назван правильный результат — зелёный.
Почитать на сайте
Почему color, заданный у <body>, применяется ко всему тексту, а border — нет? Что делают значения inherit и initial?
Интервьюер может уточнить
- Почему ссылки не становятся цвета родителя?
- Что сильнее: унаследованное значение или любое правило для элемента?
Разбор
Наследование избавляет от повторения: шрифт и цвет текста достаточно задать у body, и все потомки их получат. Свойства оформления блока не наследуются, иначе рамка у body появилась бы вокруг каждого абзаца и ссылки.
Управлять этим можно ключевыми словами, они работают для любого свойства:
inherit— взять значение родителя, даже если свойство не наследуется:border: inherit;initial— начальное значение свойства по спецификации, напримерdisplay: initialдаёт inline, а не block;unset— для наследуемых свойств работает какinherit, для остальных какinitial.
Частая практика: button, input { font: inherit; }. У полей формы браузер задаёт свой шрифт, и без этого кнопки выглядят иначе, чем текст страницы.
Уточняющие вопросы
Почему ссылки не становятся цвета родителя?
У ссылок есть собственный цвет в стилях браузера, и правило для тега a сильнее унаследованного значения: наследование срабатывает, только если у элемента нет своего правила. Сделать ссылку цвета текста можно так: a { color: inherit; }.
Что сильнее: унаследованное значение или любое правило для элемента?
Любое правило, которое прямо подходит к элементу, даже * { color: black; }. Наследованное значение используется только тогда, когда своих правил нет.
- Наследуются в основном свойства текста.
- Свойства блока (
border,margin,padding,background) не наследуются. inheritберёт значение родителя.initialвозвращает начальное значение по спецификации.- Прямое правило для элемента сильнее унаследованного значения.
Почитать на сайте
Чем отличаются способы скрыть элемент: display: none, visibility: hidden и opacity: 0? Как скрыть текст визуально, но оставить для скринридера?
Интервьюер может уточнить
- В чём опасность
opacity: 0для скрытого меню? - Почему не скрыть текст через
font-size: 0или отрицательныйtext-indent?
Разбор
| Место | Клик и фокус | Скринридер | |
|---|---|---|---|
display: none | нет | нет | не читает |
visibility: hidden | есть | нет | не читает |
opacity: 0 | есть | да | читает |
Ещё есть атрибут hidden в HTML — то же, что display: none. Opacity и visibility можно плавно анимировать через transition, а display до недавнего времени — нет. Поэтому выпадающие меню часто прячут через visibility с opacity.
Для подписи, которую видит только скринридер (например, у кнопки-иконки), используют класс, который сжимает элемент до 1px и обрезает его:
.visually-hidden {
position: absolute;
width: 1px; height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}Уточняющие вопросы
В чём опасность opacity: 0 для скрытого меню?
Прозрачные ссылки остаются на месте: их можно случайно нажать и до них доходит фокус клавиатуры. Пользователь нажимает Tab и «теряет» фокус в невидимом меню. Поэтому вместе с opacity задают visibility: hidden.
Почему не скрыть текст через font-size: 0 или отрицательный text-indent?
Такие приёмы ненадёжны: часть скринридеров пропускает текст нулевого размера, а элемент с огромным отрицательным отступом может выпасть за пределы экрана и вызвать прокрутку. Класс visually-hidden — проверенный вариант.
display: noneубирает элемент из раскладки.visibility: hiddenоставляет место.opacity: 0оставляет элемент кликабельным и доступным с клавиатуры.- Названо влияние на скринридеры.
- Назван приём
visually-hiddenдля текста только для скринридера.
Почитать на сайте
Какие значения есть у position? Как сделать, чтобы значок «Новинка» был в правом верхнем углу карточки товара?
Интервьюер может уточнить
- Почему
position: stickyможет не работать? - Как работает
z-index?
Разбор
static— по умолчанию, свойстваtop/leftне работают.relative— элемент остаётся в потоке, место за ним сохраняется, аtop/leftсдвигают его визуально. Чаще всегоrelativeнужен как точка отсчёта для вложенных absolute-элементов.absolute— элемент вынимается из потока: соседи ведут себя так, будто его нет. Координаты отсчитываются от ближайшего позиционированного предка, а если такого нет — от начального блока страницы.fixed— обычно координаты от окна. Но предок, например сtransform, может стать содержащим блоком: тогда элемент привязан к нему. Шапки, кнопка «Наверх».sticky— ведёт себя как обычный, пока родитель на экране, а при прокрутке прилипает на заданном top. Заголовки таблиц и боковые меню.
.card { position: relative; }
.card__badge { position: absolute; top: 8px; right: 8px; }Без position: relative у карточки значок улетит в угол всей страницы или другого позиционированного предка.
Уточняющие вопросы
Почему position: sticky может не работать?
Частые причины: не задан top (или другое смещение), у одного из предков стоит overflow: hidden или auto, родитель по высоте не больше самого элемента — тогда липнуть некуда.
Как работает z-index?
z-index задаёт порядок наложения у позиционированных элементов, а также у flex- и grid-элементов. Но сравнивается он только внутри одного контекста наложения: элемент с z-index: 9999 не поднимется выше, если его родитель находится под соседним блоком.
- Названы значения
static,relative,absolute,fixed,sticky. absoluteотсчитывается от ближайшего позиционированного предка.absoluteвынимает элемент из потока.- Для значка: родитель
relative, значокabsoluteс top и right. fixed— от окна,sticky— прилипает при прокрутке.
Почитать на сайте
Как отцентрировать блок по горизонтали и по вертикали внутри родителя? Назовите несколько способов.
Интервьюер может уточнить
- Почему
margin: 0 autoне центрирует по вертикали? - Чем
justify-contentотличается отalign-itemsвоflex?
Разбор
/* Flexbox */
.parent { display: flex; justify-content: center; align-items: center; }
/* Grid */
.parent { display: grid; place-items: center; }
/* Только по горизонтали, у блока есть ширина */
.child { width: 600px; margin: 0 auto; }Важно, что по вертикали центрировать можно только внутри родителя, у которого есть высота. Если родитель по высоте равен содержимому, центрировать некуда. Для экрана целиком задают min-height: 100vh или 100dvh.
text-align: center центрирует строчное содержимое: текст, картинки, inline-block, но не сам блок. Устаревший приём для блоков неизвестного размера — position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%). Он работает, но вынимает элемент из потока, поэтому сегодня его используют только для наложений поверх содержимого.
Уточняющие вопросы
Почему margin: 0 auto не центрирует по вертикали?
В обычном блочном потоке auto у вертикальных отступов вычисляется как 0. Внутри flex- и grid-контейнера margin: auto работает в обе стороны и центрирует элемент.
Чем justify-content отличается от align-items во flex?
justify-content выравнивает элементы вдоль главной оси (по умолчанию — горизонталь), align-items — вдоль поперечной. При flex-direction: column оси меняются местами.
- Назван способ через
flex:justify-contentиalign-items. - Назван способ через
grid:place-items. margin: 0 autoдля горизонтального центрирования блока с шириной.- Упомянуто, что для вертикали у родителя нужна высота.
Почитать на сайте
Когда для раскладки лучше выбрать Flexbox, а когда Grid?
Интервьюер может уточнить
- Что делает
gap? - Что означает
flex: 1?
Разбор
Flexbox раскладывает элементы вдоль одной оси и распределяет между ними свободное место. Хорош, когда элементы разного размера и каждый занимает столько, сколько ему нужно: навигация, шапка с логотипом слева и кнопками справа, выравнивание иконки с текстом. С flex-wrap: wrap элементы переносятся, но строки не выравниваются друг с другом по колонкам.
Grid задаёт сетку на контейнере: grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)) — карточки одной ширины, которые сами переносятся, и колонки ровные во всех рядах. Каркас страницы «шапка, меню, содержимое, подвал» удобно описывать через grid-template-areas.
Они не конкурируют: часто страница собрана на Grid, а внутри карточки элементы выстроены Flexbox. Простое правило: если думаешь о строках и колонках вместе — Grid, если о ряде элементов — Flexbox.
Уточняющие вопросы
Что делает gap?
Задаёт расстояние между элементами во flex- и grid-контейнере, не добавляя отступ по краям. Раньше то же самое делали margin у элементов и отрицательным margin у контейнера.
Что означает flex: 1?
Это сокращение для flex-grow: 1, flex-shrink: 1, flex-basis: 0%. Элемент забирает свободное место наравне с другими элементами с flex: 1 и может сжиматься.
- Flexbox — одномерная раскладка, Grid — двумерная.
- Во Flexbox размер элементов определяет содержимое, в Grid сетку задаёт контейнер.
- Приведены уместные примеры для каждого.
- Названо, что их совмещают на одной странице.
Почитать на сайте
Чем отличаются единицы px, em, rem, % и vw? Что лучше использовать для размера шрифта?
Интервьюер может уточнить
- Зачем некоторые пишут
html { font-size: 62.5%; }? - Что делает
clamp()для шрифта?
Разбор
px— абсолютная единица. Не меняется от настроек шрифта браузера.em— относительноfont-sizeэлемента. Во вложенных списках сfont-size: 1.2emразмер каждый раз умножается, что легко приводит к неожиданностям. Затоemудобен для отступов кнопки, которые должны расти вместе с её текстом.rem— относительноfont-sizeуhtml, по умолчанию16px.1.5rem = 24pxвезде, независимо от вложенности.%— база зависит от свойства: дляfont-sizeэто шрифт родителя, для размеров — соответствующий размер содержащего блока. Уpaddingиmarginв горизонтальном режиме письма база — ширина содержащего блока, даже для вертикальных отступов.vwиvh—1%ширины и высоты окна. На телефонах100vhможет быть больше видимой области из-за панели браузера, для этого естьdvh.
Шрифт в px не увеличится, если слабовидящий человек выставил в браузере крупный размер шрифта. rem уважает эту настройку, поэтому размеры шрифта задают в rem.
Уточняющие вопросы
Зачем некоторые пишут html { font-size: 62.5%; }?
Чтобы 1rem стал 10px и пересчитывать было проще. Но так легко забыть вернуть нормальный размер текста у body, а пересчитывать в уме не нужно — это делают DevTools или функция calc(). Сегодня этот приём используют редко.
Что делает clamp() для шрифта?
font-size: clamp(1rem, 2vw + 0.5rem, 2rem) — шрифт растёт вместе с шириной окна, но не меньше 1rem и не больше 2rem. Так делают «резиновые» заголовки без медиазапросов.
px— фиксированная единица.em— от шрифта элемента, накапливается при вложенности.rem— от шрифта корневого элемента.- % — от содержащего блока (база зависит от свойства),
vw/vh— от окна. - Для шрифта выбран
rem, объяснено про пользовательские настройки.
Почитать на сайте
Чем псевдоклассы отличаются от псевдоэлементов? Приведите примеры.
Интервьюер может уточнить
- Почему
::beforeне работает уimgиinput? - Чем
:focusотличается от:focus-visible? - Можно ли размещать важный текст в content у
::before?
Разбор
Псевдоклассы — условия для выбора элемента:
- состояние:
:hover,:focus,:focus-visible,:active,:checked,:disabled,:invalid; - положение:
:first-child,:last-child,:nth-child(2n); - логика:
:not(),:is(),:has().
Псевдоэлементы — части элемента: ::before и ::after добавляют содержимое в начало и конец элемента (нужно свойство content, хотя бы пустое), ::first-letter, ::first-line, ::placeholder, ::selection, ::marker.
.link:hover { text-decoration: none; }
.required::after { content: " *"; color: red; }Уточняющие вопросы
Почему ::before не работает у img и input?
::before и ::after вставляются внутрь элемента, перед его содержимым и после. У замещаемых элементов вроде img и input нет содержимого в этом смысле, поэтому у них псевдоэлементы не рисуются.
Чем :focus отличается от :focus-visible?
:focus срабатывает при любом фокусе, в том числе после клика мышью. :focus-visible — когда браузер считает, что рамку стоит показать, обычно при управлении с клавиатуры. Поэтому заметную обводку часто задают через :focus-visible, но не убирают совсем.
Можно ли размещать важный текст в content у ::before?
Лучше не надо: такой текст нельзя выделить и скопировать, а скринридеры читают его по-разному. Важное содержимое должно быть в HTML, а псевдоэлементы — для оформления.
- Псевдокласс выбирает элемент по состоянию или положению.
- Псевдоэлемент — часть элемента, не существующая отдельным тегом.
- Приведены примеры каждого.
- Названо одно и два двоеточия.
- Упомянуто свойство content для
::beforeи::after.
Почитать на сайте
Как сделать вёрстку адаптивной? Что такое медиазапросы и подход mobile first?
Интервьюер может уточнить
- Что ещё, кроме ширины, можно проверить в
@media? - Что такое контейнерные запросы?
Разбор
Основа адаптивности — не медиазапросы, а гибкая раскладка: относительные ширины, max-width вместо width, flex и grid с переносом, картинки с max-width: 100%. Медиазапросы меняют то, что гибкостью не решить: число колонок, порядок блоков, вид меню.
.cards { display: grid; gap: 16px; }
@media (min-width: 768px) {
.cards { grid-template-columns: 1fr 1fr; }
}Mobile first: базовые стили — для телефона, а min-width-запросы добавляют возможности большим экранам. Так стили для узкого экрана проще, для широкого экрана добавляются только необходимые изменения. При этом общий CSS-файл загружается целиком: медиазапрос сам по себе не уменьшает его размер. Обратный подход — desktop first с max-width.
Точки перелома лучше выбирать там, где ломается конкретная вёрстка, а не по списку моделей телефонов. Обязательно нужен <meta name="viewport" content="width=device-width, initial-scale=1">, иначе телефон эмулирует ширину около 980px.
Уточняющие вопросы
Что ещё, кроме ширины, можно проверить в @media?
Ориентацию (orientation), тип указателя (hover: hover, pointer: coarse — сенсорный экран), настройки пользователя: prefers-color-scheme: dark и prefers-reduced-motion: reduce, печать (@media print).
Что такое контейнерные запросы?
@container проверяет размер не окна, а родительского контейнера. Карточка может перестраиваться в зависимости от того, в какой колонке она стоит, а не от ширины всего экрана.
- Названа гибкая раскладка: относительные размеры,
flex,grid. - Объяснён медиазапрос с условием по ширине.
- Mobile first: базовые стили для узкого экрана,
min-widthдля широких. - Упомянут meta viewport.
Почитать на сайте
Почему у кнопки будет красный текст, хотя синий селектор содержит ID? Условия: один авторский stylesheet, нет inline-стилей, !important, анимаций и других правил.
@layer base {
#app .button { color: blue; }
}
.button { color: red; }
Интервьюер может уточнить
- Зачем вообще нужны каскадные слои?
- Как меняется порядок для
!important?
Разбор
!important порядок слоёв меняется, но в условиях вопроса таких объявлений нет.Уточняющие вопросы
Зачем вообще нужны каскадные слои?
Чтобы управлять приоритетом групп стилей — сброс, библиотека, компоненты, утилиты — порядком слоёв, а не наращиванием специфичности.
Как меняется порядок для !important?
Для важных объявлений он переворачивается: !important в более раннем слое сильнее, чем в позднем и вне слоёв.
- Обычное правило вне слоя сильнее обычного правила внутри слоя.
- Специфичность сравнивается после происхождения, важности и слоя.
- Порядок классов в class не решает конфликт.
Почитать на сайте
Спецификация: CSS Cascade: порядок каскада
В строке Flexbox есть иконка и длинный заголовок. Заголовок имеет flex: 1 и white-space: nowrap, но не сжимается. Исправьте CSS заголовка, чтобы он сжимался и показывал многоточие. Запишите код и объясните изменения.
Интервьюер может уточнить
- Почему не помогает
overflow: hiddenу всего контейнера? - Как обрезать текст не в одну строку, а в три?
Разбор
flex: 1 не отменяет автоматический минимальный размер. Неразрывная строка может удерживать элемент шире доступного места. Установите min-width: 0 на flex-элемент с заголовком, затем overflow: hidden и text-overflow: ellipsis; nowrap уже задан. Если текст находится во вложенном элементе, проверьте ограничения и его flex-предков. Скрытие переполнения всего контейнера лишь обрежет интерфейс и не объяснит причину.Уточняющие вопросы
Почему не помогает overflow: hidden у всего контейнера?
Он просто обрежет вылезающую часть, включая кнопки справа. Причина — в минимальном размере flex-элемента, и исправлять нужно его.
Как обрезать текст не в одну строку, а в три?
Для совместимого варианта используют display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;. Поддержку непредварённого line-clamp проверяют отдельно для целевых браузеров. Полный текст должен оставаться доступен, если он нужен для действия.
- У flex-элемента может срабатывать автоматический минимум по содержимому.
min-width: 0задаётся сжимаемому элементу.- Для многоточия нужны
overflow: hidden,text-overflow: ellipsisи ограниченная ширина.
Почитать на сайте
Спецификация: CSS Flexbox: минимальный размер
Сетка с колонками 240px 1fr выходит за ширину экрана из-за длинной строки кода во второй колонке. Запишите исправленные CSS-правила для сетки и блока кода. Объясните, почему 1fr не спасает.
Интервьюер может уточнить
- Как сделать, чтобы на телефоне колонки встали друг под другом?
- Чем это похоже на проблему с
flex?
Разбор
1fr подразумевает автоматический минимум дорожки, поэтому длинное содержимое может расширить её. minmax(0, 1fr) разрешает дорожке стать уже. На элементе с кодом задайте подходящую обработку переполнения, например overflow: auto. Нулевая нижняя граница не переносит текст сама. Также нужна адаптация фиксированной колонки 240px, если весь макет должен работать на очень узком экране.Уточняющие вопросы
Как сделать, чтобы на телефоне колонки встали друг под другом?
Медиазапросом сменить шаблон на одну колонку или сразу задать repeat(auto-fit, minmax(240px, 1fr)).
Чем это похоже на проблему с flex?
Механизм тот же: автоматический минимальный размер по содержимому. Во flex его снимает min-width: 0, в grid — minmax(0, 1fr).
1frделит свободное место, но автоматический минимум может зависеть от содержимого.- Предложен
minmax(0, 1fr)для второй дорожки. - Переполнение самого кода обрабатывается отдельно, например прокруткой.
Почитать на сайте
Спецификация: CSS Grid: размеры дорожек
Всплывающая панель с z-index: 9999 всё равно находится под соседней секцией с z-index: 2. Родитель панели имеет position: relative и z-index: 1. Как объяснить и исправить?
Интервьюер может уточнить
- Что ещё создаёт контекст наложения?
- Как вывести модальное окно поверх всего без
z-index?
Разбор
z-index перепрыгнуть соседний более высокий контекст родителя. Сначала проверьте контексты в DevTools. Можно изменить уровень родителя, убрать ненужный контекст или разместить панель в подходящем контейнере. Для настоящего диалога или popover рассмотрите нативный верхний слой. Простое увеличение 9999 не решает ограничение родительского контекста.Уточняющие вопросы
Что ещё создаёт контекст наложения?
opacity меньше 1, transform, filter, will-change, position: fixed или sticky, isolation: isolate, flex- и grid-элементы с z-index.
Как вывести модальное окно поверх всего без z-index?
Нативный <dialog> с showModal() или атрибут popover выводят элемент в верхний слой (top layer), выше любых z-index.
- Родитель создаёт контекст наложения, внутри которого ограничен
z-indexпотомка. - Сравниваются контексты родителей, а не все числа в документе.
- Предложено изменить структуру/уровень родителя или использовать подходящий верхний слой.
Почитать на сайте
Спецификация: CSS Position: контексты наложения
Как работают пользовательские свойства CSS (--brand: #0a6)? Чем они отличаются от переменных препроцессора и как сделать тёмную тему, не переписывая все правила?
Интервьюер может уточнить
- Что будет, если значение свойства некорректно для того места, где его подставили?
- Зачем
@property?
Разбор
Свойство с двумя дефисами объявляют как обычное: :root { --text: #222; --page: #fff; }, а используют через var(--text). Второй аргумент — запасное значение: var(--gap, 16px).
Главное отличие от переменных Sass — значение вычисляется в браузере для каждого элемента:
- переопределение действует на элемент и всех его потомков:
.card--danger { --accent: red; }; - его можно менять в медиазапросе и из JavaScript:
el.style.setProperty('--x', '10px'); - оно проходит внутрь Shadow DOM.
Тёмная тема — это смена значений, а не правил:
:root { --text: #222; --page: #fff; }
@media (prefers-color-scheme: dark) {
:root { --text: #eee; --page: #1b1d21; }
}
body { color: var(--text); background: var(--page); }Ручной переключатель темы добавляет атрибут, например [data-theme="dark"], с теми же переопределениями.
Уточняющие вопросы
Что будет, если значение свойства некорректно для того места, где его подставили?
Объявление не отбрасывается при разборе, а становится недействительным во время вычисления: свойство получает унаследованное или начальное значение, а не значение из предыдущего правила.
Зачем @property?
Чтобы задать свойству тип, начальное значение и наследование. Типизированное свойство можно плавно анимировать, например угол градиента.
- Объявление через
--имяи использование черезvar(), запасное значение. - Свойства наследуются и переопределяются для части страницы.
- Отличие от препроцессора: значение вычисляется в браузере.
- Тёмная тема — переопределение значений в медиазапросе или по атрибуту.
Спецификация: CSS Custom Properties
В проекте стили кнопки переопределяют в пяти местах, а в конце появляется !important. Как организовать CSS, чтобы такого не было? Как переопределять стили компонента UI-библиотеки?
Интервьюер может уточнить
- Когда
!importantвсё-таки уместен? - Чем CSS Modules отличаются от БЭМ?
Разбор
Войны специфичности начинаются, когда правило написано «по месту»: .sidebar .form .button. Следующему разработчику, чтобы его перебить, нужен селектор ещё сильнее, и в конце — !important.
Что помогает:
- Плоские селекторы. Один класс на правило, одинаковая специфичность у всех компонентов.
:where()обнуляет специфичность там, где нужна сложная выборка; - Именование по компоненту. БЭМ (
.card,.card__title,.card--wide) или CSS Modules: стиль принадлежит компоненту, а не месту на странице; - Варианты — модификаторами. Не «кнопка в шапке синяя», а
.button--primary, который ставят там, где нужно; - Слои.
@layer reset, base, components, utilities;— порядок слоёв важнее специфичности, поэтому обычное объявление утилиты сильнее обычного объявления компонента при прочих равных; для!importantпорядок слоёв обратный.
Стили UI-библиотеки: сначала посмотреть, нет ли у неё переменных или параметров темы. Если нет — подключить её в нижнем слое @layer vendor, тогда любые свои правила в слоях выше её перебьют без повышения специфичности.
Уточняющие вопросы
Когда !important всё-таки уместен?
В утилитах, которые обязаны сработать, например .visually-hidden, и в пользовательских стилях. В обычных компонентах это сигнал, что структура стилей сломана.
Чем CSS Modules отличаются от БЭМ?
БЭМ — договорённость об именах, которую соблюдают люди. CSS Modules делают имена уникальными при сборке, поэтому случайно задеть чужой класс нельзя.
- Названа причина: рост специфичности из-за правил «по месту».
- Плоские селекторы с одинаковой специфичностью.
- Названа методология именования (БЭМ, CSS Modules) или модификаторы вариантов.
- Названы
@layerили переменные библиотеки для переопределения.
Почитать на сайте
Спецификация: CSS Cascade 5: слои
Карточка товара стоит и в широкой сетке, и в узкой боковой колонке. Почему медиазапросы здесь плохо подходят и как решают задачу контейнерные запросы?
Интервьюер может уточнить
- Почему нельзя сделать контейнером саму карточку и менять её же раскладку?
- Что значит
container-type: inline-size?
Разбор
На широком экране карточка в боковой колонке может быть всего 280px шириной. Медиазапрос @media (min-width: 1024px) сработает и включит горизонтальную раскладку, для которой места нет.
.card-slot { container-type: inline-size; }
.card { display: grid; gap: 12px; }
@container (min-width: 400px) {
.card { grid-template-columns: 160px 1fr; }
}Обёртка становится контейнером запросов, и правила внутри @container проверяют её ширину. Контейнеру можно дать имя (container-name), чтобы запрашивать конкретного предка. Есть и единицы контейнера: cqi — процент ширины контейнера, удобно для размера шрифта заголовка.
Медиазапросы остаются для раскладки страницы целиком, а контейнерные — для компонентов, которые переиспользуются в разных местах.
Уточняющие вопросы
Почему нельзя сделать контейнером саму карточку и менять её же раскладку?
Запрос проверяет контейнер, а стили применяются к его потомкам. Если бы элемент менял свой размер в зависимости от своего же размера, получился бы бесконечный цикл. Поэтому нужна обёртка.
Что значит container-type: inline-size?
Контейнер отслеживает ширину в направлении строки и перестаёт зависеть по этой оси от размеров содержимого. Ширину ему задаёт окружение — сетка или колонка.
- Медиазапрос зависит от окна, а не от места компонента.
- Названы
container-typeу родителя и@container. - Стили применяются к потомкам контейнера.
- Разделение ролей: страница — медиазапросы, компонент — контейнерные.
Почитать на сайте
Спецификация: CSS Containment 3: контейнерные запросы
Когда анимацию лучше сделать на CSS, а когда на JavaScript? Как сделать её плавной и не навредить людям, которым движение мешает?
Интервьюер может уточнить
- Почему анимация «дёргается» на слабом телефоне?
- Как дождаться конца CSS-перехода в коде?
Разбор
CSS (transition, @keyframes) хорош, когда есть начальное и конечное состояние: наведение, раскрытие, появление. Анимация описана декларативно, может работать в потоке композиции и не мешает основному потоку.
JavaScript нужен, когда параметры вычисляются: элемент следует за пальцем, анимация останавливается на середине, шаги зависят от ответа сервера. element.animate() (Web Animations API) даёт те же возможности, что CSS, плюс управление из кода: пауза, обратный ход, промис окончания. Ручные анимации в цикле — только через requestAnimationFrame.
Плавность. transform и opacity не требуют раскладки; top, width, height — требуют на каждом кадре. will-change — подсказка заранее, но не для всех элементов подряд.
Доступность. У части людей движение вызывает головокружение. В медиазапросе prefers-reduced-motion: reduce крупное движение убирают или заменяют плавным появлением.
Уточняющие вопросы
Почему анимация «дёргается» на слабом телефоне?
Обычно из-за раскладки на каждом кадре или тяжёлой отрисовки (тени, размытие) либо из-за загруженного основного потока скриптами. Вкладка Performance показывает, на что уходит время кадра.
Как дождаться конца CSS-перехода в коде?
Событие transitionend или animationend, а у Web Animations API — animation.finished.
- CSS — для переходов между состояниями, JS — для движения по логике.
- Названы
transformиopacityдля плавности. - Назван
requestAnimationFrameили Web Animations API. - Назван
prefers-reduced-motion.
Почитать на сайте
Спецификация: Web Animations
Чем margin отличается от padding? У абзацев margin: 20px 0, но между ними 20px, а не 40px. Почему?
Интервьюер может уточнить
- Почему заголовок внутри карточки сдвигает вниз всю карточку?
- Можно ли задать отрицательный
margin?
Разбор
В блочной модели снаружи внутрь: margin → border → padding → содержимое. Фон элемента виден под содержимым и padding, но не под margin. Поэтому кнопке, чтобы стать больше, добавляют padding, а чтобы отодвинуть её от текста — margin.
Схлопывание (margin collapsing) бывает только у вертикальных отступов обычных блоков:
- у соседей: нижний отступ одного и верхний другого сливаются в один, равный большему, если оба положительные;
- у родителя и первого или последнего ребёнка, если между ними нет
padding,borderи содержимого: отступ ребёнка «выпадает» наружу родителя.
Схлопывания нет внутри Flexbox и Grid, у горизонтальных отступов между контейнером с display: flow-root и его детьми. Внешние отступы такого контейнера всё ещё могут схлопываться с соседними блоками. Многие задают отступы только в одну сторону, например margin-bottom, или gap во Flexbox и Grid — так поведение предсказуемее.
Уточняющие вопросы
Почему заголовок внутри карточки сдвигает вниз всю карточку?
Его margin-top схлопнулся с отступом карточки и оказался снаружи. Помогает padding у карточки, display: flow-root или margin-top: 0 у первого ребёнка.
Можно ли задать отрицательный margin?
Да: элемент сдвигается и может наехать на соседа. Отрицательного padding не бывает.
paddingвнутри рамки и закрашивается фоном,marginснаружи.- Вертикальные отступы соседей схлопываются до большего.
- Названо, где схлопывания нет: Flexbox, Grid, горизонтальные отступы или
flow-root.
Почитать на сайте
Спецификация: CSS 2: схлопывание отступов
Как подключить на сайт свой шрифт? Почему при загрузке текст сначала невидим или «прыгает», и как это смягчить?
Интервьюер может уточнить
- Зачем список шрифтов через запятую в
font-family? - Что такое вариативный шрифт?
Разбор
@font-face {
font-family: 'Golos';
src: url('/fonts/golos.woff2') format('woff2');
font-weight: 400;
font-display: swap;
}
body { font-family: 'Golos', Arial, sans-serif; }У статического шрифта каждое начертание — обычное, жирное, курсив — обычно отдельный файл и отдельное правило @font-face с font-weight и font-style. Иначе браузер сам «нарисует» жирный, и выйдет некрасиво.
Пока шрифт не загружен:
- при
font-display: autoпериод скрытия текста определяется браузером; фиксированное время не гарантируется; font-display: swapсразу показывает запасной шрифт и меняет, когда загрузится свой;- смена шрифта двигает строки — это сдвиг раскладки. Его уменьшают похожим запасным шрифтом и настройкой
size-adjust.
Ускорить: формат WOFF2, только нужные начертания и символы (кириллица и латиница), <link rel="preload" as="font" type="font/woff2" crossorigin> для главного шрифта.
Уточняющие вопросы
Зачем список шрифтов через запятую в font-family?
Если первого нет или он не загрузился, браузер берёт следующий. В конце ставят общее семейство — sans-serif или serif.
Что такое вариативный шрифт?
Один файл, в котором насыщенность и другие параметры меняются плавно. Вместо пяти файлов начертаний — один, иногда меньшего общего размера.
- Назван
@font-faceс файлом WOFF2. - Назван список запасных шрифтов в
font-family. - Назван
font-display: swapи проблема невидимого текста. - Названы способы ускорить:
preload, только нужные начертания.
Почитать на сайте
Спецификация: CSS Fonts 4: @font-face
Зачем в начале стилей сайта пишут сброс стилей? Чем полный сброс отличается от normalize и что обычно входит в современный минимальный набор?
Интервьюер может уточнить
- Почему поля формы не наследуют шрифт страницы?
- Что делает
box-sizing: border-box?
Разбор
У каждого браузера есть свои стили по умолчанию: body с отступом 8px, списки со своими отступами, кнопки и поля со своим шрифтом. Чтобы сайт выглядел одинаково, это выравнивают в начале CSS.
- Полный сброс (reset) убирает почти всё: заголовки одного размера, у списков нет маркеров. Плюс — полный контроль, минус — приходится заново оформлять даже то, что было удобно.
- Normalize оставляет разумные значения по умолчанию и только исправляет различия между браузерами.
*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; line-height: 1.5; }
img, svg, video { display: block; max-width: 100%; }
input, button, textarea, select { font: inherit; }Сброс не должен ломать доступность: нельзя убирать обводку фокуса без замены и маркеры у списков, которые остаются списками по смыслу, без проверки, что скринридер их всё ещё понимает.
Уточняющие вопросы
Почему поля формы не наследуют шрифт страницы?
У них свои системные стили. font: inherit заставляет их брать шрифт родителя.
Что делает box-sizing: border-box?
Заданная ширина включает padding и рамку. Блок в 300px остаётся 300px после добавления внутренних отступов.
- Названа причина: разные стили браузеров по умолчанию.
- Объяснено отличие полного сброса от normalize.
- Названы хотя бы два правила минимального набора.
- Сброс не убирает видимый фокус.
Почитать на сайте
Спецификация: CSS Cascade: стили браузера
Во Flexbox justify-content: center центрирует элементы по горизонтали, а после flex-direction: column — по вертикали. Почему? Чем justify-content отличается от align-items?
Интервьюер может уточнить
- Зачем
align-content? - Как прижать одну кнопку к правому краю, а остальные оставить слева?
Разбор
Ниже предполагается обычное горизонтальное направление письма writing-mode: horizontal-tb.
flex-direction: row(по умолчанию) — главная ось горизонтальная, поперечная — вертикальная;flex-direction: column— главная вертикальная, поперечная — горизонтальная.
justify-content (главная ось): flex-start, center, space-between — первый и последний к краям, остальное поровну, space-around, space-evenly.
align-items (поперечная ось): stretch — растянуть на всю высоту строки (по умолчанию), center, flex-start, baseline — по линии текста. align-self задаёт то же для одного элемента.
.header { display: flex; justify-content: space-between; align-items: center; }Логотип слева, меню справа, оба по центру высоты шапки. Отступы между элементами удобнее задавать gap, а не margin.
Уточняющие вопросы
Зачем align-content?
align-content распределяет строки многострочного flex-контейнера при свободном месте по поперечной оси. При flex-wrap: nowrap не действует. Контейнер с wrap считается многострочным даже тогда, когда элементы пока поместились в одну строку.
Как прижать одну кнопку к правому краю, а остальные оставить слева?
Дать ей margin-left: auto: автоматический отступ забирает всё свободное место.
- Названы главная и поперечная оси.
justify-content— главная ось,align-items— поперечная.flex-direction: columnменяет главную ось.
Почитать на сайте
Спецификация: CSS Flexbox: выравнивание
Нужна сетка карточек: на широком экране четыре в ряд, на узком — сколько поместится, но не уже 220px, и без медиазапросов. Как это сделать на Grid?
Интервьюер может уточнить
- Как сделать, чтобы одна карточка занимала две колонки?
- Что такое
grid-template-areas?
Разбор
.cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 16px;
max-width: calc(4 * 220px + 3 * 16px);
}Максимальная ширина здесь 928px: четыре минимальные колонки по 220px и три промежутка по 16px. Пятой колонке нужно уже 1164px, поэтому она не появится.
fr— доля свободного места: колонки делят остаток поровну;minmax(220px, 1fr)— колонка не уже 220px и не шире доли;repeat(auto-fill, …)— столько колонок, сколько влезет;auto-fitотличается, когда карточек мало: пустые колонки схлопываются, и карточки растягиваются на всю ширину.auto-fillоставляет пустые места;gap— отступы между ячейками без лишних отступов по краям.
Если карточка уже 220px на очень маленьком экране не помещается, вместо 220px пишут min(220px, 100%).
Уточняющие вопросы
Как сделать, чтобы одна карточка занимала две колонки?
grid-column: span 2. С автоматическими колонками на узком экране она может не поместиться, поэтому иногда это включают медиазапросом.
Что такое grid-template-areas?
Схема раскладки именованными областями, например "header header" "sidebar main". Удобно для каркаса страницы.
- Назван
repeat(auto-fill или auto-fit, minmax(220px, 1fr)). - Объяснены
frиminmax. - Назван
gap.
Почитать на сайте
Спецификация: CSS Grid: repeat()
Нужно подсветить карточку, в которой отмечен флажок, и форму, в которой есть поле с ошибкой, — без JavaScript. Как это сделать через :has() и чем он отличается от :is() и :where()?
Интервьюер может уточнить
- Чем
:user-invalidлучше:invalid? - Можно ли вкладывать
:has()в:has()?
Разбор
.card:has(input:checked) { border-color: var(--accent); }
form:has(:user-invalid) .submit { opacity: 0.6; }
.field:has(+ .hint) { margin-bottom: 4px; } /* поле, за которым идёт подсказка */Раньше CSS умел выбирать только вниз и вправо по дереву. :has() — «родительский селектор»: условие проверяется внутри элемента (или у соседей через + и ~), а стиль применяется к нему самому.
:is(h1, h2, h3) a= три селектора сразу. Специфичность — как у самого сильного аргумента;:where(h1, h2, h3) a— то же, но специфичность аргументов нулевая: такие правила легко перебить, удобно для базовых стилей и сброса;- оба «прощающие»: неизвестный браузеру аргумент не ломает весь список.
:has() внутри себя не прощающий: если там нераспознанный селектор, правило отбрасывается. Тяжёлые условия на больших деревьях, например body:has(…) для всего подряд, могут замедлять пересчёт стилей.
Уточняющие вопросы
Чем :user-invalid лучше :invalid?
:invalid срабатывает сразу при загрузке, и пустое обязательное поле краснеет до ввода. :user-invalid — только после того, как человек взаимодействовал с полем.
Можно ли вкладывать :has() в :has()?
Нет, вложенный :has() запрещён.
:has()выбирает элемент по содержимому или соседям.- Записаны селекторы для карточки и формы.
- Объяснено отличие
:is()и:where()по специфичности.
Почитать на сайте
Спецификация: Selectors 4: :has()
Заголовок должен быть 24px на телефоне и 48px на широком экране, плавно меняясь между ними, без пачки медиазапросов. Как это сделать и что учесть для доступности?
Интервьюер может уточнить
- Чем
clamp()отличается отmin()иmax()? - Почему максимальный размер стоит ограничивать?
Разбор
clamp(min, preferred, max) возвращает предпочтительное значение, ограниченное с двух сторон. Для плавной типографики предпочтительное — смесь фиксированной части и доли ширины окна:
h1 { font-size: clamp(1.5rem, 1rem + 2.5vw, 3rem); }Как посчитать середину: при ширине 320px нужно 24px, при 1280px — 48px. Наклон = (48 − 24) / (1280 − 320) = 0.025, то есть 2.5vw; начальная часть = 24 − 320 × 0.025 = 16px = 1rem при корневом размере шрифта 16px.
Доступность: если написать clamp(24px, 5vw, 48px), при увеличении текста в настройках браузера размер почти не изменится, а при масштабе страницы vw тоже не растёт. Часть в rem сохраняет реакцию на настройки пользователя. Тот же приём подходит для отступов: padding: clamp(1rem, 4vw, 3rem).
Для компонента в колонке вместо vw удобнее единицы контейнера cqi.
Добавление rem не гарантирует доступность само по себе: проверьте фактическое увеличение текста до 200% и отсутствие обрезания. Компонент в vw может частично компенсировать увеличение при масштабе страницы.
Уточняющие вопросы
Чем clamp() отличается от min() и max()?
min() берёт меньшее, max() — большее. clamp(a, b, c) — это max(a, min(b, c)).
Почему максимальный размер стоит ограничивать?
На очень широких мониторах заголовок без ограничения станет огромным, а строки — слишком короткими.
- Назван
clamp(min, preferred, max). - Предпочтительное значение — смесь
remиvw. - Объяснено, почему только
vwмешает увеличению текста.
Почитать на сайте
Спецификация: CSS Values 4: clamp()
При загрузке статьи текст несколько раз «прыгает» вниз: появляется баннер, картинка, шрифт. Как называется эта проблема, как её измерить и устранить средствами CSS и HTML?
Интервьюер может уточнить
- Зачем
widthиheight, если ширина задана в CSS как 100%? - Чем помогает
content-visibility: auto?
Разбор
CLS — максимальная сумма неожиданных сдвигов за окно активности: до 5 секунд с паузами между сдвигами меньше секунды. Хорошее значение — не больше 0.1. Смотрят в Lighthouse, во вкладке Performance (пункт Layout Shifts) и в данных реальных пользователей.
Частые причины и решения:
- Картинки без размеров — браузер не знает высоту, пока файл не загрузится. Атрибуты
widthиheightдают пропорции, CSSheight: autoсохраняет адаптивность. Для фона и блоков —aspect-ratio: 16 / 9; - Реклама, баннеры, фреймы —
min-heightу места под них; - Шрифты — смена запасного шрифта на свой меняет ширину строк. Помогают
size-adjustиascent-overrideв@font-faceдля запасного шрифта иpreloadосновного; - Вставка сверху — уведомление о cookie или баннер «скачайте приложение» над содержимым. Лучше показывать поверх (
position: fixed) или в зарезервированном месте; - Анимация размеров — анимировать
transform, а неheightиtop.
Из CLS исключают сдвиги в течение 500 мс после подходящего пользовательского ввода, например клика или нажатия клавиши. Прокрутка и движение указателя такого исключения не дают.
Уточняющие вопросы
Зачем width и height, если ширина задана в CSS как 100%?
Браузер берёт из атрибутов соотношение сторон и вычисляет высоту до загрузки файла.
Чем помогает content-visibility: auto?
Откладывает отрисовку блоков за пределами экрана. Вместе с contain-intrinsic-size — чтобы у отложенного блока была примерная высота и полоса прокрутки не прыгала.
- Названа метрика CLS и способ измерить.
- Резервирование места: размеры картинок,
aspect-ratioилиmin-height. - Названа проблема шрифтов или вставки содержимого сверху.
Почитать на сайте
Спецификация: CSS Sizing 4: aspect-ratio
Фотографии товаров разных пропорций нужно показать в карточках одинаковой высоты без растягивания лиц. Чем помогут aspect-ratio, object-fit и object-position?
Интервьюер может уточнить
- Что произойдёт, если задать и ширину, и высоту явно?
- Изменяет ли обрезка исходный файл?
Разбор
.photo {
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
object-position: center top;
}Здесь ширина задана, высота остаётся автоматической и получается из пропорции. object-fit меняет размещение изображения внутри его бокса, а не размер самой карточки. Если важен весь товар или схема, предпочтительнее contain. Обрезку проверяют на разных фотографиях.
Уточняющие вопросы
Что произойдёт, если задать и ширину, и высоту явно?
aspect-ratio не пересчитает две уже определённые стороны; изображение впишется в заданный бокс по object-fit.
Изменяет ли обрезка исходный файл?
Нет, это только отображение; браузер загружает исходный ресурс целиком.
- Различены размер бокса и размещение изображения.
- Объяснены обрезка
coverи полосыcontain. - Выбрано положение изображения с учётом важных деталей.
Спецификация: Первоисточник: css
Чтобы отключить кнопку отправки, ей добавили pointer-events: none. Можно ли всё ещё отправить форму с клавиатуры и как правильно запретить действие?
Интервьюер может уточнить
- Почему недостаточно изменить только цвет кнопки?
- Зачем проверять флаг загрузки в обработчике формы?
Разбор
CSS не задаёт состояние недоступности элемента. Кнопка с pointer-events: none может оставаться в обходе Tab, а клик попадёт в элемент под ней. button.disabled = true сообщает браузеру нативное состояние.
Если элемент по сценарию должен оставаться фокусируемым, возможен aria-disabled="true", но запрет активации тогда реализуется кодом. Отправку Enter и программный вызов проверяют отдельно; защита от дублирования данных также нужна на сервере.
Уточняющие вопросы
Почему недостаточно изменить только цвет кнопки?
Цвет не меняет поведения и не сообщает состояние вспомогательным технологиям.
Зачем проверять флаг загрузки в обработчике формы?
Вызвать операцию может другой путь интерфейса; единая проверка предотвращает повторный запуск, пока текущий ещё идёт.
- Названо сохранение клавиатурной доступности при
pointer-events: none. - Предложен нативный
disabled. - Для
aria-disabledтребуется отдельный запрет действия.
Почитать на сайте
Спецификация: Первоисточник: css
При печати статьи пропадает часть текста, а закреплённая шапка повторяется поверх каждой страницы. Как подготовить отдельное оформление для печати?
Интервьюер может уточнить
- Почему нельзя передать смысл только цветным фоном?
- Нужно ли ставить
break-inside: avoidна всю статью?
Разбор
@media print {
nav, .actions { display: none; }
.header { position: static; }
.article { height: auto; overflow: visible; color: #000; background: #fff; }
h2 { break-after: avoid; }
figure { break-inside: avoid; }
}Правила применяют к реальным блокам проекта, а не скрывают все элементы по тегу без проверки. Для полей страницы есть @page. Предпросмотр печати проверяют на длинной статье и таблицах: браузер вправе разорвать слишком большой блок, а фон может не печататься по настройкам пользователя.
Уточняющие вопросы
Почему нельзя передать смысл только цветным фоном?
Пользователь может отключить печать фонов; подписи и границы должны оставаться понятными.
Нужно ли ставить break-inside: avoid на всю статью?
Нет: статья длиннее страницы. Ограничение применяют к небольшим логическим блокам и проверяют фактические разрывы.
- Предложен
@media print. - Сняты ограничения высоты, переполнения и фиксированного позиционирования.
- Разрывы проверяются на длинном содержимом и в предпросмотре.
Почитать на сайте
Спецификация: Первоисточник: css
Новый эффект CSS работает не во всех целевых браузерах. Как добавить его так, чтобы основной текст и действия оставались доступны?
Интервьюер может уточнить
- Почему не определять поддержку по названию браузера?
- Что проверить кроме обычного экрана?
Разбор
.title { color: #174b73; }
@supports (background-clip: text) {
.title {
background: linear-gradient(90deg, #174b73, #6d287d);
background-clip: text;
color: transparent;
}
}Прозрачный текст включён только вместе с эффектом. В режиме печати или принудительных цветов стоит вернуть обычный цвет и убрать фон. Для селекторов доступна проверка @supports selector(:has(*)). Условие проверяет распознавание конструкции, а не внешний вид и доступность.
Уточняющие вопросы
Почему не определять поддержку по названию браузера?
Версии и реализации меняются, а браузер может иметь разные настройки. Проверка конкретной возможности ближе к задаче.
Что проверить кроме обычного экрана?
Контраст, принудительные цвета, печать и сохранение читаемости при отключённом улучшении.
- Базовый вариант работает без нового эффекта.
- Улучшение включается через
@supports. - Поддержка синтаксиса отличена от проверки результата.
Спецификация: Первоисточник: css
Каковы три результата вызовов и почему два счётчика не делят одно значение?
function makeCounter() {
let value = 0;
return () => ++value;
}
const a = makeCounter();
const b = makeCounter();
console.log(a(), a(), b());
Интервьюер может уточнить
- Где замыкания используются на практике?
- Может ли замыкание вызвать утечку памяти?
Разбор
a() вернёт 1, затем 2; b() вернёт 1. Замыкание сохраняет доступ к переменной, а не только снимок её начального значения. У a и b разные окружения, потому что makeCounter вызвана дважды. Такой механизм полезен для скрытого состояния, но долгоживущая функция также может удерживать ненужные объекты из своего окружения.Уточняющие вопросы
Где замыкания используются на практике?
Обработчики событий с данными из внешней функции, debounce и throttle, фабрики функций, приватное состояние модуля.
Может ли замыкание вызвать утечку памяти?
Да, если долгоживущая функция, например обработчик на window, держит ссылку на большие данные или удалённые DOM-элементы. Их не соберёт сборщик мусора, пока жива функция.
- Результаты: 1, 2, 1.
- Возвращённая функция сохраняет доступ к лексическому окружению.
- Каждый вызов
makeCounterсоздаёт отдельную переменнуюvalue.
Напишите кодом
Почитать на сайте
Спецификация: ECMAScript: лексические окружения
Изменится ли original после этого кода? Как обновить имя, не изменяя исходный объект?
const original = { user: { name: "Ира" } };
const copy = { ...original };
copy.user.name = "Олег";
Интервьюер может уточнить
- Как сделать полную глубокую копию?
- Почему не
JSON.parse(JSON.stringify(obj))?
Разбор
original.user.name станет «Олег»: оба верхних объекта ссылаются на один user. Для изменения используйте { ...original, user: { ...original.user, name: "Олег" } }. Полное глубокое копирование ради одного поля обычно избыточно. Важно копировать именно изменяемую ветку, а не считать spread универсальной защитой от мутаций.Уточняющие вопросы
Как сделать полную глубокую копию?
structuredClone(obj). Он копирует вложенные объекты, массивы, даты, Map и Set, но не функции и DOM-элементы.
Почему не JSON.parse(JSON.stringify(obj))?
Он теряет undefined, функции, даты превращает в строки и падает на циклических ссылках.
- Вложенный
userобщий, поэтому исходное имя станет Олег. - Spread копирует только верхний уровень.
- Предложена копия изменяемой ветки
user.
Почитать на сайте
Спецификация: ECMAScript: выражения
Нулевой лимит допустим и означает «ничего не показывать». Почему первый вариант ошибочен для limit=0 и какой вариант подходит?
const first = limit || 10;
const second = limit ?? 10;
Интервьюер может уточнить
- А параметр по умолчанию
function f(limit = 10)? - Что делает
??=?
Разбор
limit=0 первый вариант даст 10, второй сохранит 0. || выбирает запасное значение при любом falsy-операнде; ?? ограничен null и undefined. Ни один оператор не проверяет корректность лимита: например, отрицательное число потребует отдельной проверки по правилам приложения.Уточняющие вопросы
А параметр по умолчанию function f(limit = 10)?
Он подставляется только при undefined: f(0) и f(null) его не используют. Поэтому null придёт в функцию как есть.
Что делает ??=?
Присваивает значение, только если переменная равна null или undefined: options.limit ??= 10.
- Оператор
||заменяет все falsy-значения, включая 0. - Оператор
??заменяет толькоnullиundefined. - Проверка диапазона/типа остаётся отдельной задачей.
Напишите кодом
Спецификация: ECMAScript: выражения
Чем отличаются var, let и const? Можно ли изменить массив, объявленный через const?
Интервьюер может уточнить
- Что выведет цикл
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i))? - Как сделать объект действительно неизменяемым?
Разбор
Область видимости. var ограничен функцией и «протекает» из блоков: переменная из if или цикла видна после него. let и const существуют только внутри своего блока { … }.
Поднятие. Объявление var поднимается в начало функции, и до строки с присваиванием переменная равна undefined. let и const тоже известны с начала блока, но до объявления находятся во «временной мёртвой зоне»: обращение вызывает ReferenceError. Это защищает от использования переменной до её значения.
Изменяемость. const нельзя переприсвоить: const list = []; list = [1]; — ошибка. Но list.push(1) работает: меняется содержимое массива, а не связь переменной с ним.
Правило на практике: по умолчанию const, let — когда значение действительно меняется, var в новом коде не используют.
Уточняющие вопросы
Что выведет цикл for (var i = 0; i < 3; i++) setTimeout(() => console.log(i))?
Три раза 3: у var одна переменная на весь цикл, и к моменту срабатывания таймеров она уже равна 3. С let у каждой итерации своя переменная, и вывод будет 0, 1, 2.
Как сделать объект действительно неизменяемым?
Object.freeze(obj) запрещает добавлять, удалять и менять свойства. Но заморозка поверхностная: вложенные объекты остаются изменяемыми.
letиconst— блочная область видимости,var— функциональная.varдо объявления равенundefined,letиconstвызывают ошибку.constзапрещает переприсваивание, но объект внутри можно менять.- Названо правило:
constпо умолчанию,letпри изменении, безvar.
Спецификация: ECMAScript: let и const
Чем отличается == от ===? Что вернут эти сравнения?
0 == ''
'1' == 1
null == undefined
null === undefined
NaN === NaN
Интервьюер может уточнить
- Как сравнить два массива по содержимому?
- Чем
Object.isотличается от===?
Разбор
=== не приводит типы: '1' === 1 — false. == пытается привести операнды: строку к числу, true к 1 и так далее. Поэтому 0 == '' и '1' == 1 дают true — пустая строка становится 0, а '1' — числом 1.
null == undefined — true по специальному правилу, при этом ни null, ни undefined не равны через == ничему другому. Отсюда единственное популярное применение нестрогого сравнения: value == null проверяет сразу и null, и undefined.
NaN не равен ничему, даже самому себе. Проверять его нужно через Number.isNaN(x).
Объекты, массивы и функции сравниваются по ссылке: [] === [] и {} === {} — false, потому что это два разных объекта.
Уточняющие вопросы
Как сравнить два массива по содержимому?
Встроенного способа нет. Для простых массивов сравнивают длину и элементы: a.length === b.length && a.every((x, i) => x === b[i]). Для вложенных структур нужна рекурсивная функция или библиотека.
Чем Object.is отличается от ===?
Отличий два: Object.is(NaN, NaN) — true, а Object.is(0, -0) — false. В остальном работает как ===.
===сравнивает без приведения типов,==приводит.- Названы правильные результаты:
true,true,true,false,false. null == undefined— особое правило.NaNне равен сам себе, проверка черезNumber.isNaN.- Объекты сравниваются по ссылке.
Напишите кодом
Почитать на сайте
Спецификация: ECMAScript: сравнение на равенство
Какие типы данных есть в JavaScript? Чем примитивы отличаются от объектов? Что вернёт typeof null?
Интервьюер может уточнить
- Почему у строки можно вызвать метод, если это примитив?
- Чем
undefinedотличается отnull?
Разбор
Примитивы — простые неизменяемые значения. Когда вы присваиваете примитив другой переменной, копируется само значение. Методы вроде 'abc'.toUpperCase() не меняют строку, а возвращают новую.
Всё остальное — объекты: обычные объекты, массивы, функции, даты. В переменной хранится ссылка, поэтому const b = a не создаёт копию: обе переменные указывают на один объект, и изменение через b видно через a.
typeof возвращает строку с типом, но с особенностями: typeof null === 'object', typeof [] — тоже 'object', а typeof function(){} — 'function'. Поэтому массив проверяют через Array.isArray(), а null — сравнением value === null.
Уточняющие вопросы
Почему у строки можно вызвать метод, если это примитив?
При обращении к свойству JavaScript временно оборачивает примитив в объект-обёртку String, вызывает метод и выбрасывает обёртку. Создавать обёртки вручную через new String() не нужно.
Чем undefined отличается от null?
undefined — значение «ещё не задано»: неинициализированная переменная, отсутствующее свойство, функция без return. null ставят явно, чтобы сказать «значения нет». typeof undefined — 'undefined', а typeof null — 'object'.
- Названы примитивы (хотя бы
string,number,boolean,undefined,null). - Названы объекты, включая массивы и функции.
- Примитивы копируются по значению, объекты — по ссылке.
typeofnullвозвращает 'object' — историческая ошибка.Array.isArrayдля проверки массива.
Почитать на сайте
Спецификация: ECMAScript: типы языка
Какие значения в JavaScript считаются ложными в условии if? Почему условие if (items.length) работает, а if (count) может подвести?
Интервьюер может уточнить
- Что вернёт выражение
'Ира' && 0 && 'ok'? - Как явно превратить значение в логическое?
Разбор
В условиях, в !, && и || значение приводится к логическому. Ложными (falsy) считаются false, 0, -0, 0n, '', null, undefined, NaN.
Неочевидные истинные значения: строка '0' и 'false', пустой массив [], пустой объект {}. Поэтому пустоту массива проверяют по длине: if (items.length === 0).
Проблема if (count) в том, что 0 и пустая строка могут быть нормальными данными. Если нужно отличить «нет значения» от нуля, проверяют явно: count !== undefined или count != null. То же с запасными значениями: limit || 10 заменит и ноль, а limit ?? 10 — только null и undefined.
Уточняющие вопросы
Что вернёт выражение 'Ира' && 0 && 'ok'?
0. Оператор && возвращает первое ложное значение или последнее, если все истинны. Оператор || — первое истинное или последнее. Они возвращают сами значения, а не true и false.
Как явно превратить значение в логическое?
Boolean(value) или двойное отрицание !!value.
- Перечислены ложные значения (хотя бы
false, 0, '',null,undefined,NaN). - Пустой массив, пустой объект и '0' — истинные.
- Объяснена проблема с нулём как допустимым значением.
- Назван оператор
??или явная проверка наnull/undefined.
Спецификация: ECMAScript: ToBoolean
Чем объявление функции (function declaration) отличается от функционального выражения и стрелочной функции?
sayHi()
function sayHi() { return 'Привет' }
const sayBye = function () { return 'Пока' }
const double = (x) => x * 2
Интервьюер может уточнить
- Как вернуть объект из короткой стрелочной функции?
- Что будет с
this, если передать метод объекта вsetTimeout?
Разбор
Function declaration — function sayHi() {} на отдельной строке. Такие функции создаются до выполнения кода своей области, поэтому вызов sayHi() выше объявления работает.
Function expression — функция как значение: const sayBye = function () {}. Она создаётся, когда выполнение доходит до этой строки. Её удобно передавать как аргумент или возвращать из другой функции.
Стрелочная функция — короткая запись выражения: (x) => x * 2; если тело — одно выражение, оно возвращается без return. Главное отличие не в записи: у стрелки нет собственного this. Внутри метода объекта или обработчика она использует this внешнего кода. Это удобно для колбэков внутри методов, но стрелку не используют как метод объекта, которому нужен this этого объекта.
Уточняющие вопросы
Как вернуть объект из короткой стрелочной функции?
Обернуть его в скобки: () => ({ id: 1 }). Без скобок фигурные скобки считаются телом функции, и она вернёт undefined.
Что будет с this, если передать метод объекта в setTimeout?
Обычная функция потеряет объект: this определяется тем, как функция вызвана, а setTimeout вызывает её без объекта. Помогают setTimeout(() => user.say()) или user.say.bind(user).
- Function declaration поднимается и доступна до объявления.
- Function expression доступна только после присваивания.
- У стрелочной функции нет своего
this. - Стрелку нельзя вызвать через
new, у неё нетarguments.
Почитать на сайте
Спецификация: ECMAScript: стрелочные функции
Есть массив товаров. Как получить массив названий товаров дороже 1000 и их общую стоимость? Чем map отличается от forEach?
const products = [
{ title: 'Рюкзак', price: 3200 },
{ title: 'Ручка', price: 90 },
{ title: 'Лампа', price: 1500 },
]
Интервьюер может уточнить
- Какие методы массива меняют исходный массив?
- Почему [10, 9, 1].sort() даёт [1, 10, 9]?
Разбор
const expensive = products.filter((p) => p.price > 1000)
const titles = expensive.map((p) => p.title) // ['Рюкзак', 'Лампа']
const total = expensive.reduce((sum, p) => sum + p.price, 0) // 4700filter возвращает новый массив из элементов, для которых функция вернула true. map возвращает новый массив, где каждый элемент заменён результатом функции. reduce проходит по массиву с накопителем; начальное значение 0 важно — без него пустой массив вызовет ошибку.
forEach всегда возвращает undefined. Если внутри forEach собирать результат через push в другой массив, это значит, что нужен map или filter. Ещё find возвращает первый подходящий элемент, some и every — проверяют, есть ли хотя бы один подходящий и подходят ли все.
Уточняющие вопросы
Какие методы массива меняют исходный массив?
push, pop, shift, unshift, splice, sort, reverse и fill. Для sort и reverse есть копирующие варианты toSorted и toReversed. Если массив пришёл извне, перед sort стоит сделать копию: [...items].sort().
Почему [10, 9, 1].sort() даёт [1, 10, 9]?
Без функции сравнения sort сравнивает элементы как строки. Для чисел передают функцию: items.sort((a, b) => a - b).
- Решение через
filter,mapиreduceс правильным результатом. mapвозвращает новый массив,forEach—undefined.- Методы не меняют исходный массив.
- У
reduceуказано начальное значение.
Почитать на сайте
Как повесить обработчик клика на кнопку? Чем event.target отличается от event.currentTarget и что такое всплытие?
Интервьюер может уточнить
- Почему
querySelectorвернулnull? - Как найти кнопку, если кликнули по иконке внутри неё?
Разбор
const button = document.querySelector('.js-save')
button.addEventListener('click', (event) => {
console.log(event.target, event.currentTarget)
})addEventListener позволяет повесить несколько обработчиков на одно событие и снять их через removeEventListener с той же функцией. Атрибут onclick в HTML смешивает разметку с кодом и позволяет только один обработчик.
Всплытие: клик по иконке внутри кнопки сначала обрабатывается на иконке, затем на кнопке, затем на её родителях. Поэтому target может оказаться вложенным элементом, а currentTarget всегда тот, на ком висит обработчик. На всплытии построено делегирование: один обработчик на списке вместо обработчика на каждом пункте.
event.preventDefault() отменяет стандартное действие: переход по ссылке, отправку формы. event.stopPropagation() останавливает всплытие, но использовать его стоит осторожно — другие обработчики выше по дереву перестанут узнавать о событии.
Уточняющие вопросы
Почему querySelector вернул null?
Чаще всего скрипт выполнился до того, как элемент появился в DOM: подключён в head без defer. Решения — defer, скрипт в конце body или обработчик DOMContentLoaded. Вторая частая причина — ошибка в селекторе.
Как найти кнопку, если кликнули по иконке внутри неё?
event.target.closest('button') — ближайший предок (или сам элемент), подходящий под селектор.
- Назван
addEventListener. - Объяснено всплытие от элемента к предкам.
target— где произошло событие,currentTarget— чей обработчик выполняется.- Названы
preventDefaultи его отличие отstopPropagation.
Почитать на сайте
Спецификация: DOM: события
Нужно загрузить список пользователей с сервера и показать его. Как написать это через async/await и обработать ошибку? Что такое Promise?
Интервьюер может уточнить
- Как загрузить два независимых списка параллельно?
- Что будет, если забыть
awaitпередfetch?
Разбор
async function loadUsers() {
try {
const response = await fetch('/api/users')
if (!response.ok) throw new Error('HTTP ' + response.status)
const users = await response.json()
render(users)
} catch (error) {
showError('Не удалось загрузить пользователей')
}
}Сетевой запрос занимает время, и JavaScript не ждёт его, останавливая страницу: fetch сразу возвращает Promise. У промиса три состояния: ожидание, выполнен (fulfilled) и отклонён (rejected). Результат получают через .then() и .catch() или через await.
await можно писать только внутри async-функции (или на верхнем уровне модуля). Сама async-функция всегда возвращает Promise. Код после await продолжится, когда промис завершится, а пока браузер обрабатывает другие события.
Важная деталь fetch: промис отклоняется только при сетевой ошибке. Ответ 404 или 500 — это успешно полученный ответ, поэтому response.ok проверяют вручную.
Уточняющие вопросы
Как загрузить два независимых списка параллельно?
Запустить оба запроса сразу и ждать оба: const [users, posts] = await Promise.all([loadUsers(), loadPosts()]). Два await подряд выполнят запросы по очереди и займут вдвое больше времени.
Что будет, если забыть await перед fetch?
В переменной окажется сам Promise, а не ответ, и response.json станет ошибкой «не функция». А ошибка внутри промиса без await не попадёт в try/catch.
Promiseописан как результат будущей операции с состояниями.- Код с
async/awaitиtry/catch. - Проверяется
response.ok. awaitне блокирует страницу.- Упомянут
Promise.allдля параллельной загрузки.
Почитать на сайте
API может вернуть пользователя без адреса. Как безопасно прочитать город, не получив ошибку Cannot read properties of undefined?
const user = { name: 'Ира' }
console.log(user.address.city)
Интервьюер может уточнить
- Как это писали до появления ?.
- Можно ли присвоить значение через ?.
Разбор
user.address равно undefined, а у undefined и null нет свойств, поэтому .city вызывает TypeError и скрипт останавливается.
const city = user.address?.city ?? 'Город не указан'Оператор ?. проверяет значение слева: если это null или undefined, всё выражение сразу возвращает undefined. Он работает и для вызовов (user.getName?.()), и для индексов (list?.[0]). Оператор ?? подставляет запасное значение только для null и undefined, в отличие от ||, который заменит и пустую строку.
Но ?. не стоит ставить везде «на всякий случай». Если поле обязательно, его отсутствие — ошибка данных, и её лучше заметить сразу, а не показывать пустое место.
Уточняющие вопросы
Как это писали до появления ?.
Через цепочку &&: user.address && user.address.city. Работает, но длиннее и возвращает falsy-значение, а не undefined.
Можно ли присвоить значение через ?.
Нет: user.address?.city = 'Пермь' — синтаксическая ошибка. Опциональная цепочка только читает.
- Объяснена причина ошибки: обращение к свойству
undefined. - Назван оператор ?.
- Назван
??для запасного значения и его отличие от||. - Упомянуто, что обязательные поля не стоит маскировать.
Напишите кодом
Спецификация: ECMAScript: optional chaining
Почему при передаче метода отдельно теряется this? Покажите два способа передать вызов say как callback с сохранением объекта.
const user = {
name: "Ира",
say() { return this.name; }
};
const callback = user.say;
Интервьюер может уточнить
- Чем
callиapplyотличаются отbind? - Почему в классах методы иногда пишут стрелками в полях?
Разбор
user.say() передаёт user как this, а отдельный callback не сохраняет это правило автоматически. Передайте user.say.bind(user) или обёртку () => user.say(). Если функцию потом нужно передать removeEventListener, сохраните её в переменной: повторный bind создаст другой объект функции. Стрелка вместо метода не получает this объекта автоматически.Уточняющие вопросы
Чем call и apply отличаются от bind?
call и apply сразу вызывают функцию с указанным this (apply принимает аргументы массивом), а bind возвращает новую функцию с привязанным this.
Почему в классах методы иногда пишут стрелками в полях?
handleClick = () => {…} создаёт для каждого экземпляра функцию с привязанным this, и её можно передавать как обработчик. Цена — отдельная функция на каждый объект.
thisобычной функции определяется вызовом, а не местом объявления в объекте.- Предложены
user.say.bind(user)и() => user.say(). - Указано, что
bindсоздаёт новую функцию; для снятия обработчика нужна сохранённая ссылка.
Почитать на сайте
Спецификация: ECMAScript: связанные функции
В каком порядке появятся сообщения в обычном скрипте браузера? Почему таймер с нулевой задержкой не выполняется сразу?
console.log("start");
setTimeout(() => console.log("timer"), 0);
Promise.resolve().then(() => console.log("promise"));
console.log("end");
Интервьюер может уточнить
- Куда попадает код после
await? - Почему
setTimeout(fn, 0)иногда используют специально?
Разбор
start, end, promise, timer. Сначала завершается текущий синхронный код. Реакция уже выполненного Promise попадает в микрозадачи и обрабатывается до следующей задачи таймера. Нулевая задержка не прерывает текущий код и не означает выполнение ровно через 0 мс. Длинная цепочка микрозадач тоже может задерживать отзывчивость страницы.Уточняющие вопросы
Куда попадает код после await?
Продолжение после await выполняется как микрозадача, когда ожидаемый промис завершится. Если он ещё ждёт сеть или таймер, другие задачи могут выполниться раньше; обещать порядок «до всех таймеров» нельзя.
Почему setTimeout(fn, 0) иногда используют специально?
Чтобы отложить работу до следующей задачи: дать браузеру отрисовать изменения или обработать события. Для переноса на следующий кадр лучше requestAnimationFrame.
- Порядок:
start,end,promise,timer. - Синхронный код завершается до реакции
Promise. - Микрозадачи выполняются до следующей задачи таймера; 0 не обещает точное время.
Почитать на сайте
Спецификация: WHATWG: цикл событий
Страница загружает три независимых виджета. Ошибка одного не должна скрывать остальные. Что выбрать: Promise.all или Promise.allSettled? Отменит ли all остальные запросы при ошибке?
Интервьюер может уточнить
- Чем
Promise.raceотличается отPromise.any? - Как действительно отменить запросы?
Разбор
fulfilled и rejected отдельно. all удобен, когда для результата нужны все успешные значения. Его отклонение не отменяет уже начатые запросы; отмену нужно организовать отдельно. Оба метода сохраняют порядок входных элементов в успешном массиве результатов, а не сортируют по скорости.Уточняющие вопросы
Чем Promise.race отличается от Promise.any?
race завершается первым завершившимся промисом — успешным или нет. any ждёт первого успешного и отклоняется, только если отклонились все.
Как действительно отменить запросы?
Передать в fetch signal от AbortController и вызвать controller.abort().
- allSettled позволяет получить статус каждой операции.
- all отклоняется при первой обработанной ошибке, но не отменяет остальные операции.
- Порядок итоговых элементов соответствует входу, а не завершению.
Напишите кодом
Почитать на сайте
Спецификация: ECMAScript: Promise.all и Promise.allSettled
В списке динамически добавляются кнопки удаления с вложенными иконками. Как обработать клики одним слушателем? Чем preventDefault отличается от stopPropagation?
Интервьюер может уточнить
- Какие события не всплывают?
- Зачем проверять, что найденная кнопка внутри контейнера?
Разбор
click на контейнер. Если event.target является Element, найдите ближайшую кнопку через closest и проверьте принадлежность списку. Это работает и для новых кнопок. preventDefault не останавливает всплытие, а stopPropagation не отменяет переход по ссылке или отправку формы. Для вложенных списков дополнительно проверяйте, что кнопка относится именно к текущему списку.Уточняющие вопросы
Какие события не всплывают?
focus, blur, mouseenter, mouseleave, load. Для фокуса есть всплывающие аналоги focusin и focusout.
Зачем проверять, что найденная кнопка внутри контейнера?
closest может найти кнопку выше контейнера, если клик был по вложенной области. Проверка list.contains(button) защищает от чужих элементов.
- Слушатель на контейнере использует всплытие и
closestдля вложенной иконки. targetпроверяется какElement, найденная кнопка принадлежит нужному контейнеру.preventDefaultотменяет отменяемое действие по умолчанию;stopPropagationвлияет на распространение.
Почитать на сайте
Спецификация: WHATWG DOM: распространение событий
Как в JavaScript работает наследование через прототипы? Что на самом деле создаёт class Dog extends Animal?
Интервьюер может уточнить
- Как создать объект без прототипа и зачем?
- Чем
inотличается отObject.hasOwn()?
Разбор
При чтении dog.speak() движок ищет speak у самого объекта, затем у его прототипа (Object.getPrototypeOf(dog)), затем у прототипа прототипа и так до null. Так методы хранятся один раз и используются всеми экземплярами.
class Animal { speak() { return '…' } }
class Dog extends Animal { speak() { return 'Гав' } }
const dog = new Dog()
// dog → Dog.prototype → Animal.prototype → Object.prototype → nullЧто делает класс:
- методы попадают в
Dog.prototype, а не копируются в каждый объект; extendsставитAnimal.prototypeпрототипомDog.prototype, аsuper.speak()вызывает метод родителя;- в конструкторе наследника
super()нужно вызвать до обращения кthis; - в отличие от функции-конструктора, класс нельзя вызвать без
new, а код внутри всегда в строгом режиме.
Поля класса (name = 'Тони') и приватные поля (#secret) создаются у каждого экземпляра, а не в прототипе.
Уточняющие вопросы
Как создать объект без прототипа и зачем?
Object.create(null). У такого объекта нет унаследованных свойств вроде toString, поэтому он подходит как чистый словарь. Сегодня для этого чаще берут Map.
Чем in отличается от Object.hasOwn()?
in ищет свойство и в цепочке прототипов, Object.hasOwn(obj, key) — только у самого объекта.
- Поиск свойства идёт по цепочке прототипов.
- Методы класса лежат в
prototypeи общие для экземпляров. extendsсвязывает прототипы,superвызывает родителя.- Класс — синтаксис над прототипами, а не отдельная модель.
Почитать на сайте
Спецификация: ECMAScript: определения классов
Чем ES-модули (import/export) отличаются от CommonJS (require)? Как подключить тяжёлую библиотеку графиков только тогда, когда пользователь открыл вкладку со статистикой?
Интервьюер может уточнить
- Чем именованный экспорт отличается от экспорта по умолчанию?
- Что мешает tree shaking?
Разбор
ES-модули. import и export пишутся на верхнем уровне, и связи между файлами известны заранее. Это даёт tree shaking, раннюю загрузку зависимостей и понятные ошибки, если имени нет. Импорт — живая привязка: если модуль поменяет экспортированную переменную, импортирующий это увидит. В браузере подключаются через <script type="module">, который по умолчанию ведёт себя как defer.
CommonJS. require() — обычная функция: её можно вызвать в условии, а результат — значение module.exports, обычно из кеша модуля. Если это объект, возвращается ссылка на него, а не копия; последующее переназначение module.exports не меняет уже полученную ссылку. Сборщику труднее понять, что используется.
Ленивая загрузка.
tab.addEventListener('click', async () => {
const { drawChart } = await import('./chart.js')
drawChart(data)
})Сборщик выносит chart.js и его зависимости в отдельный файл, который загрузится только при первом щелчке. Стоит показать индикатор загрузки и обработать ошибку сети.
Уточняющие вопросы
Чем именованный экспорт отличается от экспорта по умолчанию?
Именованный импортируют под тем же именем (import { drawChart }), и редактор подскажет его и при переименовании. Экспорт по умолчанию каждый называет как хочет, из-за чего одно и то же в проекте называется по-разному.
Что мешает tree shaking?
Побочные эффекты при импорте, например код, который при загрузке меняет глобальные объекты, и CommonJS. Флаг sideEffects в package.json подсказывает сборщику, что выкидывать безопасно.
- ES-модули статические, CommonJS — вызов во время выполнения.
- Названы tree shaking или раннее знание зависимостей.
- Ленивая загрузка через динамический
import(). - Упомянуты индикатор загрузки или обработка ошибки.
Почитать на сайте
Спецификация: ECMAScript: модули
Одностраничное приложение через час работы занимает гигабайт памяти. Как устроена сборка мусора в JavaScript и какие ошибки в коде её ломают? Где тут поможет WeakMap?
Интервьюер может уточнить
- Как удобно снять сразу несколько слушателей?
- Почему нельзя перебрать
WeakMap?
Разбор
Объект живёт, пока до него есть путь по ссылкам от корней. Сборщик периодически помечает всё достижимое и освобождает остальное. Поэтому утечка в JavaScript — это не «забыли освободить», а «случайно держим ссылку».
Типичные причины:
- слушатели на
windowиdocument, которые не сняли при уходе со страницы, — они держат обработчик и всё, что он замкнул; setIntervalбезclearInterval;- кеш в
Mapили массиве, который только растёт; - удалённый из страницы элемент, на который осталась ссылка в переменной или массиве;
- подписки на хранилище или сокет без отписки.
WeakMap хранит ключи слабо: если на объект-ключ больше нет ссылок, запись удаляется вместе с ним. Это удобно для данных «к элементу» или «к объекту» без ручной очистки. У WeakMap нет перебора и размера — именно поэтому сборщик может удалять записи когда угодно.
Искать утечку: вкладка Memory в DevTools, два снимка кучи до и после повторения действия и сравнение того, что выросло.
Уточняющие вопросы
Как удобно снять сразу несколько слушателей?
Передать всем один signal от AbortController: addEventListener('resize', fn, { signal }), а при очистке вызвать controller.abort().
Почему нельзя перебрать WeakMap?
Её содержимое зависит от того, когда прошла сборка мусора. Перебор давал бы разный результат в разное время, поэтому его нет.
- Сборщик удаляет недостижимые объекты.
- Утечка — лишняя ссылка; названы хотя бы две причины.
WeakMapне удерживает ключ, нет перебора.- Названа диагностика: снимки памяти в DevTools.
Почитать на сайте
Спецификация: ECMAScript: WeakMap
Поле поиска отправляет запрос на каждый символ, а обработчик прокрутки пересчитывает положение элементов сотни раз в секунду. Что такое debounce и throttle, что выбрать в каждом случае и как устроен debounce?
Интервьюер может уточнить
- Что такое leading и trailing вызов?
- Как отменить отложенный вызов, например при закрытии окна поиска?
Разбор
Оба способа ограничивают частоту вызовов, но по-разному:
- debounce ждёт, пока события прекратятся: человек перестал печатать на 300 мс — отправляем запрос;
- throttle пропускает не больше одного вызова за интервал: при прокрутке положение пересчитывается, скажем, раз в 100 мс, а не на каждый пиксель.
function debounce(fn, delay) {
let timer
return function (...args) {
clearTimeout(timer)
timer = setTimeout(() => fn.apply(this, args), delay)
}
}
input.addEventListener('input', debounce(search, 300))Замыкание хранит timer между вызовами, а apply сохраняет this и аргументы. Для визуальных обновлений при прокрутке часто лучше requestAnimationFrame — он синхронизирован с кадрами — или IntersectionObserver, если нужно знать, виден ли элемент.
Debounce не решает гонку ответов: старый запрос всё ещё может прийти позже нового. Это отдельная задача — отмена через AbortController или проверка, что ответ актуален.
Уточняющие вопросы
Что такое leading и trailing вызов?
Leading — вызвать сразу на первом событии, trailing — в конце серии. Для кнопки «Отправить» с защитой от двойного нажатия нужен leading, для поиска — trailing.
Как отменить отложенный вызов, например при закрытии окна поиска?
Вернуть из debounce функцию с методом cancel(), который делает clearTimeout(timer).
- Debounce — после паузы, throttle — не чаще интервала.
- Поиск — debounce, прокрутка — throttle или
requestAnimationFrame. - Debounce устроен как сбрасываемый таймер в замыкании.
- Сохраняются
thisи аргументы.
Напишите кодом
Почитать на сайте
Спецификация: HTML: таймеры
Что делают const { name, city = 'Москва' } = user и const [first, ...rest] = items? Чем оператор ... в вызове функции отличается от ... в параметрах?
Интервьюер может уточнить
- Что будет, если деструктурировать
undefined? - Как поменять местами две переменные?
Разбор
Деструктуризация:
const { name, city = 'Москва' } = user // city — если в user его нет или undefined
const { name: title } = user // переименование
const [first, ...rest] = [1, 2, 3] // first = 1, rest = [2, 3]
function show({ id, price }) { … } // прямо в параметрахОдни и те же три точки делают противоположное:
- rest (собрать) — в параметрах
function sum(...nums)и в деструктуризацииconst { id, ...other } = obj; - spread (развернуть) — в вызове
Math.max(...nums), в массиве[...a, ...b], в объекте{ ...defaults, ...options }.
При слиянии объектов побеждает свойство, записанное позже: { ...defaults, ...options } — настройки пользователя перекроют умолчания. Копия поверхностная: вложенные объекты остаются общими.
Уточняющие вопросы
Что будет, если деструктурировать undefined?
Ошибка TypeError. Для необязательного аргумента пишут значение по умолчанию: function f({ id } = {}).
Как поменять местами две переменные?
[a, b] = [b, a].
- Объяснена деструктуризация объекта по именам и массива по порядку.
- Названо значение по умолчанию.
- Rest собирает, spread разворачивает; приведены примеры.
- Копия через spread поверхностная.
Спецификация: ECMAScript: деструктуризация
Нужно убрать повторы из списка тегов и посчитать, сколько раз встречается каждое слово в тексте. Когда для этого подходят Set и Map и чем Map лучше обычного объекта?
Интервьюер может уточнить
- Как
Setсравнивает объекты? - Как получить пять самых частых слов?
Разбор
const unique = [...new Set(['html', 'css', 'html'])] // ['html', 'css']
const counts = new Map()
for (const word of text.split(' ')) {
counts.set(word, (counts.get(word) ?? 0) + 1)
}Set проверяет наличие через has() быстро, без перебора массива, — удобно для «уже видели этот id?».
Map лучше объекта для словаря, потому что:
- ключ может быть любым: число, объект, DOM-элемент. Ключи обычного объекта — строки или символы; остальные значения преобразуются в строки;
- нет унаследованных ключей: слово
constructorилиtoStringне совпадёт со встроенным свойством; - есть
size, а перебор идёт в порядке добавления.
Обычный объект остаётся удобнее для данных с известными полями и для JSON: Map не превращается в JSON напрямую — нужно Object.fromEntries(map).
Уточняющие вопросы
Как Set сравнивает объекты?
По ссылке: два разных объекта с одинаковыми полями — разные значения. Для уникальности по id берут Map с ключом id.
Как получить пять самых частых слов?
[...counts].sort((a, b) => b[1] - a[1]).slice(0, 5).
Setхранит уникальные значения, убирает повторы.Mapхранит пары с любыми ключами.- Назван подсчёт через
get/set. - Названо отличие от объекта: ключи,
sizeили порядок.
Напишите кодом
- Задача «Убрать повторы»
- Задача «Сгруппировать по полю»
- Задача «Частота слов»
- Задача «Анаграммы»
- Задача «Два числа с заданной суммой»
- Задача «LRU-кеш»
Почитать на сайте
Спецификация: ECMAScript: Map
Нужно искать город в списке по вводу пользователя: « мос» должно найти «Москва». Какие методы строк понадобятся и что учесть?
Интервьюер может уточнить
- Как подсветить совпадение в найденном названии?
- Как отсортировать города по алфавиту правильно?
Разбор
function findCities(cities, input) {
const query = input.trim().toLowerCase().replaceAll('ё', 'е')
if (!query) return []
return cities.filter((city) =>
city.toLowerCase().replaceAll('ё', 'е').startsWith(query)
)
}trim()убирает пробелы по краям — их часто вставляют случайно;toLowerCase()у обеих строк — иначе «мос» не найдёт «Москва»;startsWith()— совпадение с начала,includes()— в любом месте;- пустой запрос лучше не искать:
''находится в любой строке; - «ё» и «е» пользователи пишут вперемешку — их стоит считать одинаковыми.
Строки в JavaScript неизменяемы: input.trim() не меняет input, а возвращает новую строку. Для сравнения с учётом языка есть localeCompare() и Intl.Collator.
Уточняющие вопросы
Как подсветить совпадение в найденном названии?
Найти позицию через indexOf, разрезать строку slice на три части и обернуть середину в элемент. Вставлять через textContent и создание элементов, а не через innerHTML с вводом пользователя.
Как отсортировать города по алфавиту правильно?
cities.sort((a, b) => a.localeCompare(b, 'ru')): обычное сравнение строк ставит заглавные и «ё» не туда.
- Названы
trim()иtoLowerCase(). - Названы
startsWith()илиincludes(). - Учтён пустой запрос или «ё».
- Строки неизменяемы, методы возвращают новую строку.
Почитать на сайте
Спецификация: ECMAScript: методы String
Как работает цикл for...of «под капотом»? Что такое генератор и где он полезен на практике?
Интервьюер может уточнить
- Почему
for...ofне работает с обычным объектом? - Чем
for...ofотличается отfor...in?
Разбор
Протокол итерации: объект перебираемый, если у него есть метод [Symbol.iterator], который возвращает объект с next(). Каждый вызов next() даёт { value, done }. Так перебираются массивы, строки, Map, Set, и на этом же работают spread [...x] и деструктуризация.
function* range(from, to) {
for (let i = from; i <= to; i++) yield i
}
[...range(1, 3)] // [1, 2, 3]
function* walk(node) {
yield node
for (const child of node.children) yield* walk(child)
}Генератор выполняется по шагам: до первого yield, затем пауза до следующего next(). Поэтому значения считаются лениво — бесконечная последовательность не зависнет, если брать из неё понемногу.
Асинхронный генератор async function* с for await...of удобен для постраничной загрузки: функция сама запрашивает следующую страницу, когда предыдущая закончилась, а вызывающий код просто перебирает элементы.
Уточняющие вопросы
Почему for...of не работает с обычным объектом?
У него нет [Symbol.iterator]. Перебирают Object.keys(obj), Object.values(obj) или Object.entries(obj).
Чем for...of отличается от for...in?
for...of перебирает значения через итератор, for...in — строковые ключи объекта, включая унаследованные. Для массивов for...in не используют.
- Назван протокол итерации:
Symbol.iteratorиnext(). - Генератор через
function*иyield, выполнение по шагам. - Назван практический пример: ленивая последовательность, обход дерева или постраничная загрузка.
Спецификация: ECMAScript: генераторы
Как организовать обработку ошибок в приложении, чтобы пользователь видел понятное сообщение, а разработчики узнавали о сбоях? Почему try/catch вокруг setTimeout не ловит ошибку внутри?
Интервьюер может уточнить
- Что будет с промисом, у которого нет
catch? - Что показывать пользователю, а что — разработчику?
Разбор
Где ловить. Ошибку ловят там, где знают, что с ней делать: в функции загрузки — показать «Не удалось загрузить, повторить?», в форме — сообщение у поля. Ловить всё подряд и молча проглатывать — хуже, чем не ловить.
Асинхронность. try/catch работает только для кода, выполняющегося внутри блока сейчас:
try {
setTimeout(() => { throw new Error('!') }, 0) // не поймается
} catch {}
try {
await loadUser() // поймается
} catch (error) { showError(error) }Свои классы ошибок. class HttpError extends Error с полем status позволяет различать сетевую ошибку, 404 и ошибку проверки. При повторном выбрасывании сохраняют причину: new Error('Не удалось сохранить', { cause: error }).
Верхний уровень. window.addEventListener('error', …) и 'unhandledrejection' ловят всё необработанное — их отправляют в систему мониторинга вместе с адресом страницы и действием пользователя. finally снимает индикатор загрузки при любом исходе.
Уточняющие вопросы
Что будет с промисом, у которого нет catch?
При отклонении браузер выведет ошибку в консоль и вызовет событие unhandledrejection; в Node.js процесс по умолчанию завершится.
Что показывать пользователю, а что — разработчику?
Пользователю — что случилось и что делать: повторить, проверить сеть, обратиться в поддержку. Технические подробности и стек — в мониторинг, а не на экран.
try/catchловит синхронный код иawaitвнутри блока.- Названы обработчики
errorиunhandledrejectionили мониторинг. - Пользователь видит понятное состояние ошибки.
- Названы свои классы ошибок,
causeилиfinally.
Почитать на сайте
Спецификация: ECMAScript: оператор try
Функция сортирует список товаров для вывода, и после этого «ломается» исходный порядок в другом месте приложения. Почему? Какие методы массива меняют его, а какие возвращают новый?
Интервьюер может уточнить
- Почему
[10, 9, 1].sort()даёт[1, 10, 9]? - Защитит ли
constмассив от изменений?
Разбор
const sorted = products.sort((a, b) => a.price - b.price)
sorted === products // true — это тот же массив
const safe = products.toSorted((a, b) => a.price - b.price) // новый
// или [...products].sort(…)Массив в переменной — это ссылка. Функция получила тот же массив, что и остальной код, и sort переставил элементы для всех.
Меняют исходный массив: push, pop, shift, unshift, splice, sort, reverse, fill, copyWithin.
Возвращают новый: map, filter, slice, concat, flat и современные toSorted, toReversed, toSpliced, with(index, value).
В интерфейсах, где важно отслеживать изменения (например, в React), массивы обновляют только через новые копии.
Уточняющие вопросы
Почему [10, 9, 1].sort() даёт [1, 10, 9]?
Без функции сравнения элементы сравниваются как строки: «10» идёт раньше «9». Для чисел нужно sort((a, b) => a - b).
Защитит ли const массив от изменений?
Нет, const запрещает только присвоить переменной другой массив. Содержимое менять можно.
sortменяет исходный массив, массивы передаются по ссылке.- Названы методы, которые меняют массив.
- Названы методы, которые возвращают новый, включая
toSortedили копию.
Напишите кодом
Почитать на сайте
Спецификация: ECMAScript: Array.prototype.toSorted
Нужно показать дату заказа «24 октября 2026» и посчитать, через сколько дней доставка. Какие подвохи есть у Date и как вывести дату по-русски?
Интервьюер может уточнить
- Как получить текущую метку времени?
- Что такое Temporal?
Разбор
Если доставка задана календарными датами без времени, сравнивайте именно даты. В примере год, месяц и день уже проверены, месяц записан как число от 1 до 12:
const order = { year: 2026, month: 10, day: 24 }
const delivery = { year: 2026, month: 10, day: 27 }
const dayNumber = ({ year, month, day }) =>
Date.UTC(year, month - 1, day) / 86400000
const days = dayNumber(delivery) - dayNumber(order) // 3
new Intl.DateTimeFormat("ru-RU", {
day: "numeric", month: "long", year: "numeric", timeZone: "UTC",
}).format(new Date(Date.UTC(order.year, order.month - 1, order.day)))UTC здесь используется для арифметики календарных дат, а не как часовой пояс доставки. Если API присылает моменты времени, сначала определите календарные даты в часовом поясе доставки. Для длительности в часах, наоборот, вычитают метки времени. Простое округление разницы локальных дат не является универсальным исправлением.
new Date(2026, 9, 24) создаёт локальную дату, месяцы нумеруются с нуля. Строка 2026-10-24 интерпретируется как полночь UTC, а 2026-10-24T00:00 — как местное время. Методы вроде setDate меняют исходный объект.
Уточняющие вопросы
Как получить текущую метку времени?
Date.now() — число миллисекунд с 1 января 1970 по UTC.
Что такое Temporal?
Новый встроенный API для дат: неизменяемые объекты, отдельные типы для даты без времени и для времени с часовым поясом. Поддерживается ещё не во всех браузерах.
- Различены календарная дата и момент времени.
- Объяснены нумерация месяцев с нуля и роль часового пояса.
- Для вывода выбран
Intl.DateTimeFormat; календарные дни не считаются слепым делением локальных миллисекунд.
Почитать на сайте
Спецификация: ECMAScript: объекты Date
Нужно загрузить 200 картинок для обработки. В чём разница между await в цикле for, Promise.all(items.map(…)) и загрузкой не больше пяти одновременно? Как написать последний вариант?
Интервьюер может уточнить
- Почему
items.forEach(async (item) => await load(item))не ждёт загрузок? - Сколько запросов браузер выполняет одновременно сам?
Разбор
async function mapLimit(items, limit, fn) {
if (!Number.isInteger(limit) || limit < 1) throw new RangeError("limit должен быть положительным целым")
const results = new Array(items.length)
let next = 0
async function worker() {
while (next < items.length) {
const index = next++
results[index] = await fn(items[index], index)
}
}
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker))
return results
}
const images = await mapLimit(urls, 5, loadImage)await fn(item)внутриfor...of— последовательно: нужно, когда шаги зависят друг от друга или порядок важен;Promise.all— всё параллельно: хорошо для нескольких независимых запросов, плохо для сотен;- пул — пять работников делят общий счётчик
next. Так как JavaScript однопоточный,next++междуawaitбезопасен; результаты кладутся по индексу, порядок сохраняется.
Ошибки: в этом варианте первая ошибка отклонит общий промис, но остальные работники продолжат. Если нужны все результаты, оборачивают fn в try/catch и сохраняют { ok, value, error } — как в Promise.allSettled. Для отмены передают AbortSignal.
Уточняющие вопросы
Почему items.forEach(async (item) => await load(item)) не ждёт загрузок?
forEach не ждёт промисы, которые возвращает колбэк: он запускает все сразу и сразу заканчивается.
Сколько запросов браузер выполняет одновременно сам?
Число зависит от протокола, браузера и сервера. HTTP/2 и HTTP/3 допускают много потоков в одном соединении, но это не отменяет лимитов сервера и памяти приложения. Размер пула выбирают по измерениям и ограничениям API.
- Объяснены последовательный вариант и
Promise.all. - Названы минусы
Promise.allна сотнях элементов. - Описан пул с ограничением параллельности.
- Учтены порядок результатов или обработка ошибок.
Напишите кодом
Почитать на сайте
Спецификация: ECMAScript: Promise.all
Нужно сохранить черновик формы и дать пользователю «откатить» изменения. Почему JSON.parse(JSON.stringify(obj)) иногда портит данные и что делает structuredClone?
Интервьюер может уточнить
- Что делает
Object.freezeи защищает ли он вложенные объекты? - Когда JSON-копия всё-таки подходит?
Разбор
const draft = { updated: new Date(), tags: new Set(['html']), note: undefined }
JSON.parse(JSON.stringify(draft))
// { updated: '2026-10-24T…', tags: {} } — дата строкой, Set пустой, note пропал
structuredClone(draft)
// { updated: Date, tags: Set {'html'}, note: undefined }structuredClone использует тот же алгоритм, что postMessage и IndexedDB:
- сохраняет
Date,RegExp,Map,Set,ArrayBuffer,Blob, вложенность и циклические ссылки; - бросает ошибку на функциях и DOM-узлах;
- экземпляр класса превращается в обычный объект: методы из прототипа теряются.
Часто глубокая копия не нужна: при неизменяемых обновлениях ({ ...state, user: { ...state.user, name } }) копируется только изменённая ветка, и «откат» — это просто сохранённая ссылка на прежнее состояние.
Уточняющие вопросы
Что делает Object.freeze и защищает ли он вложенные объекты?
Запрещает менять свойства самого объекта. Заморозка поверхностная: вложенные объекты можно менять, если не заморозить их отдельно.
Когда JSON-копия всё-таки подходит?
Когда данные и так пришли из JSON и в них только строки, числа, булевы значения, массивы и простые объекты.
- Названо, что теряет JSON-копия.
structuredCloneсохраняет даты,Map,Setи циклы.- Названы ограничения
structuredClone: функции, DOM, прототипы. - Упомянуты неизменяемые обновления как альтернатива.
Почитать на сайте
Спецификация: HTML: structuredClone
Индикатор загрузки нужно показать перед тяжёлым расчётом, но он появляется только после расчёта. Почему не помогает Promise.resolve().then(calc) и что сработает?
Интервьюер может уточнить
- Что такое долгая задача?
- Почему бесконечная цепочка промисов может «повесить» страницу?
Разбор
Порядок в цикле событий: задача (обработчик клика) → все микрозадачи (промисы) → при необходимости отрисовка → следующая задача.
button.addEventListener('click', () => {
spinner.hidden = false
Promise.resolve().then(calc) // микрозадача: до отрисовки, спиннера не видно
})
button.addEventListener('click', () => {
spinner.hidden = false
requestAnimationFrame(() => setTimeout(calc)) // даём браузеру возможность показать кадр до расчёта
})requestAnimationFrame вызывается прямо перед отрисовкой кадра — сам расчёт в нём всё равно задержит этот кадр. Поэтому внутри откладывают ещё раз через setTimeout, и расчёт переносится в новую задачу. Это даёт возможность отрисовки, но не гарантирует показ кадра, например в скрытой вкладке.
Но интерфейс всё равно «замрёт» на время расчёта: кнопки не нажимаются, анимация спиннера стоит. Правильные решения:
- разбить расчёт на части и отдавать управление между ними (
await scheduler.yield()там, где поддерживается, илиsetTimeout); - вынести расчёт в Web Worker — тогда основной поток свободен.
Уточняющие вопросы
Что такое долгая задача?
Задача дольше 50 мс. В это время страница не отвечает на ввод; во вкладке Performance они отмечены красным.
Почему бесконечная цепочка промисов может «повесить» страницу?
Микрозадачи выполняются, пока очередь не опустеет. Если каждая добавляет новую, браузер так и не дойдёт до отрисовки и ввода.
- Отрисовка — между задачами, после очереди микрозадач.
thenвыполняется до отрисовки.- Названы
requestAnimationFrameсsetTimeoutилиscheduler.yield(). - Названы разбиение расчёта или Web Worker.
Почитать на сайте
Спецификация: HTML: цикл событий
Поле количества содержит строку. Почему parseInt("12px") даёт число, а Number("") — ноль? Как принять только целое количество от 1 до 99?
Интервьюер может уточнить
- Как получить число из
input type="number"? - Почему
isNaNиNumber.isNaNведут себя по-разному?
Разбор
function parseQuantity(raw) {
const text = raw.trim()
if (!text) return null
const value = Number(text)
return Number.isInteger(value) && value >= 1 && value <= 99
? value : null
}Этот контракт допускает запись 1e1 как 10. Если разрешены только десятичные цифры, до преобразования проверьте /^[0-9]+$/. Не исправляйте дробное количество округлением без согласованного правила продукта.
Уточняющие вопросы
Как получить число из input type="number"?
Через valueAsNumber; пустой или некорректный ввод даёт NaN. Ограничения поля проверяют через checkValidity().
Почему isNaN и Number.isNaN ведут себя по-разному?
Глобальная функция сначала преобразует значение к числу; Number.isNaN проверяет именно числовое значение NaN.
- Пустой ввод проверен до преобразования.
- Объяснено чтение префикса через
parseInt. - Проверены целочисленность и диапазон.
Почитать на сайте
Спецификация: Первоисточник: javascript
Поле ограничено двадцатью видимыми символами, но эмодзи расходует несколько единиц length. Что считает JavaScript и как считать символы для пользователя?
Интервьюер может уточнить
- Исправит ли проблему
[...text].length? - Для чего может понадобиться
normalize("NFC")?
Разбор
const segmenter = new Intl.Segmenter("ru", { granularity: "grapheme" })
const parts = [...segmenter.segment(text)]
const count = parts.length
const preview = parts.slice(0, 20).map(part => part.segment).join("")Составные эмодзи и буква с комбинируемым ударением показывают разницу между этими единицами. Лимит видимых символов и лимит байтов серверного хранилища — разные ограничения: договоритесь о контракте API и повторите проверку на сервере.
Уточняющие вопросы
Исправит ли проблему [...text].length?
Только для подсчёта кодовых точек. Последовательность из нескольких точек всё ещё может быть одной графемой.
Для чего может понадобиться normalize("NFC")?
Чтобы привести канонически эквивалентные последовательности к общей форме для сравнения. Это не заменяет сегментацию графем.
- Различены кодовые единицы, кодовые точки и графемы.
- Предложен
Intl.Segmenterсgranularity: "grapheme". - Согласованы пользовательский лимит и серверная проверка.
Напишите кодом
Спецификация: Первоисточник: javascript
Функция возвращает save() из try, но её catch не ловит отклонение промиса. Что изменит return await save()?
Интервьюер может уточнить
- Нужно ли писать
return awaitв любой обёртке? - Что произойдёт, если вернуть значение из
finally?
Разбор
async function persist() {
try {
return await save()
} catch (error) {
throw new Error("Не удалось сохранить", { cause: error })
} finally {
setBusy(false)
}
}Без await этот catch поймает только синхронную ошибку вызова save(), а finally выполнится до завершения возвращённого промиса. С await очистка происходит после завершения операции. Вызывающий код всё равно должен обработать итоговое отклонение.
Уточняющие вопросы
Нужно ли писать return await в любой обёртке?
Не ради локальной обработки, если её нет. Здесь он нужен для порядка catch и finally.
Что произойдёт, если вернуть значение из finally?
Оно заменит результат или подавит выброшенную ошибку. В очистке обычно не делают return.
- Различены возврат промиса и ожидание внутри
try. - Объяснён момент выполнения
finally. - Ошибка передана вызывающему коду с сохранением причины.
Напишите кодом
Почитать на сайте
Спецификация: Первоисточник: javascript
При каждом открытии панели добавляют обработчик, а при закрытии вызывают removeEventListener с новой стрелкой. Почему после трёх открытий действие выполняется трижды?
Интервьюер может уточнить
- Поможет ли повторный
save.bind(model)при снятии? - Нужно ли отменять таймеры при закрытии панели?
Разбор
const controller = new AbortController()
button.addEventListener("click", save, { signal: controller.signal })
// При закрытии панели:
controller.abort()При следующем открытии создают новый контроллер: отменённый сигнал повторно не активируется. При ручном снятии должны совпасть тип события, ссылка на функцию и значение capture. Флаг once удаляет слушатель после первого вызова, но не решает очистку панели, если события не было.
Уточняющие вопросы
Поможет ли повторный save.bind(model) при снятии?
Нет, каждый bind создаёт новую функцию; связанную функцию нужно сохранить заранее.
Нужно ли отменять таймеры при закрытии панели?
Да, если они больше не нужны: сигнал слушателя сам по себе не отменяет setTimeout или setInterval.
- Объяснена идентичность функции при снятии.
- Предложено сохранение ссылки или отмена через сигнал.
- Учтён новый контроллер при повторном открытии.
Напишите кодом
Почитать на сайте
Спецификация: Первоисточник: javascript
Ответ API приведён к User через as User. Проверит ли TypeScript, что name действительно строка? Как безопаснее принять внешние данные?
type User = { name: string };
const data = JSON.parse(source) as User;
console.log(data.name.toUpperCase());
Интервьюер может уточнить
- Как написать проверку, после которой TypeScript поверит типу?
- Чем
asотличается отsatisfies?
Разбор
as User только сообщает компилятору, каким типом считать выражение. Никакой проверки в браузере не появляется. Примите данные как unknown и проверьте структуру вручную или схемой: объект не null, name является строкой. После успешной проверки можно работать с User. Ошибка проверки должна приводить к понятному состоянию интерфейса, а не к вызову строкового метода у неизвестного значения.Уточняющие вопросы
Как написать проверку, после которой TypeScript поверит типу?
Функция-предикат: function isUser(x: unknown): x is User { … }. После if (isUser(data)) тип data сужается до User.
Чем as отличается от satisfies?
as заставляет компилятор принять тип. satisfies проверяет, что значение подходит под тип, и при этом сохраняет более точный выведенный тип.
- Утверждение типа не выполняет проверку во время работы.
- Внешние данные принимаются как
unknownи проверяются до использования. - Проверка отличает объект от
nullи подтверждает нужные свойства.
Спецификация: TypeScript: типы и утверждения типов
Как написать describe(value: unknown), возвращающую строку, если вход может быть строкой, null или объектом? Почему проверка typeof value === "object" недостаточна?
Интервьюер может уточнить
- Чем
unknownлучшеany? - Что такое
never?
Разбор
typeof value === "string", затем value === null. В объектной ветке проверьте ненулевое значение и существование нужного свойства, после этого его тип. unknown заставляет явно доказать допустимость операции, тогда как any пропускает её без проверки. Проверка if (value) не годится для всех данных: пустая строка и ноль могут быть корректными значениями.Уточняющие вопросы
Чем unknown лучше any?
any отключает проверки: с ним можно сделать что угодно, и ошибка всплывёт во время работы. unknown заставляет сузить тип перед использованием.
Что такое never?
Тип значения, которого не бывает. Его используют для проверки полноты switch: если какой-то вариант не обработан, присваивание в never даст ошибку компиляции.
unknownтребует сужения перед доступом к свойствам.typeof nullравенobject;nullпроверяется отдельно.- Для свойства проверяется наличие и тип, а не только истинность значения.
Спецификация: TypeScript: сужение типов
Почему для загрузки данных удобнее объединение ниже, чем объект с optional data и error? Как безопасно прочитать data?
type State =
| { status: "loading" }
| { status: "success"; data: string[] }
| { status: "error"; error: string };
Интервьюер может уточнить
- Как называется такой приём?
- Как не забыть обработать новое состояние?
Разбор
state.status === "success" TypeScript знает, что data существует. Вариант loading не требует фиктивных данных, а error содержит причину ошибки. Это уменьшает число невозможных сочетаний вроде success без data. При расширении объединения можно проверять полноту switch через never. Такая модель описывает состояния, но сама не выполняет запрос и не проверяет ответ сервера.Уточняющие вопросы
Как называется такой приём?
Размеченное (дискриминированное) объединение: общее поле-литерал, по которому TypeScript различает варианты.
Как не забыть обработать новое состояние?
В конце switch присвоить значение переменной типа never: если добавится вариант, компилятор укажет на необработанный случай.
- Литерал
statusразличает варианты и сужает тип. dataдоступно вsuccess,errorвerror.- Нельзя создать противоречивый вариант без обхода проверки типов.
Спецификация: TypeScript: сужение типов
Почему readonly view меняется после items.push()? Что именно гарантирует readonly и замораживает ли объект?
const items = [1];
const view: readonly number[] = items;
items.push(2);
console.log(view);
Интервьюер может уточнить
- Чем
readonlyотличается отas const? - Как получить неизменяемость во время работы?
Разбор
view показывает тот же массив, поэтому после push содержит оба числа. TypeScript запретил бы view.push(), но не изменение через исходную mutable-ссылку. readonly не выполняет Object.freeze() и не создаёт копию. Даже заморозка объекта в JavaScript по умолчанию поверхностная. Для изоляции выбирайте копирование нужной структуры или дисциплину неизменяемых обновлений.Уточняющие вопросы
Чем readonly отличается от as const?
as const сохраняет литеральные типы, делает свойства объектного литерала доступными только для чтения, а массивы-литералы — readonly-кортежами. Это не глубокая заморозка: ранее созданный массив в const box = { items } as const по-прежнему можно менять через box.items.push(...).
Как получить неизменяемость во время работы?
Object.freeze — поверхностная заморозка, или копирование при каждом изменении. TypeScript сам этого не делает.
readonlyограничивает операции через типизированную ссылку, не замораживая объект.itemsиviewссылаются на один массив.- Глубокая неизменность не возникает автоматически; нужна отдельная стратегия.
Спецификация: TypeScript: свойства объектов и readonly
Зачем в getProperty связь K extends keyof T? Чем этот вариант полезнее функции с ключом string и результатом any?
function getProperty<T, K extends keyof T>(value: T, key: K): T[K] {
return value[key];
}
Интервьюер может уточнить
- Что вернёт
keyofдля{ id: number; title: string }? - Когда generic лишний?
Разбор
getProperty({ count: 3, title: "A" }, "count") результат имеет тип number; ключ "missing" будет ошибкой типов. Generic сохраняет связь между входом, ключом и результатом, которую any стирает. Для объекта из внешнего источника всё равно нужна проверка данных. Обобщение полезно, когда выражает такую связь, а не просто заменяет конкретное имя типа буквой.Уточняющие вопросы
Что вернёт keyof для { id: number; title: string }?
Объединение литералов 'id' | 'title'.
Когда generic лишний?
Если параметр типа используется один раз и не связывает вход с выходом. Тогда достаточно обычного типа.
- Ключ ограничен ключами переданного типа.
- Тип результата
T[K]связан с конкретным выбранным ключом. - Generic не проверяет произвольный JSON во время выполнения.
Спецификация: TypeScript: обобщённые функции
Зачем TypeScript, если в браузере всё равно выполняется JavaScript? Какие ошибки он поймает, а какие нет?
Интервьюер может уточнить
- Что делает настройка
strict? - Какие у TypeScript минусы?
Разбор
TypeScript — это JavaScript с описанием типов. Компилятор проверяет, что код использует значения правильно, и выдаёт обычный JavaScript без типов.
Что он ловит:
- опечатки:
user.nmaeвместоuser.name; - неверные вызовы: строковый метод у числа, функция с недостающим аргументом;
- забытые проверки: обращение к свойству значения, которое может быть
undefined(при включённомstrict); - последствия изменений: переименовали поле — компилятор покажет все места, которые нужно поправить.
Что не ловит: данные из внешнего мира — ответ API, localStorage, ввод пользователя — приходят уже во время работы, и тип для них — только обещание. Логические ошибки, например сложение вместо вычитания, тоже остаются. Поэтому TypeScript не заменяет проверку данных и тесты.
Уточняющие вопросы
Что делает настройка strict?
Включает строгие проверки, главное — strictNullChecks: null и undefined перестают подходить к любому типу, и компилятор требует их обработать.
Какие у TypeScript минусы?
Нужен шаг сборки, типы приходится писать и поддерживать, сложные типы трудно читать. Для маленького скрипта на одну страницу это может быть лишним.
- Проверка типов происходит до запуска, после компиляции типов нет.
- Назван хотя бы один пример ошибки, которую ловит TypeScript.
- Внешние данные и логические ошибки TypeScript не проверяет.
- Названа польза для редактора или рефакторинга.
Спецификация: TypeScript Handbook: основы
Чем interface отличается от type? Что выберете для описания пользователя, а что — для статуса загрузки 'idle' | 'loading' | 'error'?
Интервьюер может уточнить
- Как описать администратора, у которого есть всё, что у пользователя, плюс список прав?
- Где пригодится слияние интерфейсов?
Разбор
Пользователя можно описать любым способом:
interface User { id: number; name: string }
type User = { id: number; name: string }Отличия:
typeможет назвать что угодно: объединение'idle' | 'loading' | 'error', кортеж, функцию.interface— только форму объекта;interfaceрасширяют черезextends,type— через пересечение&;- два
interfaceс одним именем сливаются в один. Это нужно для дополнения чужих типов, но в обычном коде может удивить.
На практике в команде договариваются о правиле, например «объекты — interface, остальное — type», и придерживаются его.
Уточняющие вопросы
Как описать администратора, у которого есть всё, что у пользователя, плюс список прав?
interface Admin extends User { permissions: string[] } или type Admin = User & { permissions: string[] }.
Где пригодится слияние интерфейсов?
Чтобы дописать свойство в чужой тип, например добавить своё поле в глобальный Window.
- Для объекта подходят оба способа.
typeописывает объединения и другие типы,interface— только объекты.- Названо расширение:
extendsу интерфейса и&у типа. - Статус загрузки описан через
type.
Спецификация: TypeScript Handbook: type и interface
Функция принимает id: string | number. Почему нельзя сразу вызвать id.toUpperCase() и как это исправить?
Интервьюер может уточнить
- Как сузить
string[] | string? - Что такое объединение строковых литералов?
Разбор
string | number значит «или строка, или число». Пока TypeScript не знает, что именно пришло, он разрешает только общие операции, например id.toString().
function format(id: string | number) {
if (typeof id === 'string') {
return id.toUpperCase() // здесь id: string
}
return id.toFixed(0) // здесь id: number
}Такие проверки называют сужением типа. Кроме typeof сужают проверка на null, Array.isArray(), оператор in и сравнение общего поля, например state.status === 'error'.
Обходить проверку через id as string плохо: если придёт число, программа упадёт во время работы, а компилятор промолчит.
Уточняющие вопросы
Как сузить string[] | string?
Через Array.isArray(value): в ветке «да» это массив строк, в ветке «нет» — строка.
Что такое объединение строковых литералов?
Тип вроде 'small' | 'medium' | 'large': подходит только одна из этих строк. Опечатку 'midle' компилятор не пропустит.
- Для объединения доступны только общие операции.
- Тип сужают проверкой, например
typeof id === 'string'. - Внутри ветки TypeScript знает точный тип.
Спецификация: TypeScript Handbook: сужение типов
Есть тип User с полями id, name, email и password. Как без копирования полей получить тип для формы редактирования, где всё необязательно, и тип для ответа API без пароля?
Интервьюер может уточнить
- Как описать словарь «код страны → название»?
- Защитит ли
Omitот того, что сервер всё-таки пришлёт пароль?
Разбор
type User = { id: number; name: string; email: string; password: string }
type UserEdit = Partial<Omit<User, 'id' | 'password'>>
type PublicUser = Omit<User, 'password'>
type UserPreview = Pick<User, 'id' | 'name'>Частые утилиты:
Partial<T>— все поля необязательные,Required<T>— все обязательные;Pick<T, K>— только перечисленные поля,Omit<T, K>— все, кроме перечисленных;Readonly<T>— поля только для чтения;Record<K, V>— объект с ключамиKи значениямиV, например словарь.
Главная польза — один источник правды: тип User описан один раз, а производные типы следуют за ним.
Уточняющие вопросы
Как описать словарь «код страны → название»?
Record<string, string>, а если коды известны заранее — Record<'ru' | 'en', string>.
Защитит ли Omit от того, что сервер всё-таки пришлёт пароль?
Нет. Это только описание типа для компилятора; лишнее поле придёт и останется в объекте. Убирать пароль из ответа должен сервер.
- Назван
Partialдля необязательных полей. - Назван
Omit(илиPick) для типа без пароля. - Производные типы обновляются вместе с исходным.
- Утилиты работают только на уровне типов.
Напишите кодом
Спецификация: TypeScript Handbook: утилиты типов
Как описать типы для функции formatPrice(price, currency), где валюта необязательна, и для колбэка onSelect, который получает выбранный товар и ничего не возвращает?
Интервьюер может уточнить
- Как типизировать функцию с любым числом аргументов?
- Чем
voidотличается отundefinedв типе результата?
Разбор
function formatPrice(price: number, currency: string = 'RUB'): string {
return price.toFixed(2) + ' ' + currency
}
type Props = {
items: Product[]
onSelect: (item: Product) => void
}- тип параметра пишут после двоеточия, тип результата — после скобок;
- необязательный параметр —
currency?: string(тогда внутри он может бытьundefined) или со значением по умолчанию: тип выведется сам; - необязательные параметры идут после обязательных;
void— результат не используется. Функция, которая что-то возвращает, всё равно подойдёт как колбэк сvoid;- тип результата часто можно не писать — TypeScript выведет его, — но в публичных функциях его указывают явно, чтобы ошибка внутри не изменила контракт незаметно.
Уточняющие вопросы
Как типизировать функцию с любым числом аргументов?
Через rest-параметр: function sum(...nums: number[]): number.
Чем void отличается от undefined в типе результата?
undefined требует вернуть именно undefined. void говорит «результат не важен», поэтому колбэк вроде items.push, который возвращает число, тоже подходит.
- Типы параметров и результата указаны в объявлении.
- Необязательный параметр через
?или значение по умолчанию. - Колбэк описан типом функции
(item: Product) => void.
Спецификация: TypeScript Handbook: функции
Для статусов заказа можно написать enum Status { New, Paid, Sent } или type Status = 'new' | 'paid' | 'sent'. Чем они отличаются и что выбрать?
Интервьюер может уточнить
- Что делает
as const? - Как проверить, что в
switchобработаны все статусы?
Разбор
Объединение литералов — 'new' | 'paid' | 'sent': компилятор не пропустит опечатку, редактор подскажет варианты, а в данных лежат понятные строки. Кода в JavaScript не добавляется.
enum — конструкция, которая превращается в объект:
- числовой
enumпо умолчанию нумерует с нуля: в базе и API появляются 0, 1, 2, и перестановка пунктов меняет смысл старых данных; - строковый
enum Status { New = 'new' }безопаснее, но всё равно добавляет код и требует импортаStatus.Newвместо строки; - часть инструментов, которые просто убирают типы, не поддерживает
enum.
Если нужен и тип, и список значений, например для выпадающего списка:
const STATUSES = ['new', 'paid', 'sent'] as const
type Status = (typeof STATUSES)[number] // 'new' | 'paid' | 'sent'Уточняющие вопросы
Что делает as const?
Делает значения максимально точными: массив становится кортежем только для чтения из конкретных строк, а не string[].
Как проверить, что в switch обработаны все статусы?
В ветке default присвоить значение переменной типа never: новый статус вызовет ошибку компиляции.
- Объединение литералов существует только в типах.
enumсоздаёт объект в JavaScript.- Названа проблема числового
enumили лишнего кода. - Назван выбор: объединение строк или
as const.
Спецификация: TypeScript Handbook: enum
Напишите тип для функции sortBy(items, key), которая сортирует массив объектов по полю. Как не дать передать несуществующее поле или поле-объект?
Интервьюер может уточнить
- Почему функция копирует массив перед сортировкой?
- Как сделать порядок по убыванию параметром?
Разбор
function sortBy<T extends Record<K, string | number>, K extends keyof T>(
items: T[],
key: K,
): T[] {
return [...items].sort((a, b) => (a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0))
}
sortBy(products, 'price') // можно
sortBy(products, 'prise') // ошибка: нет такого поля
sortBy(products, 'tags') // ошибка: tags — массивextends в параметре типа — ограничение: «подойдёт любой тип, который совместим с этим». K extends keyof T связывает ключ с элементом, а T extends Record<K, string | number> требует, чтобы по этому ключу лежало сравнимое значение.
Параметры типа обычно не пишут при вызове: TypeScript выводит их из аргументов. Параметру можно дать значение по умолчанию: <T = unknown>. Хороший признак нужного generic — он связывает вход и выход; если параметр используется один раз, достаточно обычного типа.
Уточняющие вопросы
Почему функция копирует массив перед сортировкой?
sort меняет исходный массив. Копия [...items] или toSorted() не трогает данные вызывающего кода.
Как сделать порядок по убыванию параметром?
Добавить direction: 'asc' | 'desc' = 'asc' и умножать результат сравнения на -1 для desc.
- Назван
K extends keyof T. - Ограничено значение по ключу: строки и числа.
- Объяснено, что параметры типа выводятся из аргументов.
Спецификация: TypeScript Handbook: ограничения generic
Как устроены встроенные Partial<T> и ReturnType<F>? Напишите свой тип Nullable<T>, где каждое поле может быть null.
Интервьюер может уточнить
- Как получить тип элемента массива?
- Как достать тип значения, которое возвращает промис?
Разбор
type MyPartial<T> = { [K in keyof T]?: T[K] }
type Nullable<T> = { [K in keyof T]: T[K] | null }
type MyReturnType<F> = F extends (...args: any[]) => infer R ? R : neverСопоставленный тип строит новый объектный тип, перебирая ключи. Модификаторы ? и readonly можно добавлять и снимать: -? делает поля обязательными, так устроен Required. Через as ключи можно переименовать: [K in keyof T as `get${Capitalize<K & string>}`].
Условный тип A extends B ? X : Y выбирает тип по совместимости. infer внутри условия объявляет переменную типа, которую TypeScript находит сам. Распределение по объединению происходит, если слева от extends стоит непосредственно параметр типа, например T extends U ? X : Y: так работает Exclude<'a' | 'b', 'a'>.
Такие типы полезны в библиотеках и общих утилитах. В обычном коде лучше простые явные типы: сложную конструкцию трудно читать и отлаживать.
Уточняющие вопросы
Как получить тип элемента массива?
type Item<A> = A extends readonly (infer U)[] ? U : never. Для индексного доступа требуется ограничение: type Item<A extends readonly unknown[]> = A[number].
Как достать тип значения, которое возвращает промис?
Встроенный Awaited<T>, например Awaited<ReturnType<typeof loadUser>>.
- Объяснён сопоставленный тип через
[K in keyof T]. - Объяснён условный тип и
infer. - Написан
Nullable<T>.
Спецификация: TypeScript Handbook: сопоставленные типы
Функция parse(input) возвращает число, если пришла строка, и массив чисел, если пришёл массив строк. Как описать это, чтобы результат вызова имел точный тип?
Интервьюер может уточнить
- Проверяет ли TypeScript реализацию на соответствие каждой перегрузке?
- Почему не
parse(input: any): any?
Разбор
function parse(input: string): number
function parse(input: string[]): number[]
function parse(input: string | string[]): number | number[] {
return Array.isArray(input) ? input.map(Number) : Number(input)
}
const a = parse('5') // number
const b = parse(['1', '2']) // number[]Снаружи видны только сигнатуры перегрузок, реализация должна быть совместима со всеми. TypeScript выбирает первую подходящую перегрузку сверху вниз, поэтому более конкретные пишут раньше.
Тот же результат через generic:
function parse<T extends string | string[]>(input: T): T extends string ? number : number[]Но внутри такой функции часто нужны приведения типов. Перегрузки проще читать, когда вариантов два-три.
Если тип результата не зависит от входа, перегрузки не нужны: хватит объединения на входе.
Уточняющие вопросы
Проверяет ли TypeScript реализацию на соответствие каждой перегрузке?
Только на совместимость с объединением сигнатур. Вернуть массив для строки компилятор внутри реализации не запретит, поэтому реализацию нужно проверять тестами.
Почему не parse(input: any): any?
Тогда пропадут и проверка аргумента, и тип результата: ошибки всплывут во время работы.
- Названы сигнатуры перегрузок и одна реализация.
- Результат вызова получает точный тип.
- Названа альтернатива: generic с условным типом.
Спецификация: TypeScript Handbook: перегрузки функций
Нужно ли писать тип у каждой переменной? В каком из примеров TypeScript выведет тип сам, а где его стоит указать явно: const count = 0, let items = [], параметр функции (user) => user.name?
Интервьюер может уточнить
- Почему
const status = 'idle'иlet status = 'idle'получают разные типы? - Что такое «неявный any»?
Разбор
TypeScript выводит типы из значений, и писать их везде не нужно — это шум.
const count = 0— тип0(литерал, потому чтоconst),let count = 0—number. Тип писать не надо;let items = []— вывод зависит от контекста и настроек: локальный массив может уточняться по добавленным элементам. Чтобы явно задать контракт, напишите:let items: Product[] = [];- параметры функции ниоткуда не выводятся:
function greet(user: User). Без типа приstrictбудет ошибка «неявный any»; - колбэк получает тип из контекста: в
users.map((user) => …)параметр ужеUser, еслиusers: User[].
Явный тип полезен на границах: параметры, результат экспортируемых функций, данные из API, состояние, которое начинается с null: let selected: User | null = null. Внутри функции доверяйте выводу и смотрите тип наведением мыши в редакторе.
Уточняющие вопросы
Почему const status = 'idle' и let status = 'idle' получают разные типы?
const нельзя переприсвоить, поэтому тип — точный литерал 'idle'. let можно изменить на любую строку, поэтому тип расширяется до string.
Что такое «неявный any»?
Когда тип вывести нельзя, TypeScript подставляет any. Настройка noImplicitAny (часть strict) превращает это в ошибку.
- Тип переменной с начальным значением выводится сам.
- Для пустого массива предложен явный тип элементов, если нужен заранее заданный контракт.
- Параметры функций типизируют явно, колбэки получают тип из контекста.
Спецификация: TypeScript Handbook: вывод типов
Компилятор пишет: «Object is possibly null» на строке document.querySelector('.menu').classList.add('open'). Что это значит и какие есть способы исправить? Почему ! — не лучший?
Интервьюер может уточнить
- Как получить элемент сразу нужного типа, например поле ввода?
- Чем
??отличается от||?
Разбор
При включённом strictNullChecks null и undefined не подходят к другим типам, и TypeScript требует разобраться с ними явно.
const menu = document.querySelector('.menu')
if (menu) menu.classList.add('open') // проверка
menu?.classList.add('open') // ничего не делать, если нет
if (!menu) throw new Error('Нет .menu') // упасть сразу и понятно
document.querySelector('.menu')!.classList // «точно есть» — без проверкиКакой выбрать — зависит от смысла. Если меню может отсутствовать на части страниц — ?. или if. Если без него код не может работать — проверка с понятной ошибкой: сообщение «Нет .menu» найти легче, чем «Cannot read properties of null».
Оператор ! (non-null assertion) убирает null из типа без проверки. Если элемента не окажется, ошибка всё равно будет — только во время работы и без подсказки.
Уточняющие вопросы
Как получить элемент сразу нужного типа, например поле ввода?
document.querySelector<HTMLInputElement>('#email') задаёт ожидаемый тип, но не проверяет тег во время работы. Для проверки используйте element instanceof HTMLInputElement; она также исключает null.
Чем ?? отличается от ||?
?? подставляет значение только вместо null и undefined, а || — вместо любого ложного, включая 0 и пустую строку.
querySelectorможет вернутьnull, компилятор требует это учесть.- Названы проверка
if,?.или понятная ошибка. !ничего не проверяет во время работы.
Почитать на сайте
Спецификация: TypeScript Handbook: null и undefined
Подключили старую библиотеку на JavaScript, и TypeScript пишет: «Could not find a declaration file for module». Что такое файлы объявлений .d.ts и как решить проблему?
Интервьюер может уточнить
- Что делает
skipLibCheck? - Как подобрать версию
@typesк библиотеке?
Разбор
TypeScript проверяет код по типам. Для библиотеки на JavaScript типы берутся из файлов объявлений:
- из самого пакета — поле
typesв егоpackage.json; - из пакета DefinitelyTyped:
npm install -D @types/lodash; - из вашего файла, например
src/types/old-lib.d.ts:
declare module 'old-lib' {
export function format(value: number, options?: { currency?: string }): string
}Описывают только то, что используется, — постепенно. Временное declare module 'old-lib' без тела делает все импорты any: компиляция пройдёт, но проверки нет.
Тем же способом описывают глобальные значения, которые подключены скриптом (declare const ymaps: …), и импорт файлов, которые понимает сборщик: declare module '*.svg' { const url: string; export default url }. Дополнить чужой тип, например Window, можно через слияние интерфейсов в declare global.
Уточняющие вопросы
Что делает skipLibCheck?
skipLibCheck пропускает проверку согласованности всех файлов объявлений .d.ts, в том числе локальных. Типы из них всё ещё используются для проверки вашего кода. Это может скрыть проблемы объявлений, а не исправить их.
Как подобрать версию @types к библиотеке?
Нужна версия типов, совместимая с API установленной библиотеки; номера патчей не обязаны совпадать. Проверьте документацию пакета и используемые функции: старые объявления могут описывать уже удалённые методы.
.d.tsописывает типы без кода.- Названы типы в пакете или
@types. - Названо своё объявление
declare module.
Спецификация: TypeScript Handbook: файлы объявлений
Есть объект настроек тем const themes = { light: {…}, dark: {…} }. Как проверить, что каждая тема соответствует типу Theme, и при этом сохранить точные ключи 'light' | 'dark'? Чем satisfies отличается от аннотации типа и as?
Интервьюер может уточнить
- Где ещё полезен
satisfies? - Можно ли
satisfiesиспользовать для данных из API?
Разбор
type Theme = { text: string; page: string }
const themes = {
light: { text: "#222", page: "#fff" },
dark: { text: "#eee", page: "#1b1d21" },
} satisfies Record<string, Theme>
type ThemeName = keyof typeof themes // "light" | "dark"
// themes.sepia — ошибка: такого ключа нет
const dictionary: Record<string, Theme> = themes
// dictionary.sepia допустим как обращение к ключу словаряПри noUncheckedIndexedAccess результат неизвестного ключа словаря дополнительно включает undefined. Сам словарь не создаёт отсутствующие значения.
as T может скрыть недостающие поля, но компилятор всё же отвергает явно несовместимые утверждения. satisfies T проверяет соответствие без замены результата на общий тип T; контекстная типизация при этом может влиять на вывод. Сохранение всех литералов не гарантируется: для литеральной конфигурации можно использовать as const satisfies Record<string, Theme>. Ни один из этих операторов не проверяет ответ API во время работы.
Уточняющие вопросы
Где ещё полезен satisfies?
В таблицах маршрутов, переводах и конфигурациях: проверить форму и при этом получить из объекта точный тип ключей для автодополнения.
Можно ли satisfies использовать для данных из API?
Нет, он работает только на этапе компиляции для значений в коде. Данные из сети проверяют во время работы.
satisfiesпроверяет соответствие без замены типа объекта общим типом словаря.- Аннотация словаря расширяет ключи; утверждение
asне заменяет полную проверку структуры. - Получены точные ключи через
keyof typeof.
Спецификация: TypeScript 4.9: оператор satisfies
Функция читает users[0].name, но массив иногда пуст. Почему тип User[] сам по себе не гарантирует первый элемент и как защитить обращение?
Интервьюер может уточнить
- Что будет при чтении неизвестного ключа словаря?
- Почему одного типа кортежа недостаточно для ответа API?
Разбор
const first = users[0]
if (!first) return "Пользователей нет"
return first.nameНастройка noUncheckedIndexedAccess не входит автоматически в strict. Если непустота действительно гарантирована контрактом, её можно выразить кортежем [User, ...User[]], но входящие данные всё равно проверяют во время работы. users[0]! лишь скрывает потенциальную ошибку от компилятора.
Уточняющие вопросы
Что будет при чтении неизвестного ключа словаря?
Значение может отсутствовать; эта настройка добавит undefined и к чтению через индексную сигнатуру.
Почему одного типа кортежа недостаточно для ответа API?
Тип не проверяет полученный JSON; нужно подтвердить, что массив непустой и его элементы корректны.
- Названа возможность отсутствующего элемента.
- Предложена проверка перед обращением к свойству.
- Названа настройка
noUncheckedIndexedAccessи её отличие отstrict.
Спецификация: Первоисточник: typescript
Почему объект с лишним полем иногда принимается функцией, хотя такой же литерал прямо в вызове даёт ошибку? Удаляет ли тип лишние поля?
Интервьюер может уточнить
- Сделает ли присваивание
const publicUser: Label = userкопию? - Как гарантировать форму внешнего JSON?
Разбор
type Label = { name: string }
function show(value: Label) { return value.name }
const user = { name: "Ира", password: "secret" }
show(user) // совместим по структуре
// show({ name: "Ира", password: "secret" }) — лишнее полеДля исходной конфигурации полезен satisfies на литерале. Чтобы отправить только публичные поля, создают новый объект с явным списком свойств; аннотация Label и Omit не очищают данные.
Уточняющие вопросы
Сделает ли присваивание const publicUser: Label = user копию?
Нет, это тот же объект с теми же полями во время работы.
Как гарантировать форму внешнего JSON?
Проверить и преобразовать данные во время работы, например схемой с явным поведением для неизвестных полей.
- Объяснена структурная совместимость.
- Отмечена дополнительная проверка свежих литералов.
- Типизация отличена от удаления полей.
Спецификация: Первоисточник: typescript
В настройках theme?: string отсутствие свойства означает «сохранить выбор». Чем это отличается от theme: string | undefined и что меняет exactOptionalPropertyTypes?
Интервьюер может уточнить
- Включается ли эта настройка вместе с
strict? - Что сделает
JSON.stringifyс объектным свойствомundefined?
Разбор
Для type Options = { theme?: string } объект {} допустим. При включённой настройке { theme: undefined } уже не подходит. Если оба состояния нужны, пишут theme?: string | undefined.
При чтении необязательного свойства всё равно возможен undefined. Во время работы Object.hasOwn(options, "theme") различает отсутствующий ключ и существующий ключ со значением undefined. Контракт PATCH или функции обновления должен явно определять эти состояния.
Уточняющие вопросы
Включается ли эта настройка вместе с strict?
Нет, её включают отдельно; требуется strictNullChecks.
Что сделает JSON.stringify с объектным свойством undefined?
Пропустит его. Для явного сброса по сети часто используют null, если это предусмотрено контрактом API.
- Различены отсутствие ключа и значение
undefined. - Объяснена роль
exactOptionalPropertyTypes. - Учтено поведение при чтении и передаче в JSON.
Спецификация: Первоисточник: typescript
В файле нужен только тип User из другого модуля. Зачем писать import type и можно ли затем использовать User в instanceof?
Интервьюер может уточнить
- Можно ли импортировать тип класса через
import type? - Как импортировать модуль ради регистрации обработчиков?
Разбор
import type { User } from "./types"
export function displayName(user: User): string {
return user.name
}Если модуль экспортирует класс и код создаёт экземпляр через new или проверяет instanceof, нужен обычный импорт значения. Импорт типа сам по себе не запускает побочные эффекты модуля. При verbatimModuleSyntax различие между импортом типа и значения особенно важно: оно задаётся явно.
Уточняющие вопросы
Можно ли импортировать тип класса через import type?
Да, если класс используется лишь как тип, а не как конструктор или другое значение.
Как импортировать модуль ради регистрации обработчиков?
Явным импортом для побочного эффекта, например import "./register"; импорт типа его не заменяет.
- Импорт типа удаляется из выходного JavaScript.
- Для
instanceofтребуется значение-конструктор. - Импорт типа не гарантирует выполнение модуля.
Спецификация: Первоисточник: typescript
Сервер вернул HTTP 404, но catch не сработал. Почему? Какие ошибки нужно обработать при получении JSON?
Интервьюер может уточнить
- Как сделать тайм-аут для
fetch? - Почему сервер с ошибкой 500 отдаёт HTML вместо JSON?
Разбор
response.ok до обработки ожидаемых данных. Затем await response.json() тоже может завершиться ошибкой. Сетевой отказ и отмену обрабатывают отдельно от HTTP-статуса. Успешный HTTP-ответ ещё не гарантирует правильную структуру JSON: её проверяет приложение.Уточняющие вопросы
Как сделать тайм-аут для fetch?
Передать signal: AbortSignal.timeout(5000). По истечении времени промис отклонится с ошибкой TimeoutError.
Почему сервер с ошибкой 500 отдаёт HTML вместо JSON?
Страницу ошибки часто формирует веб-сервер или прокси. Поэтому при !response.ok не стоит сразу вызывать json() — можно проверить заголовок Content-Type.
fetchне отклоняет Promise только из-за HTTP 404/500.- Проверяется
response.okили ожидаемыйstatus. - Отдельно возможны сетевой отказ, отмена и ошибка разбора JSON.
Спецификация: WHATWG Fetch: ответы и запросы
Где в браузере можно хранить данные: localStorage, sessionStorage и cookie? Что выбрать, чтобы запомнить тему оформления, и можно ли хранить там токен авторизации?
Интервьюер может уточнить
- Как узнать, что значение изменили в другой вкладке?
- Почему не стоит хранить в cookie много данных?
Разбор
localStorage— около 5 МБ на сайт, только строки, сохраняется между сеансами, но может быть удалён пользователем или браузером. Доступен только из JavaScript, на сервер сам не отправляется.sessionStorage— то же, но живёт в пределах одной вкладки и удаляется при её закрытии.- cookie — до 4 КБ, браузер отправляет их на сервер с подходящими запросами с учётом домена, пути,
Secure,SameSiteи режима credentials. Сервер может поставить флагHttpOnly, и тогда JavaScript их не прочитает.
Объекты сохраняют через JSON.stringify и читают через JSON.parse. Обращения к хранилищу оборачивают в try/catch: в приватном режиме или при заполненной квоте оно может выбросить ошибку.
Тема оформления, свёрнутые панели, черновик — localStorage. Токен авторизации — вопрос безопасности: если на странице есть XSS-уязвимость, чужой скрипт прочитает localStorage. Cookie с HttpOnly скрипт не прочитает, а Secure и SameSite ограничивают, когда она отправляется.
Уточняющие вопросы
Как узнать, что значение изменили в другой вкладке?
Событие storage у window приходит в другие документы того же источника, использующие это хранилище. В документе, который изменил localStorage, оно не срабатывает.
Почему не стоит хранить в cookie много данных?
Каждая cookie отправляется с каждым запросом к сайту, включая картинки и стили. Большие cookie замедляют все запросы.
localStorage— бессрочно,sessionStorage— до закрытия вкладки.- Cookie отправляются на сервер с каждым запросом.
- Хранятся строки, объекты через JSON.
- Тема — в
localStorage. - Токен в
localStorageуязвим при XSS, названа cookie сHttpOnly.
Почитать на сайте
Спецификация: HTML: Web Storage
Поиск отправил запросы «ко» и «кот». Ответ на «ко» пришёл позже и затёр свежие результаты. Как предотвратить это, включая ошибки и индикатор загрузки?
Интервьюер может уточнить
- Чем debounce отличается от throttle?
- Как не показать ошибку от отменённого запроса?
Разбор
loading сравнивайте его с текущим. Можно дополнительно отменять старый fetch через AbortController, но проверка актуальности защищает и обработку уже полученного ответа. Debounce сокращает число запросов, но не задаёт порядок их завершения. Скрывайте ошибки отменённых поисков, сохраняя ошибки актуального.Уточняющие вопросы
Чем debounce отличается от throttle?
Debounce вызывает функцию после паузы во вводе, throttle — не чаще заданного интервала. Для поиска при вводе обычно берут debounce.
Как не показать ошибку от отменённого запроса?
Отмена отклоняет fetch с AbortError: в catch его пропускают, а ошибки показывают только для актуального запроса.
- Каждому поиску назначается версия; DOM обновляет только актуальная версия.
- Предыдущий запрос можно отменять через
AbortController. - Проверка актуальности применяется также в
catch/finally; debounce сам гонку не устраняет.
Спецификация: WHATWG DOM: отмена операций
API работает из серверного скрипта, но запрос страницы блокируется CORS. Почему mode: "no-cors" обычно не исправляет получение JSON?
Интервьюер может уточнить
- Что такое предварительный запрос (preflight)?
- Как обойти CORS при разработке?
Разбор
no-cors не открывает доступ к JSON: ответ непрозрачен для JavaScript. Некоторые запросы отправляются даже без разрешения прочитать ответ, а другие требуют предварительной проверки. Поэтому CORS не является общей защитой сервера от отправки запросов.Уточняющие вопросы
Что такое предварительный запрос (preflight)?
Перед «непростым» запросом — с методом PUT или DELETE, своими заголовками или JSON-телом — браузер отправляет OPTIONS и спрашивает разрешения у сервера.
Как обойти CORS при разработке?
Настроить прокси в dev-сервере, чтобы запросы шли с того же источника, или разрешить нужный origin на сервере API. Отключать проверки в браузере — не решение.
- CORS регулирует доступ браузерного JavaScript к ответу другого origin.
no-corsдаёт opaque-ответ без доступного JS тела.- Разрешение задаётся сервером; CORS не заменяет авторизацию и защиту от CSRF.
Спецификация: WHATWG Fetch: ответы и запросы
Чем различаются Cache-Control: no-cache и no-store? Какую роль играют ETag и ответ 304?
Интервьюер может уточнить
- Как кешировать стили и скрипты надолго, но сразу обновлять при выкладке?
- Что значит
private?
Разбор
no-cache не означает «не сохранять»: сохранённый ответ нужно валидировать перед использованием. С ETag клиент может отправить If-None-Match и получить 304, если представление не изменилось. no-store запрещает хранение ответа; он не является командой удаления всех ранее сохранённых данных. Для персональных ответов важно также различать private и общие кеши.Уточняющие вопросы
Как кешировать стили и скрипты надолго, но сразу обновлять при выкладке?
Добавлять хеш содержимого в имя файла (app.3f9a.js) и отдавать Cache-Control: max-age=31536000, immutable. Новая версия получает новое имя, а HTML кешируют коротко.
Что значит private?
Ответ можно хранить только в браузере пользователя, но не в общих кешах вроде CDN и прокси. Это нужно для персональных данных.
no-cacheдопускает хранение, но требует проверки перед повторным использованием.no-storeзапрещает хранить этот ответ в кеше.ETagучаствует в условном запросе; 304 позволяет использовать имеющееся тело.
Спецификация: RFC 9111: кеширование HTTP
Нужно показать имя пользователя из API. Напишите безопасный код вывода обычного текста в элемент nameElement. Объясните, почему innerHTML здесь опасен. Что меняется, если продукту действительно нужна разметка?
Интервьюер может уточнить
- Чем
textContentотличается отinnerText? - Как ещё защититься от XSS?
Разбор
textContent, если нужна строка имени: символы разметки останутся текстом. innerHTML меняет структуру DOM и создаёт возможность XSS при недоверенном вводе. Если необходим форматированный HTML, применяйте поддерживаемый санитайзер с явной политикой разрешений. Получение строки от своего API не делает её автоматически доверенной: API может возвращать пользовательский контент.Уточняющие вопросы
Чем textContent отличается от innerText?
textContent просто возвращает и задаёт текст. innerText учитывает CSS: пропускает скрытый текст и вызывает пересчёт раскладки, поэтому медленнее.
Как ещё защититься от XSS?
CSP ограничивает исполнение скриптов согласно явно заданной политике и дополняет безопасный вывод. HttpOnly мешает скрипту прочитать cookie, но не мешает XSS выполнять действия от имени пользователя; это не устранение уязвимости.
- Недоверенная строка в
innerHTMLинтерпретируется как HTML. - Для текста подходит
textContent. - Для разрешённой разметки нужна проверенная очистка по разрешённому набору, а не самодельная регулярка.
Почитать на сайте
Спецификация: OWASP: предотвращение DOM XSS
Что такое DOM? Почему во вкладке Elements в DevTools код страницы может отличаться от того, что показывает «Просмотр кода страницы»?
Интервьюер может уточнить
- Чем атрибут отличается от свойства DOM?
- Почему частые изменения DOM могут тормозить страницу?
Разбор
Браузер читает HTML и превращает каждый тег в объект-узел: так получается дерево, где у <html> есть дети <head> и <body>, у них — свои дети и так далее. Это и есть DOM (Document Object Model). Через него JavaScript находит элементы (document.querySelector), меняет текст и атрибуты, добавляет и удаляет узлы.
Почему Elements отличается от исходника:
- браузер исправляет ошибки: закрывает незакрытые теги, добавляет
<tbody>в таблицу, переносит неверно вложенные элементы; - скрипты меняют страницу после загрузки: подгружают список, открывают меню, добавляют классы;
- значения, введённые в поля, хранятся в свойствах DOM, а не в атрибутах исходного HTML.
Уточняющие вопросы
Чем атрибут отличается от свойства DOM?
Атрибут записан в HTML, свойство — у объекта в JavaScript. Для поля ввода атрибут value — начальное значение, а свойство input.value — то, что сейчас введено.
Почему частые изменения DOM могут тормозить страницу?
После изменения браузеру может понадобиться пересчитать раскладку и перерисовать часть страницы. Много мелких изменений подряд лучше собрать в одно.
- DOM — дерево объектов, построенное браузером из HTML.
- JavaScript читает и меняет страницу через DOM.
- Elements показывает текущий DOM, «Просмотр кода» — исходный HTML.
- Названа хотя бы одна причина различий: исправление ошибок или изменения скриптом.
Почитать на сайте
Спецификация: DOM Standard
Как браузер превращает HTML и CSS в картинку на экране? Почему изменение width дороже для браузера, чем изменение transform?
Интервьюер может уточнить
- Почему стили подключают в
<head>? - Что такое принудительная раскладка?
Разбор
Путь от кода до пикселей:
- HTML → дерево DOM, CSS → дерево стилей CSSOM;
- вычисление стилей: какие правила применяются к каждому элементу;
- layout — расчёт размеров и положения;
- paint — рисование текста, фона, рамок, теней;
- composite — сборка нарисованных слоёв в итоговую картинку.
Если меняется геометрия — width, height, margin, размер шрифта, — браузер пересчитывает раскладку, и это может затронуть соседей и всю страницу. Изменение цвета или фона требует только перерисовки. transform и opacity браузер часто применяет на этапе сборки, поэтому их и берут для плавной анимации.
Уточняющие вопросы
Почему стили подключают в <head>?
Без стилей браузер не может правильно нарисовать страницу. Если стили придут позже, пользователь на мгновение увидит голый HTML, а потом страница «прыгнет».
Что такое принудительная раскладка?
Если скрипт меняет стили и тут же читает размеры, например offsetHeight, браузеру приходится сразу пересчитать раскладку. В цикле это сильно тормозит.
- Названы DOM и стили (CSSOM) как исходные данные.
- Названы этапы layout, paint и composite.
- Изменение геометрии запускает раскладку заново.
transformиopacityдешевле для анимации.
Почитать на сайте
Спецификация: HTML: цикл событий и отрисовка
Пользователь пишет: «Нажимаю кнопку „Сохранить“, ничего не происходит». С чего начнёте поиск причины в браузере?
Интервьюер может уточнить
- Что делать, если ошибка возникает только у пользователя, а у вас нет?
- Почему «ничего не происходит» — само по себе проблема интерфейса?
Разбор
- Воспроизвести. Уточнить браузер, страницу, действия. Без воспроизведения легко чинить не то.
- Console. Красные ошибки часто сразу показывают файл и строку: обработчик упал до отправки.
- Network. Ушёл ли запрос после нажатия? Если ушёл — какой код ответа и что в теле: 400 с ошибкой проверки, 500, ответ не тот, что ждёт код.
- Elements. Кнопка ли это вообще, не перекрыта ли она прозрачным элементом, не стоит ли
disabled. В разделе Event Listeners видно, есть ли обработчик клика. - Сузить. Поставить точку останова в обработчике во вкладке Sources или временный
console.logи пройти код по шагам.
После исправления стоит проверить исходный сценарий ещё раз и подумать, как не допустить такой ошибки снова: например, показывать пользователю сообщение, если запрос не удался.
Уточняющие вопросы
Что делать, если ошибка возникает только у пользователя, а у вас нет?
Сравнить окружение: браузер и его версию, расширения, размер экрана, данные аккаунта, медленную сеть. В DevTools можно включить замедление сети и эмуляцию устройства.
Почему «ничего не происходит» — само по себе проблема интерфейса?
Пользователь должен видеть результат действия: загрузку, успех или ошибку. Если запрос упал молча, человек не понимает, сохранились ли данные.
- Сначала воспроизвести проблему.
- Названа вкладка Console и поиск ошибок.
- Названа вкладка Network: ушёл ли запрос и что ответил сервер.
- Названы проверка элемента или обработчика и точка останова.
Почитать на сайте
Спецификация: Chrome DevTools: отладка JavaScript
Что такое Service Worker и что он даёт сайту? Как сделать, чтобы прочитанная статья открывалась без интернета, и почему после выкладки пользователи могут видеть старую версию?
Интервьюер может уточнить
- Чем Cache API отличается от HTTP-кеша?
- Может ли Service Worker менять DOM?
Разбор
Воркер регистрируют из страницы: navigator.serviceWorker.register('/sw.js'). Работает только на HTTPS (или localhost) и только для своего пути.
Жизненный цикл:
install— сохранить в кеш нужное для запуска: каркас страницы, стили, скрипты;activate— удалить старые кеши;fetch— решать, откуда брать ответ.
Стратегии для fetch:
- сначала кеш — для файлов с хешем в имени, которые не меняются;
- сначала сеть, при ошибке — кеш — для статей: свежая версия онлайн, сохранённая офлайн;
- кеш сразу, обновить в фоне (stale-while-revalidate) — быстро и почти свежо.
Для статьи: при открытии класть ответ в кеш, без сети отдавать его, а для непрочитанных — страницу «Нет соединения».
Обновление: новый воркер устанавливается, но ждёт (waiting), пока открыта хоть одна вкладка со старым. skipWaiting() ускоряет смену, но тогда старая страница может получить файлы новой версии. Часто показывают «Доступна новая версия — обновить».
Уточняющие вопросы
Чем Cache API отличается от HTTP-кеша?
HTTP-кешем управляют заголовки и браузер. Cache API — хранилище, которым управляет ваш код: что сохранить, когда отдать и когда удалить.
Может ли Service Worker менять DOM?
Нет, у него нет доступа к странице. Он общается с ней сообщениями через postMessage.
- Воркер работает отдельно от страницы и перехватывает запросы.
- Названы события
install,activate,fetchили Cache API. - Названа стратегия для офлайн-статей.
- Объяснено ожидание новой версии и способ обновления.
Спецификация: Service Workers
Что такое CSRF и кликджекинг? Как злоумышленник может заставить вошедшего пользователя перевести деньги и что защищает от этого на стороне браузера и сервера?
Интервьюер может уточнить
- Зачем атрибут
sandboxу<iframe>? - Нужен ли CSRF-токен, если API принимает только JSON с токеном в заголовке
Authorization?
Разбор
CSRF. Пользователь вошёл в банк. На чужой странице есть форма, которая автоматически отправляет POST на адрес перевода. Если cookie сессии уходит с этим запросом, сервер считает его настоящим.
SameSite=Lax(по умолчанию во многих браузерах) не отправляет cookie с чужих сайтов вPOST-запросах,Strict— вообще ни в каких;- CSRF-токен — случайное значение в форме, которое чужой сайт не может прочитать;
- проверка заголовков
OriginилиSec-Fetch-Siteна сервере; - действия только через
POSTи другие небезопасные методы, никогда черезGET.
Кликджекинг. Злоумышленник встраивает ваш сайт во фрейм с opacity: 0 и ставит под кнопку «Перевести» свою кнопку «Получить приз». Защита — запретить встраивание: frame-ancestors в CSP или старый заголовок X-Frame-Options. Для критичных действий помогает подтверждение, которое сложно «прокликать».
CORS от этого не защищает: он ограничивает чтение ответа, а не отправку запроса.
Уточняющие вопросы
Зачем атрибут sandbox у <iframe>?
Он защищает вашу страницу от встроенной: запрещает чужому фрейму скрипты, формы, всплывающие окна и переход верхней страницы, если явно не разрешить.
Нужен ли CSRF-токен, если API принимает только JSON с токеном в заголовке Authorization?
Обычно нет: чужой сайт не может добавить такой заголовок без разрешения CORS, а браузер сам его не прикладывает, как cookie.
- Объяснён CSRF: браузер прикладывает cookie к запросу с чужого сайта.
- Названы
SameSiteи CSRF-токен или проверкаOrigin. - Объяснён кликджекинг через невидимый фрейм.
- Названы
frame-ancestorsилиX-Frame-Options.
Почитать на сайте
Спецификация: OWASP: защита от CSRF
Чем событие DOMContentLoaded отличается от load? Скрипт в <head> вызывает document.querySelector('.menu') и получает null — почему и как исправить?
Интервьюер может уточнить
- Чем
asyncотличается отdefer? - Что случится, если ждать
loadдля запуска всего интерфейса?
Разбор
Браузер читает HTML сверху вниз. Обычный <script> останавливает разбор и выполняется сразу — элементов ниже него ещё нет.
DOMContentLoaded— документ разобран, все элементы есть, отложенные скрипты выполнены. Картинки могут ещё грузиться;loadуwindow— завершилась загрузка ресурсов, задерживающих это событие, например обычных картинок, стилей и фреймов. Ленивые ресурсы и будущие запросы скрипта оно не ждёт. Нужен редко, например чтобы узнать настоящие размеры картинки.
Как исправить:
<script src="app.js" defer>— скачивается параллельно и выполняется после разбора HTML, по порядку. Лучший вариант для большинства скриптов;type="module"ведёт себя какdefer;- скрипт в конце
<body>— старый, но рабочий способ; - обернуть код в обработчик
DOMContentLoaded— если скрипт нельзя перенести.
Уточняющие вопросы
Чем async отличается от defer?
async выполняется сразу, как только скачался, в любом порядке и может прервать разбор. Подходит для независимых скриптов вроде счётчика посещений.
Что случится, если ждать load для запуска всего интерфейса?
Меню и кнопки заработают только после загрузки всех картинок: на медленной сети страница долго будет «мёртвой».
DOMContentLoaded— DOM готов,load— загружено всё.- Скрипт выполняется раньше, чем браузер дошёл до элемента.
- Названо исправление:
defer,type="module"или скрипт в конце<body>.
Почитать на сайте
Спецификация: HTML: конец разбора документа
Что такое источник (origin) и правило одного источника? Совпадают ли источники у https://shop.ru, http://shop.ru, https://api.shop.ru и https://shop.ru:8080?
Интервьюер может уточнить
- Почему фронтенд на
localhost:5173не может обратиться к API наlocalhost:3000? - Чем «сайт» в
SameSiteотличается от источника?
Разбор
Источник = протокол + домен + порт. Путь не важен: https://shop.ru/cart и https://shop.ru/about — один источник. А вот:
http://shop.ru— другой протокол;https://api.shop.ru— другой домен, даже поддомен;https://shop.ru:8080— другой порт.
Правило одного источника — базовая защита браузера. Без него любой открытый сайт мог бы прочитать вашу почту в соседней вкладке. Ограничения:
- для межсайтового
fetchв режиме CORS чтение ответа требует разрешения сервера. Если нужен preflight и он не прошёл, основной запрос вообще не отправится; localStorage, IndexedDB — свои у каждого источника;- в чужой
<iframe>нельзя заглянуть скриптом, общаться можно черезpostMessage.
Cookie живут по своим, более старым правилам: они привязаны к домену и пути, а порт не различают. Поэтому https://shop.ru и https://shop.ru:8080 видят одни и те же cookie, а cookie с Domain=shop.ru доступна и api.shop.ru.
Картинки, стили и скрипты с других источников подключать можно — запрещено именно чтение данных из кода.
Уточняющие вопросы
Почему фронтенд на localhost:5173 не может обратиться к API на localhost:3000?
Разные порты — разные источники. Нужен CORS на API или прокси в dev-сервере, чтобы запросы шли с того же источника.
Чем «сайт» в SameSite отличается от источника?
В SameSite сравнивают схему и регистрируемый домен с учётом Public Suffix List, а не просто последние две части имени. Например, https://api.shop.co.uk и https://shop.co.uk — один сайт, но разные источники. Порты при сравнении сайтов не учитываются.
- Источник — протокол, домен и порт.
- Все четыре адреса — разные источники, объяснено почему.
- Названо, что именно ограничено: чтение ответов, хранилища, фреймы.
- Названы CORS или
postMessageкак разрешённые способы.
Спецификация: HTML: источники
Как пользоваться вкладкой Network в DevTools? Как найти запрос, который вернул ошибку, посмотреть, что ушло на сервер, и проверить страницу на медленном интернете?
Интервьюер может уточнить
- Почему запрос отменён (canceled)?
- Что значит «(disk cache)» в колонке размера?
Разбор
- Записать. Вкладка записывает только при открытых DevTools. Открыть её и повторить действие или обновить страницу.
- Найти. Фильтры по типу: Fetch/XHR — запросы скриптов к API, Doc, CSS, JS, Img. Поиск по адресу. Ошибочные запросы (4xx, 5xx, сеть) выделены красным.
- Разобрать запрос. Headers — адрес, метод, код ответа и заголовки; Payload — тело запроса и параметры; Preview и Response — ответ сервера; Initiator — какой код его отправил; Timing — DNS, соединение, ожидание и загрузка.
- Условия. Throttling — «Slow 4G» или «Offline»; Disable cache — как у пользователя, который пришёл впервые; Preserve log — не очищать список при переходе на другую страницу.
Запрос можно повторить: правой кнопкой — Copy as fetch или Copy as cURL. Это удобно, чтобы передать проблему бэкенд-разработчику.
Уточняющие вопросы
Почему запрос отменён (canceled)?
Его прервали: страница ушла на другой адрес, сработал AbortController или браузер заменил запрос новым.
Что значит «(disk cache)» в колонке размера?
Ответ взят из HTTP-кеша браузера, без обращения к серверу.
- Названы фильтры и поиск ошибочного запроса.
- Названы Headers, Payload и Response.
- Названы Throttling или Disable cache.
Спецификация: Chrome DevTools: вкладка Network
Каталог должен сохранять фильтр в адресе: /catalog?color=red&page=2, чтобы ссылкой можно было поделиться. Как прочитать и изменить такие параметры в JavaScript?
Интервьюер может уточнить
- Чем параметры адреса отличаются от части после
#? - Что делать с неправильным значением, например
page=abc?
Разбор
const url = new URL(location.href)
const color = url.searchParams.get('color') // 'red' или null
const page = Number(url.searchParams.get('page') ?? 1)
url.searchParams.set('color', 'blue')
url.searchParams.delete('page')
history.replaceState(null, '', url) // адрес обновился без перезагрузки- все значения — строки: числа нужно преобразовать и проверить;
URLSearchParamsсам кодирует пробелы и кириллицу — не собирайте адрес склейкой строк;- повторяющийся параметр
?tag=html&tag=cssчитается черезgetAll('tag'); replaceStateменяет текущую запись истории,pushStateдобавляет новую. Для фильтров чащеreplaceState, чтобы «Назад» не листал каждый щелчок по фильтру;- при
pushStateнужно слушать событиеpopstateи перерисовывать каталог при переходе назад.
Уточняющие вопросы
Чем параметры адреса отличаются от части после #?
Параметры уходят на сервер, а фрагмент #… остаётся в браузере. Фрагмент обычно ведёт на место на странице.
Что делать с неправильным значением, например page=abc?
Проверить и подставить значение по умолчанию: Number.isInteger(page) && page > 0 ? page : 1.
- Названы
URLиURLSearchParamsсgetиset. - Адрес обновляется через
history.replaceStateилиpushState. - Значения — строки, их проверяют.
Почитать на сайте
Спецификация: URL Standard
Фильтрация и сортировка 100 000 строк таблицы подвешивают страницу на секунду. Как перенести работу в Web Worker и что в нём недоступно?
Интервьюер может уточнить
- Чем Web Worker отличается от Service Worker?
- Как отменить устаревший расчёт, если пользователь продолжает печатать?
Разбор
// main.js
const worker = new Worker(new URL('./filter.js', import.meta.url), { type: 'module' })
worker.postMessage({ rows, query })
worker.onmessage = (event) => renderTable(event.data)
// filter.js
self.onmessage = ({ data }) => {
const result = data.rows.filter((row) => row.name.includes(data.query))
self.postMessage(result)
}- воркер — отдельный поток со своим циклом событий. Долгий расчёт в нём не блокирует клики и анимации;
- сообщения копируются алгоритмом structured clone. Для очень больших данных — передача
ArrayBufferво втором аргументеpostMessageбез копирования; - доступны
fetch, таймеры, IndexedDB,OffscreenCanvas; недоступны DOM,document,window,localStorage; - создание воркера и копирование данных стоят времени — для расчёта на 5 мс выигрыша не будет.
Если данные таблицы нужны постоянно, их загружают и держат в воркере, а в сообщениях передают только запрос и видимую часть результата. Дополнительно помогает виртуализация: рисовать в DOM только видимые строки.
Уточняющие вопросы
Чем Web Worker отличается от Service Worker?
Web Worker — помощник конкретной страницы для вычислений, живёт, пока жива страница. Service Worker — посредник между сайтом и сетью, общий для всех вкладок, живёт отдельно и может работать без открытой страницы.
Как отменить устаревший расчёт, если пользователь продолжает печатать?
Нумеровать запросы и отбрасывать устаревшие ответы или вызывать worker.terminate() и создавать новый воркер.
- Названы
new Worker,postMessageиonmessage. - Основной поток остаётся свободным.
- Названо, что недоступно: DOM,
window,localStorage. - Данные копируются; названа передача буферов или стоимость копирования.
Спецификация: HTML: Web Workers
Нужно подгружать следующую страницу ленты, когда пользователь докрутил до конца, и запускать анимацию блока при его появлении. Почему обработчик scroll с getBoundingClientRect() — плохой путь и чем его заменить?
Интервьюер может уточнить
- Как не загрузить одну страницу дважды?
- Нужен ли
IntersectionObserverдля ленивых картинок?
Разбор
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) loadNextPage()
}
}, { rootMargin: '400px' }) // начать загрузку заранее
observer.observe(document.querySelector('.feed-end'))- браузер вычисляет пересечения сам, без пересчёта раскладки на каждом событии прокрутки;
rootMarginрасширяет зону — следующая страница начнёт грузиться за 400px до конца;threshold— какая доля элемента должна стать видимой:0.5— половина;- после срабатывания одноразового наблюдения вызывают
observer.unobserve(element).
ResizeObserver сообщает об изменении размера элемента, а не окна — удобно для графиков и компонентов в колонках. MutationObserver отслеживает изменения DOM.
Для бесконечной ленты нужна и доступность: кнопка «Показать ещё» как запасной вариант, сохранение позиции при возврате назад и доступный подвал, до которого иначе не добраться.
Уточняющие вопросы
Как не загрузить одну страницу дважды?
Флаг «идёт загрузка» и номер следующей страницы. Пока загрузка идёт, новые срабатывания игнорируют.
Нужен ли IntersectionObserver для ленивых картинок?
Обычно нет: loading="lazy" делает это встроенно. Наблюдатель нужен для своей логики.
- Названа проблема
scrollи принудительной раскладки. - Назван
IntersectionObserverи наблюдение за маячком в конце. - Названы
rootMarginилиthreshold. - Назван
ResizeObserverили доступность ленты.
Спецификация: Intersection Observer
После чтения сохранённого черновика через JSON.parse(localStorage.getItem("draft")) страница иногда падает. Какие ошибки возможны и как сохранить работоспособность формы?
Интервьюер может уточнить
- Достаточно ли успешного
JSON.parse? - Что делать с записью старой версии?
Разбор
Обрабатывайте отдельно отсутствие записи, синтаксическую ошибку JSON и неподходящую структуру. У записи полезно хранить версию формата: старую версию преобразуют либо отклоняют. try/catch нужен и вокруг записи — возможна нехватка квоты.
При невозможности сохранения форма продолжает работать в памяти и сообщает, что черновик не сохранён. Не показывайте «Сохранено» до успешного setItem и не стирайте остальные настройки через localStorage.clear().
Уточняющие вопросы
Достаточно ли успешного JSON.parse?
Нет: результат может быть числом, null или объектом с другими полями. Нужно проверить форму и допустимые значения.
Что делать с записью старой версии?
Применить известное преобразование или сбросить только эту запись; не угадывать структуру.
- Учтены недоступность хранилища и ошибки JSON.
- После разбора проверены структура и версия.
- Форма работает без сохранения и честно сообщает результат.
Почитать на сайте
Спецификация: Первоисточник: browser
Встроенный виджет присылает сообщения через postMessage. Почему опасно сразу выполнять действие по event.data и как проверить отправителя?
Интервьюер может уточнить
- Зачем проверять источник окна, если домен совпал?
- Можно ли использовать
"*"для секретного ответа?
Разбор
window.addEventListener("message", (event) => {
if (event.origin !== "https://widget.example") return
if (event.source !== frame.contentWindow) return
const data = event.data
if (!data || typeof data !== "object" || data.type !== "resize") return
if (!Number.isFinite(data.height) || data.height < 100 || data.height > 1000) return
frame.style.height = data.height + "px"
})Проверка по подстроке адреса ненадёжна: похожее имя домена может принадлежать другому владельцу. Сообщение — данные, а не исполняемый код. Даже доверенный отправитель может ошибиться, поэтому значения ограничиваются контрактом.
Уточняющие вопросы
Зачем проверять источник окна, если домен совпал?
Сообщение могло прийти из другого окна того же источника; проверка связывает его с конкретным виджетом.
Можно ли использовать "*" для секретного ответа?
Нет: окно могло перейти на другой источник. Точный targetOrigin предотвращает доставку неподходящему получателю.
- Источник сравнивается точно, без подстрок.
- Проверен
event.source. - Данные проверены по типам и допустимым значениям.
- При отправке указан конкретный
targetOrigin.
Спецификация: Первоисточник: browser
Офлайн-каталог хранит тысячи товаров и изображения. Почему localStorage неудобен и какие гарантии даёт транзакция IndexedDB?
Интервьюер может уточнить
- Когда менять схему хранилища?
- Можно ли обещать вечное хранение офлайн-данных?
Разбор
localStorage синхронен и хранит строки; большие сериализации и записи мешают интерфейсу. В IndexedDB можно хранить записи и Blob, выбирать данные по ключам и индексам.
Связанное обновление товара и служебной версии выполняют в одной readwrite-транзакции нужных хранилищ. Об успехе сообщают после complete, об отмене — по abort. Сеть обычно завершают до открытия транзакции: ожидание произвольного промиса может оставить её без работы и привести к завершению раньше следующего запроса. Квоты и удаление данных браузером всё равно возможны.
Уточняющие вопросы
Когда менять схему хранилища?
В транзакции обновления версии, открываемой через событие upgradeneeded; изменение структуры нужно планировать как миграцию.
Можно ли обещать вечное хранение офлайн-данных?
Нет: пользователь или браузер могут удалить данные. Важные записи синхронизируют или дают экспортировать.
- Названы асинхронность и структурированное хранение.
- Успех запроса отличён от завершения транзакции.
- Сетевое ожидание вынесено за пределы транзакции.
Почитать на сайте
Спецификация: Первоисточник: browser
После нажатия «Назад» страница мгновенно появляется со старым состоянием, а DOMContentLoaded не вызывается повторно. Как это объяснить и что обновить?
Интервьюер может уточнить
- Чем это отличается от ответа из HTTP-кеша?
- Стоит ли полагаться на
unloadдля сохранения черновика?
Разбор
window.addEventListener("pageshow", (event) => {
if (event.persisted) refreshSessionAndCounters()
})Обновляйте данные, которые могли устареть: состояние входа, корзину, доступность действий. Не сбрасывайте без необходимости введённый текст и положение прокрутки. Инициализация должна быть идемпотентной: повторное добавление обработчиков приведёт к дублированию действий. BFCache не является обычным HTTP-кешем файлов.
Уточняющие вопросы
Чем это отличается от ответа из HTTP-кеша?
HTTP-кеш хранит ответы на запросы, а BFCache восстанавливает состояние документа и JavaScript.
Стоит ли полагаться на unload для сохранения черновика?
Нет: событие ненадёжно. Черновик сохраняют по изменениям; жизненный цикл страницы учитывают дополнительно.
- Названо восстановление полного состояния страницы.
- Выбрано событие
pageshowи проверкаpersisted. - Обновлены устаревающие данные без потери пользовательского ввода.
Спецификация: Первоисточник: browser
Простой GET к API с другого домена работает, а PUT с заголовком Content-Type: application/json падает с ошибкой CORS. Во вкладке Network виден запрос OPTIONS. Что это за запрос и что должен ответить сервер?
Интервьюер может уточнить
- Что нужно для отправки cookie в межсайтовом запросе?
- Защищает ли CORS сервер от запросов?
Разбор
Простыми считаются запросы GET, HEAD, POST со стандартными заголовками и Content-Type из трёх вариантов (форма, multipart, text/plain). Всё остальное — другие методы, application/json, свои заголовки вроде Authorization — требует preflight.
Браузер отправляет OPTIONS с заголовками Origin, Access-Control-Request-Method и Access-Control-Request-Headers. Сервер отвечает, например:
Access-Control-Allow-Origin: https://shop.ru;Access-Control-Allow-Methods: GET, PUT, DELETE;Access-Control-Allow-Headers: Content-Type, Authorization;Access-Control-Max-Age: 600— сколько секунд браузер может помнить разрешение.
Исправляют на сервере, а не во фронтенде. Частая причина — сервер требует авторизацию для OPTIONS или не обрабатывает этот метод вовсе.
Уточняющие вопросы
Что нужно для отправки cookie в межсайтовом запросе?
На клиенте credentials: 'include', на сервере Access-Control-Allow-Credentials: true и конкретный источник вместо *.
Защищает ли CORS сервер от запросов?
Нет: это правило браузера о чтении ответа. curl или серверный скрипт отправят любой запрос, поэтому авторизацию проверяет сервер.
- Объяснено, что такое preflight и когда он нужен.
- Названы заголовки ответа сервера.
- Сказано, что без разрешения основной запрос не уйдёт.
Спецификация: Fetch: CORS protocol
После смены IP сайта часть пользователей попадает на старый сервер. Как DNS и TTL могут это объяснить и что проверить?
Интервьюер может уточнить
- Как подготовить переезд на новый сервер?
- Чем запись A отличается от
CNAME?
Разбор
Уточняющие вопросы
Как подготовить переезд на новый сервер?
Заранее снизить TTL записи, например до 300 секунд, дождаться истечения старого TTL, переключить адрес и какое-то время держать оба сервера.
Чем запись A отличается от CNAME?
A указывает на IPv4-адрес, AAAA — на IPv6, CNAME — на другое имя, адрес которого нужно узнать отдельно.
- DNS сопоставляет имя с записями, включая адреса; это не HTTP-перенаправление.
- Кешированные ответы могут действовать до истечения TTL.
- Нужно сравнить ответы резолверов и авторитетного сервера, проверить записи и их TTL.
Спецификация: RFC 1034: DNS и кеширование
Сайт использует HTTPS. Что это защищает и означает ли это, что самому сайту можно доверять любые данные?
Интервьюер может уточнить
- Что проверяет браузер в сертификате?
- Что такое HSTS?
Разбор
Уточняющие вопросы
Что проверяет браузер в сертификате?
Что он выдан доверенным центром, не просрочен, не отозван и выдан именно для этого домена.
Что такое HSTS?
Заголовок, который велит браузеру всегда открывать сайт по HTTPS, даже если пользователь ввёл адрес с http.
- TLS защищает конфиденциальность и целостность передаваемых данных.
- Проверка сертификата связывает соединение с ожидаемым сервером.
- HTTPS не гарантирует добросовестность сайта и безопасность данных на конечных узлах.
Спецификация: RFC 8446: TLS 1.3
Первое открытие HTTPS-сайта медленное, повторное быстрее. Какие сетевые этапы стоит проверить, прежде чем оптимизировать JavaScript?
Интервьюер может уточнить
- Как ускорить соединение со сторонним доменом?
- Что входит в TTFB?
Разбор
Уточняющие вопросы
Как ускорить соединение со сторонним доменом?
<link rel="preconnect" href="https://cdn.example.com"> заранее выполняет DNS, TCP и TLS.
Что входит в TTFB?
Время от отправки запроса до первого байта ответа: сеть в обе стороны и подготовка ответа сервером.
- Различены поиск адреса, установка соединения, TLS и обмен HTTP.
- Учтены кеши и повторное использование соединения: этапы не всегда выполняются заново.
- Проверяются Timing и Waterfall, а не только общая длительность.
Спецификация: Chrome DevTools: время сетевых запросов
Почему утверждение «HTTP всегда работает поверх TCP» неверно? Чем HTTP/2 и HTTP/3 различаются при потере пакета?
Интервьюер может уточнить
- Что дал HTTP/2 по сравнению с HTTP/1.1?
- Ненадёжен ли HTTP/3, раз UDP не гарантирует доставку?
Разбор
Уточняющие вопросы
Что дал HTTP/2 по сравнению с HTTP/1.1?
Мультиплексирование многих запросов в одном соединении, сжатие заголовков и бинарный формат вместо нескольких параллельных соединений.
Ненадёжен ли HTTP/3, раз UDP не гарантирует доставку?
Нет: QUIC сам восстанавливает потерянные пакеты и шифрует данные. UDP служит только основой.
- HTTP/2 использует TCP, HTTP/3 использует QUIC поверх UDP.
- QUIC обеспечивает надёжные потоки; UDP не означает потерю частей HTTP-ответа без восстановления.
- Потеря в TCP может задержать все потоки соединения; QUIC разделяет доставку по потокам.
Спецификация: RFC 9114: HTTP/3 и QUIC
POST создания заказа завершился тайм-аутом. Можно ли автоматически повторить его? Как избежать двух заказов?
Интервьюер может уточнить
- Какие методы HTTP идемпотентны?
- Как повторять запросы, не перегружая сервер?
Разбор
POST способен создать второй. Согласуйте с API идентификатор операции или ключ идемпотентности, сохраняемый при повторах; сервер должен распознавать дубликаты. Сам заголовок без серверной реализации ничего не гарантирует. Идемпотентность означает одинаковый предполагаемый эффект повторов, а не обязательно одинаковый статус ответа.Уточняющие вопросы
Какие методы HTTP идемпотентны?
GET, HEAD, PUT, DELETE, OPTIONS и TRACE идемпотентны по семантике HTTP. POST и PATCH не имеют такой гарантии по определению, но конкретная операция может быть реализована идемпотентно.
Как повторять запросы, не перегружая сервер?
С экспоненциальной задержкой и случайным разбросом (jitter) и с ограничением числа попыток.
- Тайм-аут не доказывает, что сервер не выполнил операцию.
POSTне является идемпотентным по определению; повтор зависит от контракта API.- Нужен механизм дедупликации на сервере или проверка результата по идентификатору операции.
Напишите кодом
Спецификация: RFC 9110: идемпотентные методы
После успешного POST формы сервер отвечает перенаправлением. Чем 303 отличается от 307 и какой вариант подходит для перехода к странице результата?
Интервьюер может уточнить
- Что такое шаблон Post/Redirect/Get?
- Чем 301 отличается от 308?
Разбор
GET. При 307 он сохраняет POST и тело, поэтому адрес назначения должен принимать эту операцию. Нельзя заменять эти статусы друг другом только потому, что оба перенаправляют. Такой переход к странице результата сам по себе не обеспечивает защиту от повторного создания заказа.Уточняющие вопросы
Что такое шаблон Post/Redirect/Get?
После успешного POST сервер отвечает перенаправлением на страницу результата. Обновление страницы повторит безопасный GET, а не отправку формы.
Чем 301 отличается от 308?
Оба — постоянное перенаправление, но 308 гарантирует сохранение метода и тела, а при 301 браузеры исторически меняют POST на GET.
- 303 направляет к получению другого ресурса, обычно
GETстраницы результата. - 307 сохраняет метод и тело исходного запроса.
- Выбор зависит от того, нужно показать результат или повторить операцию по другому адресу.
Спецификация: RFC 9110: перенаправления
API возвращает небольшой JSON, но экран долго загружается. Как по Network отличить медленную передачу от цепочки зависимых запросов?
Интервьюер может уточнить
- Как разорвать цепочку?
- Что значит длинный этап Queueing?
Разбор
Уточняющие вопросы
Как разорвать цепочку?
Запускать независимые запросы параллельно через Promise.all, заранее загружать важные ресурсы через preload или получать данные одним запросом.
Что значит длинный этап Queueing?
Запрос ждал свободного соединения или был отложен браузером из-за низкого приоритета.
- Сопоставляются Waterfall, Initiator и Timing отдельных запросов.
- Различаются очередь, ожидание ответа и скачивание тела.
- Независимые запросы можно запускать параллельно; реальные зависимости сохраняются.
Спецификация: Chrome DevTools: Network
Какие HTTP-методы вы знаете и для чего каждый? Как бы вы назвали запросы для списка задач: получить все, создать, изменить, удалить?
Интервьюер может уточнить
- Почему удаление по ссылке
GET /tasks/42/delete— плохая идея? - Чем
PUTотличается отPATCH?
Разбор
Метод говорит серверу, что сделать с ресурсом по адресу:
GET— прочитать. Безопасный метод: клиент не запрашивает изменение состояния ресурса. Логирование на сервере допустимо; возможность кеширования зависит от ответа;POST— создать или выполнить действие. Повтор может создать вторую запись;PUT— заменить ресурс целиком;PATCH— изменить часть полей;DELETE— удалить;HEADиOPTIONS— служебные: получить только заголовки и узнать, что разрешено (браузер шлётOPTIONSперед некоторыми запросами CORS).
Для списка задач: GET /tasks — список, GET /tasks/42 — одна задача, POST /tasks — новая, PATCH /tasks/42 — отметить выполненной, DELETE /tasks/42 — удалить.
Важное слово — идемпотентность: GET, PUT и DELETE имеют одинаковый предполагаемый эффект при повторах, хотя статус ответа может различаться. Для POST такой гарантии по определению метода нет. Это важно, когда запрос нужно повторить после обрыва связи.
Уточняющие вопросы
Почему удаление по ссылке GET /tasks/42/delete — плохая идея?
Браузер, расширения и поисковые роботы считают GET безопасным и могут открыть ссылку заранее, например для предзагрузки. Задача удалится без действия пользователя.
Чем PUT отличается от PATCH?
PUT присылает ресурс целиком: незаполненные поля считаются удалёнными. PATCH присылает только то, что меняется.
- Названы
GET,POST,PUT/PATCHиDELETEс назначением. - Предложены адреса и методы для списка задач.
GETне меняет данные на сервере.- Упомянута идемпотентность или то, что повтор
POSTможет создать дубль.
Почитать на сайте
Спецификация: RFC 9110: методы HTTP
Что означают группы кодов ответа 2xx, 3xx, 4xx и 5xx? Чем 401 отличается от 403, а 404 — от 500?
Интервьюер может уточнить
- Какой код вернуть, если форма прислала неверный email?
- Стоит ли автоматически повторять запрос при 404?
Разбор
Первая цифра кода сразу говорит, кто виноват и что делать:
- 2xx — всё хорошо: 200 OK, 201 Created после создания, 204 No Content, когда тела нет;
- 3xx — идите по другому адресу (301, 302, 303, 307, 308) или используйте кеш (304);
- 4xx — проблема в запросе: 400 — неверные данные, 401 — нужна авторизация, 403 — доступ запрещён, 404 — не найдено, 429 — слишком много запросов;
- 5xx — проблема на сервере: 500 — внутренняя ошибка, 502 — шлюз получил некорректный ответ вышестоящего сервера, 504 — не дождался ответа вовремя, 503 — сервис временно недоступен.
Для интерфейса это значит разное: при 401 предложить войти, при 403 объяснить, что нет прав, при 404 показать «не найдено», при 5xx — «попробуйте позже» и, возможно, повторить запрос.
Уточняющие вопросы
Какой код вернуть, если форма прислала неверный email?
Обычно 400 или 422 с описанием ошибки по полям, чтобы интерфейс показал сообщение рядом с нужным полем.
Стоит ли автоматически повторять запрос при 404?
Нет: ресурс не появится от повтора. Повторять имеет смысл временные ошибки — 503, 504, обрыв сети — и только безопасные или идемпотентные запросы.
- Объяснены все четыре группы: 2xx, 3xx, 4xx, 5xx.
- 401 — нет авторизации, 403 — нет прав.
- 404 — нет ресурса, 500 — ошибка сервера.
- Сказано, как реагирует интерфейс на разные коды.
Почитать на сайте
Спецификация: RFC 9110: коды ответа
Что происходит после того, как вы ввели адрес сайта в браузере и нажали Enter? Расскажите по шагам.
Интервьюер может уточнить
- Почему второй раз сайт открывается быстрее?
- Где посмотреть все эти шаги?
Разбор
- Адрес. Браузер разбирает URL: протокол, домен, путь. Если это не адрес, а слова — отправляет их в поиск.
- DNS. Ищет IP-адрес домена: сначала в своём кеше, потом в системе, потом спрашивает DNS-сервер.
- Соединение. Устанавливает соединение с сервером, для HTTPS — ещё и защищённый канал TLS с проверкой сертификата.
- Запрос и ответ. Отправляет
GETс заголовками и cookie, получает ответ: код, заголовки и HTML. Возможны перенаправления. - Разбор. Строит из HTML дерево DOM, находит стили, скрипты и картинки и загружает их. Обычные скрипты останавливают разбор, пока не выполнятся.
- Отрисовка. Из DOM и CSS вычисляет стили, размеры и положение элементов (layout), рисует их (paint) и показывает на экране.
На собеседовании важно не перечислить всё, а показать, что вы понимаете цепочку и знаете, где она может тормозить.
Уточняющие вопросы
Почему второй раз сайт открывается быстрее?
Срабатывают кеши: DNS уже знает адрес, соединение может быть переиспользовано, а стили, скрипты и картинки берутся из HTTP-кеша браузера.
Где посмотреть все эти шаги?
Во вкладке Network в DevTools: у каждого запроса есть раздел Timing с DNS, соединением, TLS и ожиданием ответа.
- Назван поиск IP-адреса через DNS.
- Названы соединение и TLS для HTTPS.
- Названы HTTP-запрос и ответ с HTML.
- Названы разбор HTML, загрузка ресурсов и отрисовка.
Почитать на сайте
Спецификация: HTML: разбор документа
Из чего состоит HTTP-запрос и ответ? Как отправить на сервер новую задачу в формате JSON и как сервер поймёт, что пришёл именно JSON?
Интервьюер может уточнить
- Можно ли передать данные в
GET-запросе? - Что будет, если отправить объект без
JSON.stringify?
Разбор
POST /api/tasks HTTP/1.1
Host: example.com
Content-Type: application/json
Accept: application/json
{"title": "Купить молоко"}Первая строка — метод и путь, затем заголовки, пустая строка и тело. Ответ устроен так же, но начинается с кода: HTTP/1.1 201 Created.
const response = await fetch('/api/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Купить молоко' }),
})
const created = await response.json()Полезные заголовки: Content-Type — формат тела, Accept — какой формат ждём в ответе, Authorization — токен, Cookie и Set-Cookie — cookie, Cache-Control — правила кеширования.
Все части запроса видны во вкладке Network в DevTools: Headers, Payload и Response.
Уточняющие вопросы
Можно ли передать данные в GET-запросе?
Через параметры адреса: /api/tasks?done=true&page=2. Тело у GET не используют. Значения кодируют через URLSearchParams.
Что будет, если отправить объект без JSON.stringify?
Он превратится в строку "[object Object]", и сервер получит бессмыслицу.
- Названы метод, адрес, заголовки и тело запроса.
- Названы код, заголовки и тело ответа.
- JSON отправлен через
JSON.stringifyиContent-Type: application/json.
Почитать на сайте
Спецификация: RFC 9110: сообщения HTTP
Что такое API и REST? Как бы вы описали адреса для статей блога и комментариев к ним?
Интервьюер может уточнить
- Как передать фильтры и сортировку?
- Что такое документация API?
Разбор
Фронтенд не работает с базой данных напрямую — он просит сервер через API. В стиле REST:
- адрес — существительное, ресурс:
/articles,/articles/7,/articles/7/comments; - действие — метод HTTP:
GETчитать,POSTсоздать,PATCHизменить,DELETEудалить; - результат — код статуса: 200, 201 Created, 404, 422 с ошибками проверки;
- сервер не помнит предыдущие запросы: каждый запрос несёт всё нужное, например токен.
Для блога:
GET /articles?page=2 список статей, вторая страница
GET /articles/7 одна статья
POST /articles/7/comments новый комментарий
DELETE /comments/15 удалить комментарийПлохо: /getArticles, /deleteComment?id=15 — действия в адресе вместо методов.
Уточняющие вопросы
Как передать фильтры и сортировку?
Параметрами адреса: /articles?tag=css&sort=-date&page=2.
Что такое документация API?
Описание адресов, параметров, форм ответа и ошибок. Часто в формате OpenAPI, по которому можно сгенерировать типы для фронтенда.
- API объяснён как договорённость об обмене данными.
- В REST адрес — ресурс, метод — действие.
- Предложены адреса для статей и комментариев.
- Названы коды статуса или формат JSON.
Спецификация: RFC 9110: семантика HTTP
Нужно показывать новые сообщения чата и курс валют в реальном времени. Чем отличаются опрос сервера (polling), long polling, Server-Sent Events и WebSocket, и что выбрать для каждой задачи?
Интервьюер может уточнить
- Почему WebSocket может не работать в корпоративной сети?
- Как не перегрузить сервер при массовом переподключении?
Разбор
- Polling:
setIntervalсfetch. Работает везде, но данные приходят с задержкой до интервала, а пустые ответы нагружают сервер. - Long polling: сервер не отвечает, пока нет новостей, затем клиент сразу шлёт следующий запрос. Задержка меньше, но на каждое событие — новый запрос.
- Server-Sent Events:
new EventSource('/rates')— сервер пишет события в открытый HTTP-ответ. Только от сервера к клиенту, только текст, зато браузер сам переподключается и может отправитьLast-Event-ID, если сервер задавал идентификаторы. Хранение и повторную выдачу пропущенных событий реализует сервер. Работает через обычную инфраструктуру HTTP. - WebSocket:
new WebSocket('wss://…')— постоянный двусторонний канал с маленькими накладными расходами, текст и бинарные данные. Переподключение, проверку связи и восстановление пропущенного пишут сами.
Курс валют и уведомления — SSE: данные идут в одну сторону. Чат, совместное редактирование, игры — WebSocket. Если обновлений мало и задержка не важна, обычный polling раз в минуту честнее и проще.
При любом варианте нужно продумать обрыв связи: показать состояние, переподключиться с паузой и догрузить пропущенное.
Уточняющие вопросы
Почему WebSocket может не работать в корпоративной сети?
Некоторые прокси и файрволы обрывают долгие соединения или не пропускают обновление протокола. Поэтому библиотеки умеют откатываться на long polling.
Как не перегрузить сервер при массовом переподключении?
Увеличивать паузу перед повтором экспоненциально и добавлять случайный разброс, чтобы тысячи клиентов не вернулись в одну секунду.
- Объяснены polling и long polling.
- SSE — односторонний поток с автопереподключением.
- WebSocket — двусторонний канал.
- Выбор обоснован для курса валют и чата, учтён обрыв связи.
Почитать на сайте
Спецификация: HTML: Server-Sent Events
Экран профиля делает шесть запросов к REST API и получает много лишних полей. Решит ли проблему GraphQL? Какие у него плюсы и минусы по сравнению с REST?
Интервьюер может уточнить
- Что такое проблема N+1?
- Чем мутация отличается от запроса в GraphQL?
Разбор
Что даёт GraphQL. Клиент описывает нужную форму данных: пользователь, его последние заказы и адреса — в одном запросе POST /graphql. Нет лишних полей и цепочек запросов. Схема типов служит документацией и источником типов для TypeScript.
Чем платят:
- кеш: в REST каждый ресурс — свой адрес, который кешируют браузер и CDN. В GraphQL запросы идут через один адрес, обычный HTTP-кеш для POST использовать сложнее. Возможны клиентский кеш, серверный кеш и GET-запросы для чтения, в том числе сохранённые запросы с устойчивым URL;
- нагрузка: клиент может запросить глубокую вложенность. Нужны ограничения сложности и защита от проблемы N+1 на сервере;
- ошибки: ответ часто 200 с полем
errors, их нужно обрабатывать отдельно; - стоимость: новый сервер, инструменты и знания команды.
Варианты без смены технологии: адрес под экран (Backend for Frontend), параметр выбора полей ?fields=name,avatar, вложение связей ?include=orders, параллельные запросы через Promise.all вместо последовательных.
Уточняющие вопросы
Что такое проблема N+1?
Для списка из N пользователей сервер делает один запрос за списком и ещё N — за заказами каждого. Решают пакетной загрузкой, например через DataLoader.
Чем мутация отличается от запроса в GraphQL?
Запрос (query) только читает, мутация (mutation) меняет данные — аналог POST, PATCH и DELETE.
- GraphQL — одним запросом нужные поля, строгая схема.
- Названы минусы: кеширование, нагрузка, ошибки или стоимость перехода.
- Названы способы улучшить REST без смены технологии.
Спецификация: Спецификация GraphQL
Как сайт «помнит», что вы вошли, если HTTP не хранит состояние между запросами? Чем вход через cookie сессии отличается от токена в localStorage?
Интервьюер может уточнить
- Почему cookie сессии не уходит на запрос к API на другом домене?
- Что такое cookie без
Max-AgeиExpires?
Разбор
HTTP не помнит прошлые запросы, поэтому каждый запрос должен нести «пропуск».
Cookie сессии. После проверки пароля сервер отвечает:
Set-Cookie: session=a8f3…; HttpOnly; Secure; SameSite=Lax; Max-Age=2592000Дальше браузер сам прикладывает Cookie: session=a8f3… к запросам на этот сайт. Сервер находит сессию по идентификатору. HttpOnly прячет cookie от JavaScript, Secure отправляет только по HTTPS, SameSite ограничивает отправку с чужих сайтов, Max-Age задаёт срок.
Токен. Сервер отдаёт токен в теле ответа, код сохраняет его и добавляет Authorization: Bearer … к запросам. Удобно для мобильных приложений и API на другом домене, но если хранить токен в localStorage, при XSS его украдут.
Выход из аккаунта — удалить сессию на сервере, а не только cookie в браузере.
Уточняющие вопросы
Почему cookie сессии не уходит на запрос к API на другом домене?
Сначала проверьте, есть ли cookie именно для домена API и подходит ли её путь. credentials: "include" разрешает отправку подходящих cookie в cross-origin fetch, но не пересылает cookie чужого домена. Нужны согласованные CORS-заголовки; для cross-site сценария также учитываются SameSite=None; Secure и ограничения браузера на сторонние cookie.
Что такое cookie без Max-Age и Expires?
Сессионная cookie: удаляется при закрытии браузера (хотя некоторые браузеры восстанавливают сессии).
- Объяснён
Set-Cookieи автоматическая отправка cookie. - Названы
HttpOnly,SecureилиSameSite. - Объяснён токен в заголовке
Authorization. - Назван риск токена в
localStorageпри XSS.
Почитать на сайте
Спецификация: RFC 6265: HTTP-cookie
Страница весит всего 300 КБ, но на мобильном интернете открывается 6 секунд. Чем задержка (latency) отличается от пропускной способности и почему число запросов иногда важнее их размера?
Интервьюер может уточнить
- Что такое TTFB?
- Поможет ли сжатие gzip или Brotli с задержкой?
Разбор
Представьте трубу: ширина — пропускная способность, длина — задержка. Даже маленькому файлу нужно пройти всю длину туда и обратно.
Почему 300 КБ грузятся долго:
- новое соединение: DNS, TCP и TLS — несколько кругов по 150 мс ещё до первого байта;
- цепочки зависимостей: HTML → CSS → в CSS указан шрифт → в скрипте запрос к API. Каждый шаг ждёт предыдущий;
- много доменов: на каждый — своё соединение.
Что помогает:
- убрать цепочки:
preloadдля важного, критический CSS в странице, запросы к API параллельно; - меньше доменов и
preconnectк нужным; - HTTP/2 и HTTP/3 загружают много файлов по одному соединению;
- кеш и CDN рядом с пользователем.
Проверять — с Throttling во вкладке Network и смотреть Waterfall: длинные «лестницы» и есть цепочки.
Уточняющие вопросы
Что такое TTFB?
Time To First Byte — время от отправки запроса до первого байта ответа. Включает сеть и время работы сервера.
Поможет ли сжатие gzip или Brotli с задержкой?
Сжатие уменьшает объём, то есть помогает с пропускной способностью. Число кругов ожидания оно не меняет.
- Объяснено отличие задержки от пропускной способности.
- Названы цепочки зависимых запросов или затраты на соединение.
- Названы способы:
preload, параллельные запросы, HTTP/2, CDN или кеш.
Спецификация: Chrome DevTools: Waterfall и Timing
Пользователи из Владивостока жалуются, что сайт с сервером в Москве грузится медленно. Как помогут CDN и сжатие и какие файлы как кешировать?
Интервьюер может уточнить
- Как сбросить кеш CDN после выкладки?
- Что такое
stale-while-revalidate?
Разбор
CDN. 7000 км — это 100+ мс на каждый круг запроса, и несколько кругов на соединение. Узел CDN во Владивостоке устанавливает соединение рядом с пользователем, а файлы отдаёт из своего кеша. Сервер в Москве получает только запросы, которых нет в кеше.
Сжатие. Браузер присылает Accept-Encoding: br, gzip, сервер отвечает Content-Encoding: br. HTML, CSS, JS и SVG сжимаются в 3–5 раз. Картинки в JPEG, WebP, AVIF и шрифты WOFF2 уже сжаты — их повторно не жмут. Статические файлы сжимают заранее, при сборке, с максимальным уровнем.
Кеширование:
app.3f9a.js,style.81c2.css— новое содержимое получает новое имя, поэтомуmax-age=31536000, immutable;- HTML —
no-cache(с проверкой поETag) или короткийmax-age, чтобы выкладка становилась видна быстро; - ответы с личными данными —
privateилиno-store, чтобы CDN не отдал их другому человеку; Vary: Accept-Encoding— чтобы кеш хранил сжатую и несжатую версии отдельно.
Уточняющие вопросы
Как сбросить кеш CDN после выкладки?
Для файлов с хешем не нужно — у них новое имя. HTML очищают командой purge у провайдера или держат с коротким сроком.
Что такое stale-while-revalidate?
Разрешение отдать устаревший ответ сразу и обновить кеш в фоне. Пользователь не ждёт, а данные почти свежие.
- CDN отдаёт файлы с ближайшего сервера и снижает задержку.
- Названо сжатие Brotli или gzip для текстовых файлов.
- Файлы с хешем — долгий кеш, HTML — короткий или с проверкой.
- Персональные ответы —
privateилиno-store.
Спецификация: RFC 9111: HTTP-кеширование
Какие заголовки безопасности стоит включить на сайте и от чего каждый защищает: Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options, Referrer-Policy?
Интервьюер может уточнить
- Почему CSP часто ломает сайт при включении?
- Чем опасен слишком большой
max-ageу HSTS?
Разбор
Strict-Transport-Security: max-age=31536000; includeSubDomains— после первого визита браузер сам переписываетhttp://наhttps://. Политика запоминается только из защищённого ответа. Самый первый HTTP-визит до получения политики остаётся уязвимым; предварительное знание политики может дать список HSTS preload.Content-Security-Policy: default-src 'self'; script-src 'self'; img-src 'self' https:; frame-ancestors 'none'— скрипты только со своего сайта, без встроенныхonclickи<script>в разметке; встраивание сайта во фреймы запрещено. Такая политика блокирует многие способы исполнения внедрённого кода, но не гарантирует защиту от всех XSS, например при небезопасных скриптах на разрешённом источнике. Вводят постепенно черезContent-Security-Policy-Report-Only, чтобы сначала собрать нарушения.X-Content-Type-Options: nosniff— браузер не выполнит как скрипт файл, который сервер отдал как картинку или текст.Referrer-Policy: strict-origin-when-cross-origin— другим сайтам уходит только домен, без пути и параметров, где могут быть личные данные.Permissions-Policy— отключить ненужные камеру, микрофон, геолокацию.
Заголовки не заменяют правильный код: экранирование вывода, проверка данных на сервере и защита от CSRF нужны всё равно.
Уточняющие вопросы
Почему CSP часто ломает сайт при включении?
Встроенные скрипты и обработчики в атрибутах, сторонние счётчики и шрифты оказываются запрещены. Нужно перенести код в файлы или разрешить конкретные скрипты по nonce или хешу.
Чем опасен слишком большой max-age у HSTS?
Если HTTPS сломается, браузеры не пустят пользователей на сайт по HTTP до истечения срока. Поэтому начинают с короткого срока.
- HSTS — только HTTPS.
- CSP — источники ресурсов и защита от XSS, вводится через Report-Only.
- Названы
nosniffилиReferrer-Policy. - Заголовки дополняют, а не заменяют защиту в коде.
Спецификация: CSP Level 3
Удаление записи прошло успешно: сервер вернул 204. Почему await response.json() падает и как обработать такой ответ?
Интервьюер может уточнить
- Чем
nullв JSON отличается от пустого тела? - Есть ли тело у ответа на
HEAD?
Разбор
const response = await fetch("/api/items/42", { method: "DELETE" })
if (!response.ok) throw new Error("HTTP " + response.status)
if (response.status === 204) {
removeItemFromView(42)
} else {
const result = await response.json()
renderResult(result)
}В примере остальные успешные ответы по контракту содержат JSON. Успешный статус сам по себе не обещает JSON: формат и наличие тела должны быть оговорены. Пустая строка не является корректным JSON; возвращать вместо ошибки пустой объект опасно.
Уточняющие вопросы
Чем null в JSON отличается от пустого тела?
null — корректный JSON из четырёх символов, а у пустого тела нет JSON-значения для разбора.
Есть ли тело у ответа на HEAD?
Нет: метод получает метаданные представления без содержимого ответа.
- 204 объяснён как успех без тела.
- Вызов
json()исключён для пустого ответа. - Формат остальных ответов определяется контрактом.
Спецификация: Первоисточник: network
Два редактора открыли одну статью. Второй сохранил старую версию поверх правок первого. Как сервер и клиент могут обнаружить такой конфликт?
Интервьюер может уточнить
- Почему сравнение версии должно быть атомарным?
- Чем
If-None-Matchпри чтении отличается отIf-Matchпри записи?
Разбор
Получив ETag: "v7", клиент отправляет PUT с If-Match: "v7". Сервер атомарно сравнивает текущую версию и применяет изменение только при совпадении. Если другой редактор уже создал версию 8, ответ 412 сообщает о невыполненном условии.
Интерфейс сохраняет локальный черновик, предлагает загрузить актуальную версию и сопоставить изменения. Молчаливый повтор без условия снова создаёт риск потери правок. Защита от дублей запроса и защита от устаревшего редактирования решают разные задачи.
Уточняющие вопросы
Почему сравнение версии должно быть атомарным?
Между отдельной проверкой и записью может успеть сохраниться другой редактор.
Чем If-None-Match при чтении отличается от If-Match при записи?
Первое обычно позволяет не пересылать неизменившееся представление; второе разрешает изменение только ожидаемой версии.
- Клиент отправляет версию через
If-Match. - Сервер атомарно проверяет условие и сообщает конфликт.
- Локальный черновик сохраняется для разрешения конфликта.
Спецификация: Первоисточник: network
Поиск получил 429 и Retry-After: 30. Нужно ли сразу повторить запрос и как избежать лавины повторов от клиентов?
Интервьюер может уточнить
- Что делать, если заголовок отсутствует или неверен?
- Почему одинаковая задержка у всех клиентов неудобна?
Разбор
Retry-After бывает числом секунд или HTTP-датой, поэтому его нельзя всегда читать как миллисекунды. При известной минимальной задержке случайный разброс добавляют поверх неё, а не сокращают ожидание.
Для поиска пользователь может уже изменить запрос: повторять старый не нужно. Покажите состояние ожидания, соблюдайте предел попыток и учитывайте отмену. Повтор изменяющей операции допустим только при согласованной идемпотентности; статус 429 не делает любой запрос безопасным для повторения.
Уточняющие вопросы
Что делать, если заголовок отсутствует или неверен?
Использовать ограниченную политику задержек приложения, а не немедленный бесконечный цикл.
Почему одинаковая задержка у всех клиентов неудобна?
Они одновременно повторят запросы. Случайный разброс распределяет нагрузку во времени.
- 429 распознан как ограничение частоты.
- Учтены оба формата
Retry-After. - Повторы ограничены и учитывают актуальность операции.
Напишите кодом
Спецификация: Первоисточник: network
Сервер отдаёт по одному URL русский и английский текст в зависимости от Accept-Language. Почему общий кеш может перепутать версии и зачем нужен Vary?
Интервьюер может уточнить
- Заменяет ли
VaryзаголовокCache-Control? - Когда нужен
Vary: Origin?
Разбор
Если представление зависит от заголовка запроса, одного URL недостаточно для выбора ответа. Vary описывает дополнительные измерения ключа кеша. Срок и возможность хранения задаются отдельно, например через Cache-Control.
Для локализованных страниц отдельные URL часто проще: ссылкой можно поделиться, а число вариантов кеша предсказуемо. Не добавляйте Vary: Cookie механически ко всем страницам — это создаёт множество вариантов. Персональные ответы требуют осознанного private или no-store.
Уточняющие вопросы
Заменяет ли Vary заголовок Cache-Control?
Нет, он определяет выбор варианта, а не его срок годности или разрешение на хранение.
Когда нужен Vary: Origin?
Когда сервер формирует разные CORS-ответы по заголовку Origin, например отражает разрешённый источник.
- Объяснено влияние заголовка запроса на вариант ответа.
Varyотличён от политики хранения.- Персональные данные не объявлены безопасными только из-за
Vary.
Спецификация: Первоисточник: network
Бэкенд предлагает авторизацию через JWT: access-токен на 15 минут и refresh-токен на 30 дней. Чем это отличается от серверной сессии, где хранить токены в браузере и как выйти из всех устройств?
Интервьюер может уточнить
- Почему нельзя принимать JWT с алгоритмом
noneили выбирать алгоритм из заголовка токена? - Как фронтенду обновлять access-токен при нескольких параллельных запросах?
Разбор
Сессия: сервер хранит запись, клиент — cookie с идентификатором. Выход — удаление записи. Нужно общее хранилище сессий для нескольких серверов.
JWT: заголовок, данные (claims) и подпись в base64url. Данные не зашифрованы — их может прочитать любой, поэтому секреты туда не кладут. Удобно для нескольких сервисов, но отозвать выданный токен нельзя без чёрного списка.
Типичная схема:
- access-токен живёт минуты, хранится в памяти приложения и отправляется в
Authorization: Bearer; - refresh-токен — в cookie
HttpOnly; Secure; SameSite=Strictна путь обновления, недоступен скриптам; - при обновлении сервер выдаёт новую пару и помечает старый refresh использованным (ротация); повторное использование старого — признак кражи, все токены семьи отзываются.
Хранение токена в localStorage делает его доступным при XSS. Cookie защищены от чтения скриптом, но требуют защиты от CSRF.
Уточняющие вопросы
Почему нельзя принимать JWT с алгоритмом none или выбирать алгоритм из заголовка токена?
Злоумышленник сможет подделать токен без подписи или подменить алгоритм. Сервер должен проверять подпись строго ожидаемым алгоритмом и ключом.
Как фронтенду обновлять access-токен при нескольких параллельных запросах?
Одним общим запросом обновления: остальные запросы ждут его результата, а не запускают своё обновление каждый.
- Различены сессия и самодостаточный токен.
- Названа проблема отзыва JWT.
- Описаны короткий access и refresh в
HttpOnlycookie. - Упомянута ротация refresh-токенов или выход везде.
Спецификация: RFC 7519: JSON Web Token
Нужно добавить кнопку «Войти через VK ID / Google». Как устроен вход через OAuth 2.0 с кодом авторизации и зачем одностраничному приложению PKCE?
Интервьюер может уточнить
- Чем
scopeважен для пользователя? - Что будет, если не проверять
state?
Разбор
Поток Authorization Code с PKCE:
- приложение создаёт
code_verifierиstate, перенаправляет на провайдера сclient_id,redirect_uri,scope,stateиcode_challenge— SHA-256 от verifier; - пользователь входит у провайдера и соглашается дать доступ;
- провайдер возвращает на
redirect_uriсcodeиstate; приложение сверяетstate; - код обменивается на токены вместе с
code_verifier.
OAuth сам по себе про доступ к API, а не про личность. Для входа нужен OpenID Connect: он добавляет id_token с идентификатором пользователя, который проверяют.
Надёжнее всего, когда обмен кода и хранение токенов делает собственный бэкенд (паттерн BFF), а браузер получает обычную сессионную cookie. Устаревший implicit flow с токеном в адресе не используют. redirect_uri должен точно совпадать с зарегистрированным.
Уточняющие вопросы
Чем scope важен для пользователя?
Он определяет права приложения. Запрашивают минимум нужного — лишние права пугают и увеличивают ущерб при утечке.
Что будет, если не проверять state?
Злоумышленник может подсунуть свой код авторизации, и жертва войдёт в чужой аккаунт или привяжет его к своему (login CSRF).
- Описаны редирект, код и обмен на токены.
- Объяснено назначение PKCE.
- Назван
state. - Различены OAuth и OpenID Connect.
Спецификация: RFC 7636: PKCE
Дизайнер предлагает кликабельный div вместо кнопки. Что нужно для доступного действия «Удалить» и когда вместо button нужна ссылка?
Интервьюер может уточнить
- Что придётся сделать, если всё-таки использовать
div? - Зачем
type="button"внутри формы?
Разбор
button, а для навигации ссылку с href. Кнопка уже поддерживает фокус и активацию Enter/Пробелом. Иконке удаления нужно доступное имя, например aria-label="Удалить запись". Одна роль button на div не добавляет клавиатурного поведения. Видимый фокус помогает понять, где находится пользователь. Внутри формы задайте type="button", если отправка не нужна.Уточняющие вопросы
Что придётся сделать, если всё-таки использовать div?
Добавить role="button", tabindex="0", обработку Enter и пробела, состояние disabled и видимый фокус. Проще и надёжнее взять button и сбросить его стили.
Зачем type="button" внутри формы?
По умолчанию кнопка внутри формы имеет type="submit" и отправит форму. Для кнопки «Удалить строку» это неожиданное поведение.
- Для действия выбран
button, для переходаaсhref. - Кнопка доступна с клавиатуры и имеет понятное имя, в том числе если видна только иконка.
- Фокус заметен; у кнопки внутри формы при необходимости указан
type="button".
Почитать на сайте
Спецификация: WAI: поведение кнопок
Модальное окно видно, но Tab уходит на страницу за ним. Какие требования нарушены и что нужно сделать при открытии и закрытии?
Интервьюер может уточнить
- Как сделать фон недоступным?
- Куда ставить фокус при открытии?
Разбор
dialog с showModal() даёт часть поведения, но выбор начального фокуса и проверка сценария остаются за разработчиком.Уточняющие вопросы
Как сделать фон недоступным?
showModal() делает это сам. Для своей реализации — атрибут inert на остальном содержимом страницы.
Куда ставить фокус при открытии?
На первое поле формы или на заголовок окна, если сначала нужно прочитать текст. Ставить его сразу на кнопку удаления в окне подтверждения опасно.
- Фокус переводится в диалог и остаётся внутри при Tab/Shift+Tab.
- Фон недоступен для взаимодействия, есть имя и способ закрытия.
- Escape закрывает; после закрытия фокус обычно возвращается к инициатору.
Почитать на сайте
Спецификация: WAI: модальный диалог
Одна и та же иконка используется как украшение рядом с текстом и как единственное содержимое ссылки на корзину. Как отличается альтернативный текст?
Интервьюер может уточнить
- Чем
alt=""отличается от отсутствияalt? - Как подписать inline-SVG иконку?
Разбор
alt="", чтобы не дублировать соседний текст. Во втором нужен смысл действия, например «Корзина», а не «серая тележка». Если ссылка уже имеет другое доступное имя, изображение может оставаться декоративным. Для графика одного названия недостаточно: значимые данные требуют текстового эквивалента. Решение зависит от функции изображения на конкретной странице.Уточняющие вопросы
Чем alt="" отличается от отсутствия alt?
Пустой alt говорит «это украшение, пропусти». Без alt скринридер может прочитать имя файла, например «icon-cart-24.svg».
Как подписать inline-SVG иконку?
Для декоративной — aria-hidden="true". Для значимой — role="img" и aria-label или элемент <title> внутри SVG.
- У декоративной картинки рядом с достаточным текстом
altпустой. - У единственного изображения в ссылке текст передаёт назначение ссылки.
altописывает смысл в контексте, а не имя файла или все визуальные детали.
Почитать на сайте
Спецификация: WAI: выбор альтернативного текста
После отправки неверное поле лишь обводится красным. Почему этого недостаточно? Как связать текст ошибки с полем и помочь исправить форму?
Интервьюер может уточнить
- Когда показывать ошибку: при вводе или при отправке?
- Как сообщить об ошибке скринридеру без перевода фокуса?
Разбор
input, выставьте aria-invalid при ошибке. После отправки можно перевести фокус к первой ошибке или к сводке со ссылками на поля. Динамическое уведомление должно быть доступно скринридеру, но не нужно объявлять все поля при каждом символе. Placeholder не заменяет постоянную подпись.Уточняющие вопросы
Когда показывать ошибку: при вводе или при отправке?
Обычно при уходе из поля или при отправке. Ошибка на каждый символ мешает: человек ещё не закончил ввод, а скринридер постоянно объявляет её.
Как сообщить об ошибке скринридеру без перевода фокуса?
Через область с role="alert" или aria-live: её содержимое объявляется при изменении.
- Ошибка описана словами и не зависит только от цвета.
- Поле имеет
label; сообщение связано черезaria-describedbyили подходящий механизм. - Используются
aria-invalidи понятная стратегия фокуса/уведомления без потери ввода.
Почитать на сайте
Спецификация: WAI: сообщения об ошибках формы
Что такое доступность сайта и кому она нужна? Что можно проверить на своей странице за пять минут без специальных программ?
Интервьюер может уточнить
- Что такое WCAG?
- Хватит ли проверки Lighthouse, чтобы считать сайт доступным?
Разбор
Доступность (a11y) нужна не только людям с инвалидностью. Подписи к полям помогают всем, контраст — тем, кто читает на ярком солнце, а работа с клавиатуры — тем, у кого сломалась мышь. Поисковики тоже лучше понимают страницу с хорошей разметкой.
Быструю проверку можно сделать руками:
- пройти страницу клавишей Tab: видно ли, где фокус, и можно ли нажать каждую кнопку и ссылку;
- проверить, что у содержательных картинок есть
alt, а у декоративных — пустойalt=""; - убедиться, что у каждого поля формы есть
<label>; - увеличить масштаб до 200% — не разваливается ли страница;
- проверить контраст текста, например во вкладке Elements в DevTools.
Есть и автоматические проверки, например Lighthouse, но они находят только часть проблем: понятность текста ссылки или порядок фокуса нужно смотреть самому.
Уточняющие вопросы
Что такое WCAG?
Рекомендации по доступности веб-содержимого от W3C. В них есть уровни A, AA и AAA; на практике обычно ориентируются на AA — например, контраст обычного текста не меньше 4.5:1.
Хватит ли проверки Lighthouse, чтобы считать сайт доступным?
Нет. Автоматика ловит отсутствие alt или низкий контраст, но не поймёт, что alt бессмысленный или что фокус прыгает в неожиданное место. Нужна ручная проверка с клавиатуры и, по возможности, со скринридером.
- Доступность объяснена как удобство для людей с разными ограничениями, а не только для незрячих.
- Названа проверка с клавиатуры: Tab, видимый фокус, все элементы нажимаются.
- Названы альтернативный текст картинок и подписи полей.
- Названы контраст или масштабирование.
Почитать на сайте
Спецификация: WCAG 2.2: рекомендации по доступности
Как проверить, что страницей можно пользоваться без мыши? Что делает атрибут tabindex и почему значения больше нуля обычно вредят?
Интервьюер может уточнить
- Почему нельзя просто убрать обводку фокуса, если она некрасивая?
- Зачем ссылка «Перейти к содержимому» в начале страницы?
Разбор
Ссылки, кнопки и поля формы получают фокус сами — поэтому для действий лучше брать именно их. Проверка с клавиатуры: Tab переходит вперёд, Shift+Tab — назад, Enter нажимает ссылку и кнопку, пробел — кнопку и флажок, Escape закрывает окна.
tabindex меняет поведение фокуса:
tabindex="0"— элемент попадает в обход в порядке кода; нужен редко, например для своей виджет-обёртки;tabindex="-1"— элемент не попадает в обход, но на него можно перевести фокус черезfocus(): так переводят фокус на заголовок окна или на сообщение об ошибке;- положительное значение ставит элемент в начало обхода раньше всех остальных. На реальной странице это быстро превращается в путаницу.
Порядок фокуса должен совпадать с порядком чтения. Если элементы переставлены только через CSS, например order во Flexbox, фокус пойдёт по коду, а не по экрану.
Уточняющие вопросы
Почему нельзя просто убрать обводку фокуса, если она некрасивая?
Без неё человек с клавиатуры не видит, где находится. Обводку можно перерисовать в стиле сайта, например через :focus-visible, но не удалять без замены.
Зачем ссылка «Перейти к содержимому» в начале страницы?
Чтобы не проходить Tab-ом всё меню на каждой странице. Ссылка ведёт на <main> и обычно появляется только при фокусе.
- Названа проверка Tab и Shift+Tab: фокус виден и доходит до всех элементов.
- Объяснены
tabindex="0"иtabindex="-1". - Положительный
tabindexломает естественный порядок обхода. - Порядок фокуса совпадает с порядком чтения.
Почитать на сайте
Спецификация: HTML: порядок фокуса и tabindex
Что такое ARIA и когда её стоит использовать? Почему говорят, что лучшая ARIA — та, без которой удалось обойтись?
Интервьюер может уточнить
- Чем
aria-labelотличается от видимой подписи? - Что будет, если повесить
aria-hidden="true"на кнопку?
Разбор
Скринридер узнаёт о странице через дерево доступности: у каждого элемента есть роль (кнопка, ссылка, заголовок), доступное имя и состояние (нажата, развёрнута). Нативные элементы заполняют это дерево сами. ARIA позволяет дописать то, чего HTML не умеет, например aria-expanded="true" у кнопки, которая раскрывает меню.
Первое правило ARIA: если есть HTML-элемент с нужным смыслом и поведением, используйте его. <button> уже фокусируется, нажимается Enter и пробелом и называется кнопкой. <div role="button"> только называется кнопкой — клавиатуру, фокус и состояние придётся писать самому.
Частые полезные атрибуты: aria-label — имя для кнопки-иконки, aria-describedby — связь с подсказкой или ошибкой, aria-expanded — состояние раскрытия, aria-live — объявлять изменения, aria-hidden="true" — спрятать декоративное от скринридера.
Уточняющие вопросы
Чем aria-label отличается от видимой подписи?
aria-label слышит только скринридер. Если подпись видна на экране, лучше связать её с элементом, например через <label> или aria-labelledby: тогда зрячий и незрячий пользователи видят и слышат одно и то же.
Что будет, если повесить aria-hidden="true" на кнопку?
Скринридер её не увидит, но с клавиатуры на неё по-прежнему можно попасть: пользователь окажется на «пустом» элементе. aria-hidden ставят только на то, что не нужно для работы.
- ARIA сообщает роль, имя и состояние, но не меняет поведение.
- Сначала выбирают нативный HTML-элемент, ARIA — когда его нет.
- Назван хотя бы один уместный пример:
aria-label,aria-expanded,aria-describedbyилиaria-live.
Почитать на сайте
Спецификация: Using ARIA: правила применения
После нажатия «В корзину» появляется уведомление «Товар добавлен» и через три секунды исчезает. Как сделать, чтобы его услышал пользователь скринридера, и чем role="status" отличается от role="alert"?
Интервьюер может уточнить
- Почему не перевести фокус на уведомление?
- Зачем
aria-atomic="true"?
Разбор
Живая область — элемент, изменения которого скринридер объявляет сам:
role="status"=aria-live="polite": дождётся паузы. Подходит для «Товар добавлен», «Сохранено», «Найдено 12 результатов»;role="alert"=aria-live="assertive": прервёт текущее чтение. Только для срочного: «Сессия истекает», ошибка оплаты.
<div role="status" class="toast"></div>
…
toast.textContent = 'Товар добавлен в корзину'Главная ошибка — вставлять в страницу новый элемент с role="status" вместе с текстом: многие скринридеры следят только за областями, которые уже были. Поэтому пустой контейнер кладут в разметку заранее.
Исчезающее через три секунды уведомление не успеет прочитать и человек, который медленно читает. Сообщение должно висеть дольше или до закрытия, а важное действие — например «Отменить» — не стоит прятать во всплывающем окне.
Уточняющие вопросы
Почему не перевести фокус на уведомление?
Человек потеряет место, где был, и придётся возвращаться к товару. Фокус переводят, только когда дальше нужно действие в этом месте, например в окне подтверждения.
Зачем aria-atomic="true"?
Чтобы при изменении части текста, например счётчика, объявлялась вся область целиком: «В корзине 3 товара», а не просто «3».
- Названа живая область:
aria-liveили рольstatus/alert. - Объяснено отличие вежливого и срочного объявления.
- Контейнер существует заранее, меняется текст.
- Учтено время показа или закрытие вручную.
Спецификация: WAI-ARIA: живые области
Нужны вкладки «Описание», «Характеристики», «Отзывы». Какие роли и атрибуты понадобятся и как должна работать клавиатура?
Интервьюер может уточнить
- Почему не сделать все вкладки доступными по Tab?
- Зачем панели
tabindex="0"?
Разбор
<div role="tablist" aria-label="О товаре">
<button role="tab" id="t1" aria-selected="true" aria-controls="p1">Описание</button>
<button role="tab" id="t2" aria-selected="false" aria-controls="p2" tabindex="-1">Характеристики</button>
</div>
<div role="tabpanel" id="p1" aria-labelledby="t1">…</div>
<div role="tabpanel" id="p2" aria-labelledby="t2" hidden>…</div>Клавиатура по шаблону WAI-ARIA:
- Tab приводит на активную вкладку, следующий Tab — в панель, а не на соседние вкладки. Для этого у неактивных
tabindex="-1"(приём «блуждающего tabindex»); - стрелки влево и вправо переводят фокус между вкладками, Home и End — на первую и последнюю;
- переключение сразу при переходе стрелкой или по Enter — выбирают в зависимости от того, быстро ли загружается панель.
При смене вкладки обновляют aria-selected, tabindex и hidden у панелей. Если «вкладки» на деле ведут на разные страницы, это навигация: ссылки с aria-current="page", а не роли вкладок.
Уточняющие вопросы
Почему не сделать все вкладки доступными по Tab?
Это допустимо, но шаблон ожидает одну точку входа: пользователь скринридера знает, что внутри списка вкладок работают стрелки, и не проходит Tab-ом каждый пункт.
Зачем панели tabindex="0"?
Если в панели нет фокусируемых элементов, без него следующий Tab перескочит через неё, и текст панели сложно найти.
- Названы роли
tablist,tab,tabpanel. - Названы
aria-selectedи связь вкладки с панелью. - Стрелки переключают вкладки, Tab ведёт в панель.
- Назван блуждающий
tabindexилиtabindex="-1"у неактивных.
Почитать на сайте
Спецификация: ARIA APG: шаблон вкладок
Как проверить страницу скринридером, если вы им никогда не пользовались? Что именно смотреть и какие сочетания браузера и программы брать?
Интервьюер может уточнить
- Почему скринридер читает кнопку как «кнопка» без названия?
- Что такое режим просмотра и режим фокуса в NVDA?
Разбор
Скринридеры ведут себя по-разному, поэтому проверяют на тех сочетаниях, которыми реально пользуются: NVDA и JAWS на Windows, VoiceOver на Apple, TalkBack на Android.
Что проверять:
- Навигация по заголовкам (в NVDA — клавиша H): читается ли структура страницы как оглавление;
- Области (D в NVDA): есть ли шапка, навигация, основное содержимое, подвал;
- Список ссылок и полей: понятны ли тексты без контекста — «Подробнее» десять раз подряд не помогает;
- Элементы управления: у каждого есть имя, роль и состояние — «Меню, кнопка, свёрнуто»;
- Динамика: объявляются ли ошибки, результаты поиска, уведомления;
- Главный сценарий целиком: найти товар, положить в корзину, оформить заказ.
Перед этим полезно посмотреть дерево доступности в DevTools (панель Accessibility): там видно имя и роль каждого элемента. Но настоящий опыт даёт только прохождение сценария на слух, а лучше всего — тестирование с людьми, которые пользуются скринридером каждый день.
Уточняющие вопросы
Почему скринридер читает кнопку как «кнопка» без названия?
У неё нет доступного имени: внутри только иконка без текста. Нужен видимый текст, скрытый текст для скринридера или aria-label.
Что такое режим просмотра и режим фокуса в NVDA?
В режиме просмотра клавиши-буквы перемещают по элементам страницы, в режиме фокуса — вводят текст в поле. Сложные виджеты иногда ломают переключение между режимами.
- Названы реальные сочетания скринридера и браузера.
- Названа навигация по заголовкам и областям.
- Проверены имена ссылок, кнопок и полей.
- Пройден главный сценарий целиком.
Почитать на сайте
Спецификация: W3C WAI: оценка доступности
Дизайнер предлагает светло-серый текст на белом фоне и выделять обязательные поля только красной рамкой. Что не так и как проверить?
Интервьюер может уточнить
- Нужно ли проверять контраст текста на картинке?
- Что такое тёмная тема с точки зрения контраста?
Разбор
Контраст. Рекомендации WCAG уровня AA: обычный текст — 4.5:1, крупный (от 24px или 18.7px жирным) и значимые элементы интерфейса вроде рамки поля — 3:1. Серый #999 на белом — около 2.8:1, это мало. При выборе элемента в DevTools во вкладке Elements в подсказке показан коэффициент контраста.
Не только цвет. Около 8% мужчин плохо различают красный и зелёный, а на ярком солнце цвета видны хуже всем. Поэтому:
- обязательное поле — словом «обязательно» или звёздочкой с пояснением в начале формы, плюс атрибут
required; - ошибка — текстом рядом с полем, а не только красной рамкой;
- ссылки в тексте — подчёркиванием, а не только цветом;
- на графиках — подписи или разные узоры линий.
Проверить «не только цветом» просто: посмотреть страницу в оттенках серого — в DevTools есть эмуляция нарушений цветового зрения.
Уточняющие вопросы
Нужно ли проверять контраст текста на картинке?
Да. Помогает однотонная подложка. Проверяют место с наименьшим контрастом: для светлого текста обычно самый светлый фон, для тёмного — самый тёмный.
Что такое тёмная тема с точки зрения контраста?
Те же требования, но наоборот. Чисто белый текст на чёрном фоне некоторым режет глаза, поэтому берут чуть смягчённые цвета с достаточным контрастом.
- Названы требования к контрасту: 4.5:1 для обычного текста.
- Информацию нельзя передавать только цветом.
- Предложено подписать обязательные поля и ошибки текстом.
- Названа проверка в DevTools или калькулятором.
Почитать на сайте
Спецификация: WCAG 2.2: контраст
В карточках новостей стоит ссылка «Подробнее», а в тексте — «нажмите здесь». Почему это плохо для доступности и как переписать?
Интервьюер может уточнить
- Можно ли сделать ссылкой всю карточку?
- Нужно ли писать «ссылка на…» в тексте?
Разбор
Ссылку читают вне контекста: в списке ссылок, при переходе Tab-ом, поисковик — как описание целевой страницы. «Подробнее», «здесь», «тут» ничего не сообщают.
Как исправить:
- написать цель прямо: «Скачать прайс-лист (PDF, 2 МБ)», «Тарифы для бизнеса»;
- в карточке сделать ссылкой заголовок — он уже описывает новость, — а «Подробнее» убрать или оставить декоративно;
- если короткая видимая подпись обязательна — дописать скрытую часть:
Подробнее<span class="visually-hidden"> о запуске тарифа</span>; - предупреждать о необычном: открытие в новой вкладке, скачивание файла.
aria-label тоже заменяет имя ссылки, но тогда видимый текст и то, что слышит скринридер, расходятся. Люди, управляющие голосом, говорят «нажать Подробнее» — и программа может не найти ссылку.
Уточняющие вопросы
Можно ли сделать ссылкой всю карточку?
Можно, но тогда скринридер прочитает весь текст карточки как имя ссылки. Чаще ссылкой делают заголовок, а кликабельность всей карточки добавляют через растянутый псевдоэлемент.
Нужно ли писать «ссылка на…» в тексте?
Нет, скринридер сам сообщает роль «ссылка».
- Объяснено, что ссылки читают вне контекста, например списком.
- Текст ссылки описывает цель.
- Предложен способ для карточки: ссылка-заголовок или скрытый текст.
Почитать на сайте
Спецификация: WCAG 2.2: назначение ссылки
Слабовидящий пользователь увеличивает страницу до 200–400%. Что на ней может сломаться и как вёрстка должна это выдержать?
Интервьюер может уточнить
- Как быстро проверить?
- А таблицы и карты?
Разбор
Увеличение в браузере на 400% при ширине окна 1280px равно экрану шириной 320px. Если страница адаптивна, она перестраивается в одну колонку — это требование WCAG «Reflow».
Что ломается:
- фиксированная высота блока: текст вылезает или обрезается
overflow: hidden; - фиксированная ширина в
px: появляется горизонтальная прокрутка; - шрифты в
pxне увеличиваются, если человек изменил только размер текста в настройках браузера; - закреплённые шапка и плашки занимают пол-экрана;
- текст в картинках становится размытым.
Как сделать правильно: rem для шрифтов, min-height вместо height, max-width и % вместо фиксированной ширины, Flexbox и Grid с переносами, медиазапросы. В метатеге viewport не указывать maximum-scale=1 и user-scalable=no.
Уточняющие вопросы
Как быстро проверить?
Нажать Ctrl + «плюс» до 400% на окне шириной около 1280px и пройти страницу: всё ли читается без горизонтальной прокрутки и ничего ли не перекрыто.
А таблицы и карты?
Им горизонтальная прокрутка допустима внутри своего блока: двумерные данные нельзя перестроить в одну колонку без потери смысла.
- Названо, что ломается: обрезка, наложение, горизонтальная прокрутка.
- Названы
remи гибкие размеры вместо фиксированных. - Нельзя запрещать масштаб через
viewport.
Почитать на сайте
Спецификация: WCAG 2.2: Reflow
На сайте курса есть видеоуроки и фоновое видео на главной. Что нужно сделать, чтобы ими могли пользоваться глухие и слабослышащие, незрячие и люди, которым мешает движение?
Интервьюер может уточнить
- Чем субтитры (
captions) отличаются отsubtitles? - Почему браузеры блокируют автовоспроизведение со звуком?
Разбор
- Субтитры — текст речи и важных звуков синхронно с видео. Подключаются файлом WebVTT:
<track kind="captions" src="lesson.vtt" srclang="ru" label="Русский">. Автоматические субтитры нужно вычитать: ошибки в терминах сбивают с толку. - Расшифровка — весь текст урока под видео. Её можно прочитать, найти поиском, перевести, и она помогает тем, кто смотрит без звука в транспорте.
- Описание изображения — если в видео показывают код или схему и не проговаривают, незрячий пользователь это пропустит. Решение — проговаривать в самом уроке или дать текстовое описание.
- Управление — атрибут
controlsили свои кнопки, доступные с клавиатуры и с подписями. - Фоновое видео —
muted, кнопка паузы, остановка приprefers-reduced-motion: reduce. WCAG требует способ приостановить, остановить или скрыть автоматически начавшееся движение длительностью больше 5 секунд, показанное параллельно другому содержимому, если оно не существенно для действия.
Для аудио, например подкаста, нужна расшифровка.
Уточняющие вопросы
Чем субтитры (captions) отличаются от subtitles?
captions — для тех, кто не слышит: речь плюс звуки («звонит телефон»). subtitles — перевод речи для тех, кто слышит, но не знает языка.
Почему браузеры блокируют автовоспроизведение со звуком?
Внезапный звук мешает всем и перекрывает речь скринридера. Браузеры разрешают автозапуск только без звука или после действия пользователя.
- Названы субтитры через
<track>. - Названа текстовая расшифровка или описание того, что видно только на экране.
- Фоновое видео без звука, с паузой или с учётом
prefers-reduced-motion.
Почитать на сайте
Спецификация: WCAG 2.2: субтитры
Дизайнер хочет выпадающий список городов с поиском и иконками, которые нельзя сделать в обычном <select>. Что придётся реализовать самому, чтобы список был доступен, и какие есть альтернативы?
Интервьюер может уточнить
- Почему фокус не переводят на сами пункты списка?
- Как проверить готовый компонент?
Разбор
Нативный <select> уже умеет всё: клавиатура, поиск по первой букве, скринридер, мобильный интерфейс выбора. Своя замена должна повторить это вручную:
- роли и связи: поле
role="combobox"сaria-expanded,aria-controlsиaria-autocomplete="list"; всплывающийrole="listbox"с пунктамиrole="option"иaria-selected; - клавиатура: стрелки вниз и вверх по пунктам, Enter выбирает, Escape закрывает и возвращает фокус, Home и End, ввод фильтрует;
- активный пункт — фокус остаётся в поле, а текущий пункт указывает
aria-activedescendant; - объявления: «Найдено 5 городов» через живую область;
- мелочи: прокрутка к активному пункту, щелчок мимо закрывает, работа на сенсорных экранах и при увеличении.
Прежде чем писать: <input list="cities"> с <datalist> даёт подсказки без кода (оформить их нельзя); в новых браузерах <select> можно стилизовать через appearance: base-select; готовые библиотеки компонентов проверены со скринридерами. Иконки часто можно отказаться показывать в самом списке без потери смысла.
Уточняющие вопросы
Почему фокус не переводят на сами пункты списка?
Тогда человек не сможет продолжать печатать запрос. aria-activedescendant позволяет оставить фокус в поле и одновременно сообщать скринридеру текущий пункт.
Как проверить готовый компонент?
Пройти сценарий только с клавиатуры, затем с NVDA и VoiceOver, и на телефоне с TalkBack и VoiceOver: выбрать город, исправить выбор, закрыть без выбора.
- Назван шаблон combobox с ролями
combobox,listbox,option. - Описана клавиатура: стрелки, Enter, Escape.
- Назван
aria-activedescendantили объявление результатов. - Названы альтернативы:
<datalist>, стилизуемый<select>или библиотека.
Почитать на сайте
Спецификация: ARIA APG: шаблон combobox
На кнопке написано «Купить», а aria-label содержит «Добавить товар в корзину». Почему голосовая команда «Нажать Купить» может не сработать?
Интервьюер может уточнить
- Нужно ли писать
aria-labelна каждую кнопку? - Чем имя отличается от описания?
Разбор
Пользователь голосового управления ориентируется на видимую надпись, а программа может искать элемент по доступному имени. Несовпадение мешает найти кнопку. Если текст кнопки достаточен, отдельный aria-label не нужен.
Для уточнения используйте имя, начинающееся с видимой подписи, например aria-label="Купить рюкзак". Дополнительное объяснение вроде условий доставки связывают через aria-describedby, не заменяя имя. Проверьте вычисленное имя в дереве доступности.
Уточняющие вопросы
Нужно ли писать aria-label на каждую кнопку?
Нет, нативная кнопка обычно получает имя из своего текста. Лишний атрибут может создать противоречие.
Чем имя отличается от описания?
Имя отвечает «что это за действие», описание добавляет условия и подсказки. Они не взаимозаменяемы.
- Объяснена замена доступного имени через
aria-label. - Видимый текст включён в имя.
- Дополнительная подсказка отделена от имени.
Почитать на сайте
Спецификация: Первоисточник: accessibility
В форме две группы переключателей с ответами «Да» и «Нет». Как сделать понятным, на какой вопрос отвечает каждая группа?
Интервьюер может уточнить
- Достаточно ли визуально поставить заголовок рядом?
- Как отключить группу целиком?
Разбор
<fieldset>
<legend>Нужна доставка?</legend>
<label><input type="radio" name="delivery" value="yes">Да</label>
<label><input type="radio" name="delivery" value="no">Нет</label>
</fieldset>Другой вопрос получает другое name, иначе выбор во второй группе снимет ответ в первой. legend задаёт общий контекст, но не заменяет подписи отдельных вариантов. Рамку можно оформить CSS, сохранив семантику.
Уточняющие вопросы
Достаточно ли визуально поставить заголовок рядом?
Не всегда: семантическая группа связывает вопрос и варианты для вспомогательных технологий.
Как отключить группу целиком?
fieldset disabled отключает вложенные элементы управления, кроме находящихся внутри его первого legend.
- Выбраны
fieldsetиlegend. - Каждый вариант имеет подпись.
- Разные вопросы используют разные имена групп.
Почитать на сайте
Спецификация: Первоисточник: accessibility
Карточки задач можно переставлять только перетаскиванием мышью. Как дать тот же результат пользователям клавиатуры и людям, которым трудно удерживать указатель?
Интервьюер может уточнить
- Почему недостаточно поменять CSS-свойство
order? - Как проверить сценарий без перетаскивания?
Разбор
Одного клавиатурного сочетания недостаточно для людей, использующих указатель, но не способных выполнить жест перетаскивания. Кнопки или меню позиции дают альтернативу без удержания.
Порядок DOM должен следовать новому порядку карточек. На границах списка соответствующее действие отключается, фокус не теряется. В заранее существующую область role="status" можно вывести «Задача перенесена на позицию 2 из 5». Перетаскивание остаётся дополнительным способом.
Уточняющие вопросы
Почему недостаточно поменять CSS-свойство order?
Визуальное положение изменится, но порядок чтения и клавиатурного обхода может остаться прежним.
Как проверить сценарий без перетаскивания?
Переместить задачу только кнопками указателя, затем только клавиатурой; проверить границы, фокус и объявление результата.
- Предложена альтернатива без жеста удержания.
- Альтернатива работает и с клавиатуры.
- Сохранены логичный порядок DOM, фокус и сообщение о результате.
Спецификация: Первоисточник: accessibility
После перехода на другую страницу внутри SPA экран изменился, но скринридер продолжает читать старую ссылку, а название вкладки прежнее. Что нужно обновить?
Интервьюер может уточнить
- Почему недостаточно изменить только URL?
- Нужно ли объявлять весь текст страницы живой областью?
Разбор
Используйте ссылки с реальными href, чтобы сохранялись открытие в новой вкладке и копирование адреса. После успешного перехода обновите document.title, текущий пункт навигации через aria-current="page" и фокус на заголовке с tabindex="-1" либо другом осмысленном элементе.
Не переносите фокус при каждом обновлении данных или фильтра: это не переход страницы. Возврат назад требует продуманного восстановления позиции и фокуса. При долгой загрузке или ошибке сообщите состояние, сохранив доступный путь продолжения.
Уточняющие вопросы
Почему недостаточно изменить только URL?
Адрес сам по себе не перемещает фокус и не сообщает новое название и содержимое при клиентской подмене страницы.
Нужно ли объявлять весь текст страницы живой областью?
Нет: это перегружает чтение. Достаточно понятного заголовка, стратегии фокуса и короткого состояния при необходимости.
- Обновлены название документа и текущий пункт навигации.
- Предложена стратегия фокуса после перехода.
- Сохранены реальные ссылки и поведение возврата.
- Навигация отличена от фонового обновления данных.
Спецификация: Первоисточник: accessibility
Страница каталога выросла до одного большого компонента: фильтры, товары, запросы и корзина. По каким признакам разделять её на части?
Интервьюер может уточнить
- Обязан ли каждый выделенный компонент использоваться дважды?
- Почему нельзя передать каждой карточке весь объект страницы?
Разбор
Фильтры описывают выбранные условия, список показывает товары, карточка представляет один товар. Связь с загрузкой и состоянием остаётся у осмысленного владельца; дочерним частям передают необходимые данные и действия.
Граница полезна, когда часть можно понять, изменить и проверить без чтения всей страницы. Не создавайте десятки обёрток, которые лишь переименовывают один тег. После разделения должно быть понятно, кто принимает решение, а кто только отображает результат.
Уточняющие вопросы
Обязан ли каждый выделенный компонент использоваться дважды?
Нет: самостоятельная ответственность и читаемость могут оправдывать компонент даже для одного места.
Почему нельзя передать каждой карточке весь объект страницы?
Контракт становится неявным и слишком широким. Карточка начинает зависеть от деталей, которые ей не нужны.
- Границы выбраны по обязанностям.
- У частей определены входы и действия.
- Владелец данных остаётся понятным после разделения.
Спецификация: React: разбиение интерфейса
У общей кнопки появились параметры isCart, isProfile и isCheckout. Как сохранить переиспользование, не заставляя UI-компонент знать все страницы?
Интервьюер может уточнить
- Что делать, если один экран требует уникального оформления?
- Можно ли общей кнопке самой обращаться к API заказа?
Разбор
Вместо флагов страниц используйте осмысленные варианты дизайна и содержимое. Если оформление различается из-за задачи, назовите сам вариант, например основное или опасное действие. Проверку прав и отправку заказа выполняет вызывающий слой.
Не объединяйте несовместимые элементы лишь потому, что они похожи: ссылка для перехода и кнопка для действия имеют разные контракты. Композиция позволяет собрать бизнес-кнопку поверх общей, сохранив доступное имя, клавиатуру и состояния.
Уточняющие вопросы
Что делать, если один экран требует уникального оформления?
Оценить, является ли это новым общим вариантом или локальной композицией. Не добавлять флаг с названием страницы автоматически.
Можно ли общей кнопке самой обращаться к API заказа?
Для базового UI-компонента это лишняя бизнес-зависимость; действие передают снаружи.
- Общий UI отделён от бизнес-сценария.
- Параметры описывают осмысленные варианты.
- Сохранена семантика кнопки или ссылки.
Спецификация: React: композиция через props
Правило скидки одинаково нужно в карточке, корзине и тесте. Сейчас каждый компонент считает его сам. Куда вынести правило и от чего оно должно зависеть?
Интервьюер может уточнить
- Почему не вынести расчёт в хук только ради повторного использования?
- Как проверять такую функцию?
Разбор
Функция получает сумму, условия скидки и согласованное правило округления. Отображение валюты и запрос подтверждённой цены остаются отдельными задачами. Так изменение правила в одном месте отражается во всех потребителях.
Если правило зависит от времени, передавайте нужную дату или часы явно, а не читайте текущее время внутри без возможности контроля. Клиентский расчёт служит интерфейсу, но окончательную стоимость подтверждает сервер. Общее правило не требует объединять всю связанную разметку.
Уточняющие вопросы
Почему не вынести расчёт в хук только ради повторного использования?
Если React-возможности не нужны, обычная функция проще и доступна другим окружениям.
Как проверять такую функцию?
По независимым входам и ожидаемым результатам, включая границы скидки и округление, без запуска интерфейса.
- Правило выделено в функцию с явными входами.
- Нет ненужной зависимости от UI или глобального store.
- Расчёт отделён от форматирования и серверного подтверждения.
Спецификация: React: чистые вычисления
API отдаёт цену строкой и дату в нестандартном поле. Компоненты по-разному преобразуют их и проверяют ошибки. Как организовать границу между API и интерфейсом?
Интервьюер может уточнить
- Достаточно ли написать
as Product[]? - Где выбирать текст ошибки для пользователя?
Разбор
Адаптер различает HTTP-ошибку, некорректный JSON и неподходящую структуру. Он переводит внешние поля в согласованные типы, сохраняя договорённость о единицах цены и часовом поясе.
Не превращайте любой отказ в пустой массив: «товаров нет» и «товары не удалось загрузить» — разные состояния. При изменении API адаптер локализует часть изменений, но не отменяет обновление контракта и проверок. Для теста можно подставить реализацию этой границы, явно указав, что транспорт тогда не проверяется.
Уточняющие вопросы
Достаточно ли написать as Product[]?
Нет: утверждение TypeScript не проверяет внешние данные во время работы.
Где выбирать текст ошибки для пользователя?
На уровне сценария интерфейса, опираясь на классифицированную ошибку; детали транспорта не обязаны становиться пользовательским текстом.
- Внешние данные проверяются на границе.
- Преобразование централизовано в адаптере.
- Ошибка загрузки не подменяется пустым результатом.
Спецификация: TypeScript: ограничения утверждений типов
Модуль корзины импортирует каталог, каталог — пользовательский профиль, а профиль — корзину. Чем опасна такая связь и как начать её распутывать?
Интервьюер может уточнить
- Решит ли проблему общий файл, который переэкспортирует всё?
- Когда помогает
import type?
Разбор
Не любой цикл немедленно ломает программу, но чтение ещё не инициализированного экспорта может привести к ошибке. Простая замена импорта на динамический не устраняет неправильную ответственность.
Например, функция корзины может принимать идентификатор пользователя аргументом вместо импорта всего профиля. Общий тип товара может быть независимым контрактом. Публичные экспорты модуля делают намеренными, а доступ к внутренним файлам ограничивают. Не создавайте свалку shared, куда без разбора перемещены все зависимости.
Уточняющие вопросы
Решит ли проблему общий файл, который переэкспортирует всё?
Не обязательно: он может скрыть или создать дополнительные циклы. Нужно проверить реальный граф зависимостей.
Когда помогает import type?
Когда зависимость действительно нужна только для типов: такой импорт не создаёт runtime-связь. Архитектурный смысл зависимости всё равно нужно оценить.
- Названы риски инициализации и связанности.
- Определены конкретные зависимости, а не только имена папок.
- Предложены явные входы или независимый общий контракт.
Спецификация: TypeScript: модули
Изменение оформления заказа требует правок в общих папках компонентов, hooks, store и utils. Когда группировка по функции продукта полезнее группировки только по типу файла?
Интервьюер может уточнить
- Когда код выносить в общий модуль?
- Как предотвратить возврат глубоких импортов?
Разбор
Например, оформление заказа объединяет свои компоненты, модель и API-адаптер. Общие UI-примитивы и независимые сущности остаются отдельно. Другие модули обращаются через публичные экспорты, а не импортируют произвольные внутренние файлы.
Нужны правила: какие слои могут зависеть от каких, где собираются сценарии, что действительно является общим. Feature-Sliced Design — один из способов формализовать такие границы, а не обязательное название структуры любого проекта. Перенос файлов без ограничения связей сам по себе архитектуру не исправляет.
Уточняющие вопросы
Когда код выносить в общий модуль?
Когда у него есть самостоятельный стабильный смысл и несколько реальных потребителей, а не только похожие строки.
Как предотвратить возврат глубоких импортов?
Зафиксировать публичные точки входа и проверять ограничения импортов линтером или анализом зависимостей.
- Граница соответствует функции продукта.
- Есть публичный контракт и скрытые внутренние детали.
- Заданы правила зависимостей и критерии общего кода.
Спецификация: Feature-Sliced Design: slices и segments
Нужно сделать публичный каталог и личный кабинет. Как выбирать между статической генерацией, серверным и клиентским рендерингом, и откуда берутся ошибки hydration?
Интервьюер может уточнить
- Почему SSR не гарантирует мгновенную интерактивность?
- Стоит ли подавлять все hydration-предупреждения?
Разбор
Редко меняющиеся публичные страницы можно готовить заранее и кешировать. Персональные или часто меняющиеся данные могут требовать запроса к серверу либо клиентской загрузки; способы можно сочетать по маршрутам и областям.
Разные дата, случайное значение, локаль или чтение localStorage на первом клиентском рендере способны создать несовпадение с HTML сервера. Передавайте согласованные начальные данные и откладывайте браузерные различия по явной стратегии. SSR не убирает стоимость JavaScript и не разрешает хранить данные всех запросов в одном глобальном пользовательском store.
Уточняющие вопросы
Почему SSR не гарантирует мгновенную интерактивность?
Клиенту всё ещё может понадобиться загрузить и выполнить JavaScript и связать обработчики с интерфейсом.
Стоит ли подавлять все hydration-предупреждения?
Нет: сначала ищут различие исходных данных и рендера. Подавление для отдельных неизбежных различий не исправляет общий контракт.
- Сравнены свежесть, персонализация и стоимость.
- Объяснено требование согласованного первого рендера.
- Учтены JavaScript и изоляция серверных запросов.
Спецификация: React: hydrateRoot
Главная страница загружает редактор изображений, которым пользуются редко. Где провести границу ленивой загрузки и как избежать новой цепочки ожиданий?
Интервьюер может уточнить
- Что показать, если chunk не загрузился?
- Почему много маленьких chunks не всегда лучше?
Разбор
Начните с анализа сборки: действительно ли редактор попадает в начальный пакет и какие зависимости дают размер. Динамический import() позволяет сборщику выделить отдельный chunk; в React для компонента можно использовать lazy с подходящей границей Suspense.
Необходимые для первого экрана части не стоит откладывать автоматически. При вероятном переходе возможна предварительная загрузка, но она расходует сеть. Проверьте время появления содержимого, отзывчивость и поведение после новой выкладки, когда старый клиент запросит уже удалённый chunk.
Уточняющие вопросы
Что показать, если chunk не загрузился?
Понятную ошибку и действие повтора или обновления страницы с сохранением важных данных; не бесконечный индикатор.
Почему много маленьких chunks не всегда лучше?
Появляются накладные расходы и цепочки зависимостей; решение оценивают по сетевому графу и реальному сценарию.
- Граница выбрана по независимой тяжёлой функции.
- Есть загрузка и обработка отказа.
- Проверены зависимости и реальные метрики вместо одного размера файла.
Спецификация: webpack: разделение кода
Старый кабинет нужно перенести на новую архитектуру, но разработка функций не должна остановиться. Как уменьшить риск по сравнению с заменой всего приложения за один релиз?
Интервьюер может уточнить
- Как выбрать первую область миграции?
- Почему наличие feature flag ещё не гарантирует откат?
Разбор
Начните с карты зависимостей и одного самостоятельного сценария. Новая часть взаимодействует со старой через адаптер или согласованный API. Маршрут либо флаг включения направляет пользователей в нужную реализацию, а метрики показывают ошибки и успешное завершение сценария.
Не поддерживайте бесконечно две независимые копии изменяемых данных. Заранее определите владельца, совместимость форматов и условия удаления старого кода. Откат интерфейса должен учитывать, сможет ли старая версия читать уже записанные новые данные.
Уточняющие вопросы
Как выбрать первую область миграции?
Ограниченный полезный сценарий с понятными входами, измеримым результатом и небольшим числом связей.
Почему наличие feature flag ещё не гарантирует откат?
Данные, API или формат хранения могли измениться несовместимо; переключение флага не обращает эти изменения автоматически.
- Выбрана ограниченная область за стабильной границей.
- Есть проверка поведения, наблюдаемость и откат.
- Учтены владельцы данных и совместимость форматов.
Спецификация: Azure Architecture: постепенная замена системы
Команда спорит, нужен ли отдельный store или достаточно локального состояния и кеша запросов. Как принять решение так, чтобы его можно было пересмотреть через полгода?
Интервьюер может уточнить
- Нужно ли менять решение после появления более популярной библиотеки?
- Что даёт небольшой прототип?
Разбор
Опишите потребителей состояния, сложность переходов, требования к отладке, SSR, persistence, тестированию и знаниям команды. Сравните несколько подходов на характерном сценарии, включая вариант без новой зависимости.
В коротком ADR сохраните контекст, решение, альтернативы и принятые компромиссы. Добавьте признаки, при которых выбор перестанет подходить: например, рост межэкранных обновлений или проблемы подписок. Это не обещание универсально лучшей архитектуры и не документ на десятки страниц.
Уточняющие вопросы
Нужно ли менять решение после появления более популярной библиотеки?
Само по себе это не причина. Важны изменившиеся требования, измеримые проблемы или стоимость поддержки.
Что даёт небольшой прототип?
Он проверяет наиболее рискованные предположения на реальном сценарии, но его удачный демопоказ не заменяет оценку эксплуатации.
- Выбор связан с требованиями и ограничениями.
- Сравнены альтернативы и последствия.
- Решение записано вместе с условиями пересмотра.
Спецификация: AWS: архитектурные решения
В корзине есть расчёт скидки, форма промокода и оформление заказа. Что проверить unit-тестом, интеграционным тестом и сквозным сценарием?
Интервьюер может уточнить
- Можно ли назвать любой тест в браузере сквозным?
- Почему не проверить всё только сквозными тестами?
Разбор
Для расчёта проверяют входные данные и ожидаемую сумму без DOM и сети. Для формы вводят промокод, отправляют её и проверяют итог и ошибки, оставляя реальные связи между компонентами. Сетевую границу можно подменить управляемыми ответами.
Сквозной тест проходит оформление в браузере с тестовым сервером и данными. Внешнюю оплату можно заменить sandbox или заглушкой, явно обозначив границу. Названия уровней в командах различаются: важнее сказать, что реально проверено и что подменено.
Уточняющие вопросы
Можно ли назвать любой тест в браузере сквозным?
Нет: браузерный тест отдельного компонента может оставаться компонентным или интеграционным. Среда запуска сама по себе не задаёт охват.
Почему не проверить всё только сквозными тестами?
Они дороже и сложнее диагностируются. Правила расчёта быстрее проверить отдельно, а сквозные тесты оставить для важных связей и путей.
- Для расчёта выбран изолированный тест.
- Интеграционный тест проверяет реальные связи нескольких частей.
- У сквозного сценария обозначены инфраструктура и подмены.
Спецификация: Redux: интеграционный подход к тестам
Скидка 10% действует при сумме от 1000 рублей включительно. Какие проверки полезнее десяти случайных сумм и как получить ожидаемый результат?
Интервьюер может уточнить
- Почему важно значение ровно 1000?
- Нужно ли повторять формулу функции в ожидаемом значении?
Разбор
При целых копейках примеры: 99900 копеек остаются 99900, 100000 превращаются в 90000, 101000 — в 90900. Эти эталоны взяты из правила, а не из повторного вызова проверяемой функции.
Тест разделяют на подготовку, действие и проверку. Отдельно решают, что происходит с отрицательной суммой и как округляется дробная копейка: тест должен защищать договорённость, а не случайное поведение текущей реализации.
Уточняющие вопросы
Почему важно значение ровно 1000?
Оно различает условия >= и > и ловит ошибку включённости границы.
Нужно ли повторять формулу функции в ожидаемом значении?
Нет: одинаковая ошибка может оказаться и в реализации, и в тесте. Для небольшого набора используют независимо рассчитанные результаты.
- Названы значения по обе стороны порога и на нём.
- Ожидаемые результаты рассчитаны независимо.
- Определены единицы денег и правило округления.
Спецификация: Vitest: написание тестов
Тест формы проверяет вызов внутреннего setError, но после рефакторинга ломается, хотя пользователь по-прежнему видит ошибку. Что лучше проверять?
Интервьюер может уточнить
- Когда проверка вызова функции уместна?
- Почему поиск только по CSS-классу хрупок?
Разбор
Найдите поле по подписи и кнопку по роли и имени, введите неверное значение и отправьте форму. Проверьте текст ошибки, связь с полем и отсутствие успешного результата. Затем исправьте ввод и убедитесь, что форма продолжает работать.
Не обращайтесь к экземпляру компонента и его внутренним переменным без необходимости. Такая проверка допускает смену хука или структуры компонентов, но обнаруживает исчезнувшее сообщение.
Уточняющие вопросы
Когда проверка вызова функции уместна?
Когда вызов — часть публичного контракта, например переданного onSubmit с оговорёнными данными или внешнего адаптера.
Почему поиск только по CSS-классу хрупок?
Класс может измениться ради оформления. Роль и доступное имя ближе к способу, которым пользователь находит элемент.
- Проверяется поведение после пользовательского действия.
- Элементы найдены по роли или подписи.
- Внутренняя реализация не закреплена без необходимости.
Спецификация: Testing Library: принципы
Тест нажимает «Загрузить» и сразу ищет результат через getByText. Иногда он падает. Как дождаться результата без фиксированной паузы?
Интервьюер может уточнить
- Почему увеличение паузы не решает проблему надёжно?
- Как проверить, что индикатор загрузки исчез?
Разбор
В Testing Library ожидайте await user.click(button), затем await screen.findByRole("heading", { name: "Результаты" }). getBy ищет сразу и бросает ошибку при отсутствии; queryBy удобен для проверки отсутствия; findBy повторяет поиск до успеха или тайм-аута.
Для других асинхронных условий есть waitFor с проверкой, которая бросает ошибку до выполнения условия. Не помещайте повторяемое действие пользователя внутрь waitFor: callback может выполниться много раз.
Уточняющие вопросы
Почему увеличение паузы не решает проблему надёжно?
На медленном окружении её всё равно может не хватить, а на быстром она зря замедлит каждый запуск.
Как проверить, что индикатор загрузки исчез?
Дождаться его удаления или повторять проверку отсутствия; это проверка состояния, а не задержка наугад.
- Асинхронные действия и поиск ожидаются.
- Различены
getBy,queryByиfindBy. - Повторяемое ожидание не содержит пользовательского действия.
Спецификация: Testing Library: запросы
Тест проходит отдельно, но падает после другого теста. Оба используют общий store и меняют время. С чего начать исправление?
Интервьюер может уточнить
- Поможет ли принудительно запускать тесты в нужном порядке?
- Всегда ли достаточно очистить историю вызовов мока?
Разбор
Тест не должен зависеть от порядка выполнения. Общий singleton-store может сохранить пользователя или ошибку предыдущего сценария; аналогично ведут себя кеш запросов, storage и глобальные моки.
Создавайте окружение в подготовке теста, а восстановление обеспечивайте даже при падении проверки. Для параллельных браузерных сценариев нужны отдельные аккаунты или уникальные данные, если операции меняют сервер. Повторный запуск полезен для диагностики, но не устраняет зависимость.
Уточняющие вопросы
Поможет ли принудительно запускать тесты в нужном порядке?
Это скрывает связь. Лучше сделать сценарии независимыми или явно оформить общий сценарий одним тестом.
Всегда ли достаточно очистить историю вызовов мока?
Нет: история вызовов и подменённая реализация — разные вещи. Нужно восстановить именно то состояние, которое менял тест.
- Найдены возможные общие store, кеши и глобальные подмены.
- Окружение создаётся независимо для каждого теста.
- Очистка выполняется и при неуспешной проверке.
Спецификация: Vitest: подмены и очистка
Нужно проверить форму сохранения профиля вместе со store и API-клиентом. Что оставить настоящим, где подменить сеть и какие исходы проверить?
Интервьюер может уточнить
- Как не получить ложный успех на неожиданном запросе?
- Нужен ли настоящий сервер в каждом таком тесте?
Разбор
Можно использовать MSW для управляемых ответов HTTP. Тест заполняет форму и отправляет её обычным действием; обработчик сети проверяет метод и полезную нагрузку. Затем интерфейс должен отразить результат и снять состояние загрузки.
Отдельные сценарии — ошибка проверки, сетевой отказ и повтор после исправления. Не подменяйте одновременно хук загрузки, store и дочерний компонент: иначе связи, ради которых нужен интеграционный тест, исчезнут. Подмена сети не доказывает совместимость с настоящим сервером; её проверяют отдельно.
Уточняющие вопросы
Как не получить ложный успех на неожиданном запросе?
Настроить тестовое окружение так, чтобы необработанные запросы считались ошибкой, кроме явно разрешённых.
Нужен ли настоящий сервер в каждом таком тесте?
Нет, если проверяется клиентская интеграция. Для контракта и развёрнутой системы нужны отдельные проверки с сервером.
- Сохранены реальные связи интерфейса, store и API-клиента.
- Сеть подменена на явной границе.
- Проверены успех и отказ с сохранением ввода.
- Назван предел проверки с подменённым сервером.
Спецификация: MSW: подмена сетевой границы
Поиск ждёт 300 мс после последнего ввода. Как проверить debounce быстро и убедиться, что закрытая форма не отправит отложенный запрос?
Интервьюер может уточнить
- Почему
runAllTimersопасен для бесконечного интервала? - Проверяет ли такой тест реальную скорость сервера?
Разбор
После первого ввода продвиньте часы на 299 мс и убедитесь, что запроса нет. Затем имитируйте новый ввод, снова выдержите 299 мс без запроса и ещё 1 мс: должен уйти запрос для последнего значения. Обновления React и промисы нужно корректно ожидать средствами тестового окружения.
В сценарии закрытия создайте ожидающий таймер, размонтируйте форму и продвиньте время: запроса быть не должно. После теста восстановите реальные таймеры. При использовании user-event согласуйте его собственные задержки с продвижением тестовых часов.
Уточняющие вопросы
Почему runAllTimers опасен для бесконечного интервала?
Он может запускать постоянно создаваемые таймеры. Лучше продвигать конкретный интервал или выполнять только ожидаемую работу.
Проверяет ли такой тест реальную скорость сервера?
Нет: он проверяет планирование и отмену работы, а не производительность сети.
- Проверено отсутствие запроса до порога.
- После нового ввода задержка начинается заново.
- Проверена отмена при закрытии и восстановлены часы.
Спецификация: Vitest: управление таймерами
Отчёт показывает 100% покрытия строк функции оплаты, но тесты не замечают неверную сумму. Что означает покрытие и как оценить качество проверок?
Интервьюер может уточнить
- Почему исполненная ветка может остаться непроверенной по смыслу?
- Стоит ли одинаково тестировать расчёт оплаты и декоративную обёртку?
Разбор
Тест может вызвать функцию и не проверить сумму вообще. Покрытие ветвей полезнее одного покрытия строк для поиска пропущенных условий, но также не гарантирует проверку всех сочетаний входов.
Сначала перечислите риски: повторное списание, неверное округление, отказ и частичный результат. Проверьте, падают ли тесты при осмысленной ошибке в реализации; автоматизировать такую оценку помогает mutation testing. Метрику используют как подсказку для ревью, а не как самостоятельную цель.
Уточняющие вопросы
Почему исполненная ветка может остаться непроверенной по смыслу?
Код выполнился, но итог мог не участвовать ни в одном утверждении или ожидание повторяет ту же ошибочную формулу.
Стоит ли одинаково тестировать расчёт оплаты и декоративную обёртку?
Приоритет зависит от последствий ошибки и сложности поведения, а не от одинакового процента для каждого файла.
- Покрытие отделено от правильности ожиданий.
- Названы важные границы и ветви.
- Предложена проверка способности теста обнаружить ошибку.
Спецификация: Vitest: покрытие кода
Тест компонента в DOM-эмуляторе проходит, но в браузере выпадающая панель обрезается и недоступна с клавиатуры. Какие проверки нужно перенести в настоящий браузер?
Интервьюер может уточнить
- Что оставить в быстрых тестах без браузера?
- Нужен ли скриншот для проверки клавиатуры?
Разбор
В браузере откройте панель клавиатурой, пройдите её элементы и закройте Escape. Проверьте видимость, границы экрана, фокус после закрытия и узкий viewport. Локаторы по ролям и ожидающие проверки уменьшают зависимость от разметки и скорости.
Скриншот помогает обнаружить визуальное изменение, но сам по себе не доказывает доступность. Не пытайтесь исправить отсутствие layout в эмуляторе случайными координатами: такая подмена лишь проверяет собственную заглушку.
Уточняющие вопросы
Что оставить в быстрых тестах без браузера?
Чистые расчёты, преобразования данных и состояния, которым не нужны измерения раскладки.
Нужен ли скриншот для проверки клавиатуры?
Нет, нужно выполнить клавиатурный сценарий и проверить фокус; изображение не показывает доступность действия.
- Названы ограничения DOM-эмулятора.
- Проверены реальная видимость и клавиатурный сценарий.
- Визуальная проверка не подменяет проверку поведения.
Спецификация: Playwright: практики браузерных тестов
Интеграционные тесты зелёные, но после обновления бэкенда форма падает: мок возвращает старое поле name, сервер — displayName. Как обнаруживать такие расхождения?
Интервьюер может уточнить
- Достаточно ли TypeScript-типа ответа?
- Заменяют ли контрактные тесты сквозной сценарий?
Разбор
Опишите обязательные поля, допустимые значения и ошибки. Генерация типов из схемы помогает находить часть изменений при сборке, но тип не проверяет фактический JSON. На границе клиента можно валидировать данные и преобразовывать их в модель интерфейса.
Контрактные тесты проверяют ожидания потребителя на поставщике API. Даже без специальной платформы полезна небольшая проверка ключевых ответов тестового сервера по схеме. Управляемые моки остаются для ошибок и редких состояний; успешный мок не гарантирует совместимость развёрнутых версий.
Уточняющие вопросы
Достаточно ли TypeScript-типа ответа?
Нет: во время работы сервер может прислать другую структуру, а типы уже удалены.
Заменяют ли контрактные тесты сквозной сценарий?
Нет, они проверяют договорённость между частями, а не весь пользовательский путь и окружение.
- Определён единый контракт данных и ошибок.
- Моки сверяются с контрактом.
- Проверка типов отделена от реального JSON и проверки поставщика.
Спецификация: Pact: контрактное тестирование
Компонент получает товар через props, но пользователь может раскрыть его описание. Какие данные должны приходить снаружи, а какие можно хранить в компоненте?
Интервьюер может уточнить
- Как сообщить родителю о выборе товара?
- Нужно ли хранить итоговую цену отдельно, если она получается из цены и количества?
Разбор
Название и цена приходят от владельца данных. Флаг раскрытия можно хранить через useState, если он нужен только карточке. Нельзя менять объект товара из props: это изменит общие данные без явного обновления.
Если родителю нужно закрывать все карточки, он хранит состояние и передаёт значение вместе с обработчиком изменения. Не копируйте props в state без причины: копия перестанет следовать последующим изменениям.
Уточняющие вопросы
Как сообщить родителю о выборе товара?
Вызвать переданный обработчик, например onSelect(product.id), а изменение данных оставить владельцу.
Нужно ли хранить итоговую цену отдельно, если она получается из цены и количества?
Обычно нет: её вычисляют из исходных данных, чтобы не синхронизировать несколько копий.
- Props не изменяются компонентом.
- Локальное состояние выбрано по области использования.
- Общее состояние имеет одного владельца.
Спецификация: React: props и состояние
В одном обработчике трижды вызывается setCount(count + 1), но счётчик растёт только на один. Почему и как увеличить его на три последовательными обновлениями?
Интервьюер может уточнить
- Что выведет чтение
countсразу послеsetCount? - Можно ли в функции обновления отправить аналитику?
Разбор
Если count равен 0, три вызова setCount(count + 1) ставят в очередь замену на 1. Значение переменной внутри уже выполняющегося обработчика не меняется.
Функциональное обновление описывает переход из предыдущего состояния. Обновляющая функция должна быть чистой: в ней нельзя отправлять запросы или уведомления. Это правило очереди состояния, а не совет добавлять задержку между вызовами.
Уточняющие вопросы
Что выведет чтение count сразу после setCount?
Значение из того рендера, которому принадлежит обработчик. Новое значение будет доступно при следующем рендере.
Можно ли в функции обновления отправить аналитику?
Нет: React может вызывать её повторно для проверки чистоты. Аналитику действия размещают в обработчике события.
- Назван снимок состояния текущего рендера.
- Предложено функциональное обновление.
- Функция обновления не содержит побочных эффектов.
Спецификация: React: очередь обновлений
После сортировки списка текст в полях карточек оказался у других товаров. В качестве key использован индекс. Как ключ связан с состоянием компонента?
Интервьюер может уточнить
- Почему
Math.random()не подходит для ключа? - Когда индекс не создаёт такого риска?
Разбор
При сортировке элемент на позиции 0 становится другим товаром, но ключ 0 остаётся прежним. React может сохранить состояние поля на этой позиции. Используйте key={product.id}.
Ключ должен быть стабильным, уникальным среди соседей и не генерироваться случайно при рендере. Смена ключа намеренно пересоздаёт поддерево и сбрасывает состояние. Сам key не передаётся компоненту как обычный prop: идентификатор передают отдельно.
Уточняющие вопросы
Почему Math.random() не подходит для ключа?
Ключ каждый раз меняется: компоненты пересоздаются, теряют состояние и фокус.
Когда индекс не создаёт такого риска?
В действительно статичном списке без перестановки, удаления и добавления; стабильный ID всё равно предпочтительнее при его наличии.
- Ключ связан с идентичностью элемента, а не порядком отображения.
- Для товаров выбран стабильный ID.
- Объяснены последствия случайного или изменившегося ключа.
Спецификация: React: ключи списков
Поле React получает value={name}, но не даёт печатать. Как устроено управляемое поле и когда вместо value нужен defaultValue?
Интервьюер может уточнить
- Как сделать поле намеренно только для чтения?
- Как прочитать неуправляемую форму при отправке?
Разбор
Для текстового поля используйте строковое начальное состояние и пару value={name}, onChange={event => setName(event.target.value)}. Для флажка управляют checked, а не текстовым value.
Без обновления состояния React возвращает заданное значение после ввода. Не переключайте поле неожиданно из неуправляемого в управляемое: например, заменой undefined на строку после загрузки. Подпись label нужна при обоих подходах.
Уточняющие вопросы
Как сделать поле намеренно только для чтения?
Указать readOnly и объяснить его назначение; отсутствие обработчика не заменяет явный контракт.
Как прочитать неуправляемую форму при отправке?
Через FormData у формы; у полей должны быть имена name.
- Связаны значение и обработчик обновления.
defaultValueотличён от текущего управляемого значения.- Для флажка названо свойство
checked.
Спецификация: React: input
Нужно хранить идентификатор таймера и показывать число оставшихся секунд. Что положить в useRef, а что в useState?
Интервьюер может уточнить
- Подойдёт ли обычная переменная в теле компонента?
- Когда безопасно обращаться к DOM через ref?
Разбор
Ref сохраняет изменяемое значение между рендерами и позволяет обратиться к DOM-элементу. Но React не отслеживает изменения current для обновления интерфейса. Если время должно меняться на экране, обновляют state.
При создании таймера в эффекте очистка отменяет его. Если ID нужен только этой очистке, достаточно локальной переменной внутри эффекта; ref нужен, когда к ID обращаются другие обработчики. Не храните в ref скрытую копию отображаемого состояния.
Уточняющие вопросы
Подойдёт ли обычная переменная в теле компонента?
Она создаётся заново при каждом вызове компонента и не является устойчивым хранилищем между рендерами.
Когда безопасно обращаться к DOM через ref?
После commit, например в обработчике события или подходящем эффекте; при рендере DOM может ещё не существовать.
- Отображаемое значение хранится в state.
- Изменение ref не вызывает рендер.
- Для таймера предусмотрена очистка.
Спецификация: React: ref и state
Компонент хранит список товаров и строку поиска, а отфильтрованный список записывает в state через эффект. Что здесь можно упростить?
Интервьюер может уточнить
- Как проверить, нужна ли мемоизация?
- Когда копия данных в локальном состоянии оправдана?
Разбор
Вычислите const visible = products.filter(matchesQuery) из актуальных props и state. Если фильтрация действительно дорогая, можно применить useMemo с полными зависимостями, но корректность не должна зависеть от кеша.
Эффект нужен для синхронизации с внешней системой: подпиской, таймером или сторонним виджетом. Действие пользователя, например покупку, выполняют в его обработчике. Производное значение не становится внешней системой только потому, что зависит от состояния.
Уточняющие вопросы
Как проверить, нужна ли мемоизация?
Измерить стоимость вычисления и задержку сценария на реалистичных данных, затем сравнить результат оптимизации.
Когда копия данных в локальном состоянии оправдана?
Например, независимый редактируемый черновик. Тогда явно задают момент его создания, сброса и сохранения.
- Производные данные вычисляются без эффекта.
- Объяснены лишний рендер и риск рассогласования.
- Мемоизация отделена от корректности логики.
Спецификация: React: когда эффект не нужен
Компонент подписывается на чат по roomId. После смены комнаты сообщения приходят из старой, а при возвращении дублируются. Как организовать эффект?
Интервьюер может уточнить
- Почему опасно подавить предупреждение линтера о зависимости?
- Почему нельзя просто сделать функцию эффекта
async?
Разбор
Внутри эффекта создайте подключение, подключите его и верните функцию отключения. Все прочитанные props, state и другие реактивные значения учитываются в зависимостях. Объект настроек часто проще создать внутри эффекта, чем делать его новым зависимым объектом на каждом рендере.
Очистка выполняется также при размонтировании. Асинхронный результат старой подписки не должен менять состояние новой комнаты: нужен механизм отмены или проверки актуальности. Пустой массив зависимостей не исправляет устаревшее замыкание.
Уточняющие вопросы
Почему опасно подавить предупреждение линтера о зависимости?
Эффект может продолжить использовать значения прежнего рендера и перестать следовать реальным входным данным.
Почему нельзя просто сделать функцию эффекта async?
Она вернёт промис вместо функции очистки. Асинхронную работу запускают внутри эффекта и отдельно возвращают синхронную очистку.
- Подписка связана с текущим
roomId. - Возвращается очистка конкретного подключения.
- Учтены зависимости и устаревшие асинхронные результаты.
Спецификация: React: жизненный цикл эффектов
При разработке под StrictMode настройка эффекта выполняется, очищается и выполняется снова. Коллега предлагает убрать StrictMode. Что стоит проверить сначала?
Интервьюер может уточнить
- Как распознать мутацию при рендере?
- Является ли дополнительная проверка гарантией отсутствия утечек?
Разбор
Когда Strict Mode включён на корне приложения, React в разработке дополнительно проверяет эффекты циклом setup → cleanup → setup. Также повторные вызовы функций, которые должны быть чистыми, выявляют мутации во время рендера. Это не означает, что все действия пользователя в production выполняются дважды.
Подписка должна отключаться, таймер — отменяться. Покупку или отправку формы нельзя переносить в эффект монтирования: это действие конкретного события. Ref-флаг «уже запускали» может скрыть ошибку очистки вместо её исправления.
Уточняющие вопросы
Как распознать мутацию при рендере?
Например, компонент добавляет элемент в массив из props, и повторный вызов добавляет его ещё раз. Нужно создать новую структуру без изменения входа.
Является ли дополнительная проверка гарантией отсутствия утечек?
Нет, она помогает обнаружить часть ошибок; реальные сценарии открытия, закрытия и смены параметров всё равно проверяют.
- Дополнительный цикл отнесён к проверкам разработки.
- Проверены чистота рендера и симметрия очистки.
- Действия пользователя не запускаются из эффекта монтирования.
Спецификация: React: StrictMode
Список тормозит при вводе, и предлагается обернуть все компоненты в memo, а функции в useCallback. Как проверить, что оптимизация поможет?
Интервьюер может уточнить
- Остановит ли
memoобновление потребителя Context? - Что опасного в своей функции сравнения props?
Разбор
Проверьте объём списка, расположение состояния ввода, стоимость расчётов и необходимость виртуализации. По умолчанию memo сравнивает каждый prop через Object.is: новый объект или функция могут нарушить равенство.
useCallback сохраняет ссылку на функцию, а не ускоряет её выполнение. useMemo кеширует результат вычисления. Их применение имеет цену и требует корректных зависимостей. Если в проекте включён React Compiler, часть мемоизации выполняется автоматически; решение всё равно оценивают измерениями.
Уточняющие вопросы
Остановит ли memo обновление потребителя Context?
Нет: изменение используемого контекста может вызвать рендер независимо от равенства props.
Что опасного в своей функции сравнения props?
Если проигнорировать изменившийся обработчик или другие значимые данные, компонент продолжит использовать устаревшие значения. Само сравнение тоже может быть дорогим.
- Оптимизация начинается с измерения.
- Различены назначения
memo,useMemoиuseCallback. - Учтены ссылки props, state и Context.
Спецификация: React: memo и профилирование
Ошибка рендера виджета обрушила весь экран. Как ограничить область сбоя и поймает ли Error Boundary ошибку запроса в обработчике кнопки?
Интервьюер может уточнить
- Нужна ли граница вокруг каждой кнопки?
- Что показать при ошибке загрузки данных?
Разбор
Границы размещают вокруг самостоятельных областей: например, график может отказать, а фильтры и навигация остаются доступны. Запасной интерфейс сообщает о проблеме и даёт осмысленный повтор; ошибку отправляют в мониторинг.
При повторе важно сбросить состояние границы и устранить причину или заново получить данные. Ошибка самой границы передаётся выше. Обработчик сохранения отдельно ловит сетевой отказ и сохраняет введённые данные.
Уточняющие вопросы
Нужна ли граница вокруг каждой кнопки?
Обычно нет: выбирают области, которые могут независимо восстановиться, а не дробят интерфейс без сценария.
Что показать при ошибке загрузки данных?
Явное состояние ошибки с возможностью повторить запрос; оно не обязано возникать через исключение при рендере.
- Определена область изоляции сбоя.
- Ошибки рендера отличены от ошибок обработчиков и запросов.
- Предусмотрены запасной интерфейс, повтор и диагностика.
Спецификация: React: Error Boundary
Когда React перерисовывает компонент? Что такое «виртуальный DOM» и почему повторный рендер ещё не означает изменения страницы?
Интервьюер может уточнить
- Сбросится ли состояние поля, если вместо
<div>вокруг него на том же месте стал<section>? - Медленный ли повторный рендер сам по себе?
Разбор
Причины повторного рендера:
- вызван
setStateс новым значением; - изменилось значение контекста, который компонент читает через
useContext; - перерисовался родитель — по умолчанию вместе с ним рендерятся все дочерние компоненты, даже если их props не изменились.
Рендер — это только вызов функции компонента: она возвращает дерево элементов, лёгких объектов с описанием интерфейса. Это описание иногда называют виртуальным DOM. React сравнивает новое дерево с предыдущим и применяет к настоящему DOM минимальные изменения: если текст и атрибуты те же, DOM не трогается.
При сравнении элемент другого типа на том же месте пересоздаётся целиком вместе с состоянием, а в списках элементы сопоставляются по key.
Уточняющие вопросы
Сбросится ли состояние поля, если вместо <div> вокруг него на том же месте стал <section>?
Да: тип элемента на этой позиции изменился, React удалит старое поддерево и создаст новое, состояние внутри потеряется.
Медленный ли повторный рендер сам по себе?
Обычно нет: вызов функций и сравнение дешевле работы с DOM. Проблемы начинаются, когда рендер тяжёлый или затрагивает тысячи компонентов, — это видно в профайлере.
- Названы причины рендера: состояние, контекст, рендер родителя.
- Рендер отличён от изменения DOM.
- Объяснено сравнение деревьев и роль типа элемента и
key.
Спецификация: React: Render and Commit
Фильтр каталога и список товаров — соседние компоненты. Фильтр хранит выбранную категорию у себя, а списку она тоже нужна. Как передать её списку?
Интервьюер может уточнить
- Почему не синхронизировать две копии состояния через
useEffect? - Что такое prop drilling и когда он действительно проблема?
Разбор
Соседи не могут читать состояние друг друга. Общий родитель, например Catalog, хранит category через useState и передаёт:
- фильтру —
value={category}иonChange={setCategory}; - списку —
category={category}, по которой он фильтрует товары.
Фильтр становится управляемым: он не хранит своё значение, а показывает то, что пришло, и сообщает о выборе. У каждого значения один источник правды, поэтому фильтр и список не рассогласуются.
Если категорию нужно передать через много уровней, рассматривают композицию (передачу готовых элементов через children) или Context, а для сохранения в ссылке — параметры адреса.
Уточняющие вопросы
Почему не синхронизировать две копии состояния через useEffect?
Две копии расходятся хотя бы на один рендер, а эффекты добавляют лишние циклы обновления. Проще иметь одно значение у владельца.
Что такое prop drilling и когда он действительно проблема?
Передача props через компоненты, которым они не нужны. Два-три уровня — нормально; при глубокой цепочке помогают композиция и Context.
- Состояние поднято к общему родителю.
- Названы передача значения через props и изменения через колбэк.
- Упомянут единый источник правды.
Спецификация: React: Sharing State Between Components
В трёх компонентах повторяется одинаковый код: состояние «онлайн/офлайн» и подписка на события online и offline. Как вынести его в свой хук и какие правила хуков нужно соблюдать?
function StatusIndicator() {
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 online ? 'Online' : 'Offline';
}
Интервьюер может уточнить
- Почему нельзя вызвать хук внутри
if? - Нужно ли называть хуком функцию, которая не вызывает хуков?
Разбор
function useOnlineStatus() {
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 online;
}Свой хук — обычная функция с именем на use, которая вызывает другие хуки. Компонент пишет const online = useOnlineStatus() и не знает о подписках.
Важно:
- каждый вызов хука создаёт отдельное состояние — два компонента не делят одно значение, как и при прямом использовании
useState; - подписка в эффекте обязательно снимается в функции очистки;
- правила хуков: вызывать на верхнем уровне, в одном и том же порядке при каждом рендере, только из компонентов и других хуков. React различает хуки по порядку вызова.
Префикс use нужен, чтобы линтер проверял правила хуков внутри функции.
Уточняющие вопросы
Почему нельзя вызвать хук внутри if?
При другом результате условия изменится порядок вызовов, и React сопоставит состояние не с тем хуком.
Нужно ли называть хуком функцию, которая не вызывает хуков?
Нет, это обычная функция-утилита. Префикс use ставят, только если внутри есть хуки.
- Логика вынесена в функцию с префиксом
use. - Подписка снимается в функции очистки.
- Сказано, что состояние у каждого вызова своё.
- Названы правила хуков: верхний уровень, постоянный порядок.
Спецификация: React: Reusing Logic with Custom Hooks
Профиль загружается в useEffect по userId. При быстром переключении пользователей иногда показывается чужой профиль. Почему и как исправить?
useEffect(() => {
const controller = new AbortController();
fetch('/api/users/' + userId, { signal: controller.signal })
.then((response) => response.json())
.then(setUser)
.catch((error) => {
if (error.name !== 'AbortError') setError(error);
});
return () => controller.abort();
}, [userId]);
Интервьюер может уточнить
- Почему в разработке запрос уходит дважды?
- Можно ли сделать саму функцию эффекта
async?
Разбор
При смене userId React выполняет очистку прошлого эффекта и запускает новый. Без очистки оба запроса живут, и тот, что завершился последним, записывает свой результат — даже если он устарел.
Очистка с controller.abort() отменяет прошлый запрос, а ошибку AbortError не показывают пользователю. Вариант попроще — флаг let ignore = false, который очистка ставит в true.
Ещё нужны состояния загрузки и ошибки и проверка response.ok. Кеширование, повтор и предзагрузку руками писать долго, поэтому используют TanStack Query, SWR или загрузку данных фреймворка.
Уточняющие вопросы
Почему в разработке запрос уходит дважды?
StrictMode в разработке монтирует компонент, очищает эффект и монтирует снова. С правильной очисткой первый запрос отменяется, в production такого нет.
Можно ли сделать саму функцию эффекта async?
Нет: эффект должен вернуть функцию очистки, а не промис. Асинхронную функцию объявляют внутри эффекта и вызывают.
- Названа гонка ответов.
- Устаревший запрос отменяется или игнорируется в очистке.
- Упомянуты состояния загрузки и ошибки.
Спецификация: React: Fetching data with Effects
Код {count && <Badge count={count} />} при нуле товаров выводит на странице «0». Почему и как правильно выводить элемент по условию?
Интервьюер может уточнить
- Чем скрытие через условие отличается от CSS
display: none? - Что вернуть из компонента, если показывать нечего?
Разбор
0 && x равно 0. React не рисует false, true, null и undefined, но число и строку выводит. Поэтому появляется «0».
Способы условного вывода:
{count > 0 && <Badge … />}— явное логическое условие;{isLoading ? <Spinner /> : <List />}— выбор из двух вариантов;- ранний
return nullили переменная с элементом доreturn— когда вариантов больше.
То же случается с items.length && … для пустого массива.
Уточняющие вопросы
Чем скрытие через условие отличается от CSS display: none?
Условие удаляет компонент вместе с его состоянием, а CSS оставляет его в дереве и сохраняет состояние.
Что вернуть из компонента, если показывать нечего?
null: компонент ничего не выведет.
- Объяснено, что
&&возвращает левый операнд. - Сказано, что React выводит 0, но не
falseиnull. - Предложено логическое условие или тернарный оператор.
Спецификация: React: Conditional Rendering
Когда useMemo и useCallback действительно помогают, а когда только усложняют код? Почему useCallback без memo у дочернего компонента обычно бесполезен?
Интервьюер может уточнить
- Гарантирует ли
useMemo, что вычисление не повторится? - Как проверить, что мемоизация помогла?
Разбор
Разумные случаи:
- дорогое вычисление при рендере — сортировка или фильтрация тысяч элементов, которое видно в профайлере;
- значение или функция уходит в компонент, обёрнутый в
memo: новая ссылка на каждом рендере отменила бы пропуск рендера; - объект или функция — зависимость
useEffect: без стабильной ссылки эффект перезапускался бы каждый раз.
Цена: код сложнее, зависимости нужно поддерживать, а кеш занимает память. Обернуть всё подряд — не оптимизация.
Часто лучше изменить структуру: опустить состояние ниже, передать тяжёлую часть через children, вынести объект за пределы компонента. React Compiler умеет мемоизировать автоматически, и в проектах с ним ручные useMemo нужны реже.
Уточняющие вопросы
Гарантирует ли useMemo, что вычисление не повторится?
Нет, это оптимизация производительности, а не гарантия: React может сбросить кеш. Код должен работать правильно и без него.
Как проверить, что мемоизация помогла?
Сравнить время рендера в React Profiler до и после на одном сценарии, желательно с замедлением процессора.
- Различены кеширование значения и функции.
- Названы случаи пользы: дорогое вычисление,
memo, зависимости эффекта. - Объяснено, почему без
memoу ребёнка нет выигрыша. - Упомянуто измерение профайлером.
Спецификация: React: useMemo
Страница настроек и редактор отчётов загружаются вместе со всем приложением. Как подгружать их только при переходе, что показывать во время загрузки и что делать, если файл не загрузился?
const ReportEditor = lazy(() => import('./ReportEditor'));
function ReportsPage() {
return (
<ErrorBoundary fallback={<LoadError />}>
<Suspense fallback={<Spinner />}>
<ReportEditor />
</Suspense>
</ErrorBoundary>
);
}
Интервьюер может уточнить
- Почему при переключении вкладок уже показанный интерфейс вдруг заменяется спиннером?
- Что ещё умеет Suspense, кроме ленивого кода?
Разбор
lazy принимает функцию с динамическим import(); модуль должен экспортировать компонент по умолчанию. Объявлять lazy нужно на верхнем уровне модуля: внутри компонента он создавался бы заново на каждом рендере и сбрасывал состояние.
Suspense показывает fallback, пока какой-то компонент внутри не готов. Где поставить границу — решение об интерфейсе: одна граница на страницу проще, несколько позволяют показывать готовые части раньше.
Если после выкладки новой версии старый файл удалён, импорт упадёт — его перехватывает Error Boundary. Чтобы переход не ждал загрузки, код можно начать загружать заранее, например при наведении на ссылку.
Уточняющие вопросы
Почему при переключении вкладок уже показанный интерфейс вдруг заменяется спиннером?
Новый ленивый компонент приостановил рендер, и ближайшая граница показала fallback. Обновление можно пометить через startTransition — тогда React оставит прежний интерфейс до готовности.
Что ещё умеет Suspense, кроме ленивого кода?
Ждать данные в фреймворках и библиотеках, которые его поддерживают, — например, в серверных компонентах или use с промисом.
- Названы
lazyи динамическийimport(). - Объяснена роль
Suspenseиfallback. - Ошибка загрузки обрабатывается Error Boundary.
Спецификация: React: lazy
Поле поиска фильтрует большой список прямо при вводе, и буквы появляются с задержкой. Чем помогут useTransition и useDeferredValue и чем они отличаются от debounce?
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
// <input value={query} onChange={(e) => setQuery(e.target.value)} />
// <List query={deferredQuery} dim={isStale} />
Интервьюер может уточнить
- Ускоряет ли переход сам рендер списка?
- Для чего
isPending?
Разбор
В обоих случаях React сначала рендерит срочное обновление — поле с новым текстом, а затем в фоне готовит тяжёлое. Если пришёл новый ввод, незаконченный фоновый рендер выбрасывается.
useTransitionвозвращает[isPending, startTransition]; внутриstartTransitionставят несрочныйsetState. Подходит, когда вы управляете местом обновления.useDeferredValue(value)— когда значение приходит снаружи, например через props: компонент получает отстающую версию.
Чтобы это работало, тяжёлый список обычно оборачивают в memo, иначе он рендерится и при срочном обновлении. Управляемое поле нельзя обновлять внутри перехода — ввод станет отставать.
Debounce уменьшает число запросов к серверу, а переходы — нет. Для сетевого поиска их часто сочетают.
Уточняющие вопросы
Ускоряет ли переход сам рендер списка?
Нет: работа та же, но она не блокирует ввод. Если рендер очень тяжёлый, нужна виртуализация или меньший объём.
Для чего isPending?
Показать, что результат обновляется: например, приглушить старый список, пока готовится новый.
- Разделены срочное и несрочное обновления.
- Сказано, что фоновый рендер прерывается новым вводом.
- Различены
useTransitionиuseDeferredValue. - Объяснено отличие от debounce.
Спецификация: React: useTransition
Всплывающая подсказка внутри карточки с overflow: hidden обрезается. Как помогает createPortal и как ведут себя события и контекст у компонента в портале?
Интервьюер может уточнить
- Куда портал выводит разметку при серверном рендеринге?
- Почему обработчик на
document, закрывающий меню «по клику снаружи», может сломаться с порталом?
Разбор
Портал меняет только место в DOM. Поэтому:
- подсказку или окно больше не обрезает
overflowкарточки и не ограничивает еёz-index; - Context продолжает работать — портал видит провайдеры своих React-предков;
- события всплывают по дереву React: клик внутри портала дойдёт до
onClickкарточки, хотя в DOM её там нет. Иногда это неожиданно, и всплытие приходится останавливать.
Доступность остаётся задачей разработчика: перенести фокус, вернуть его после закрытия, закрыть по Escape, а для модального окна сделать фон неактивным. Для модального окна нативный <dialog> с showModal() выводится в верхний слой браузера и часто избавляет от портала.
Уточняющие вопросы
Куда портал выводит разметку при серверном рендеринге?
На сервере нет document, поэтому порталы рендерят на клиенте — например, после монтирования.
Почему обработчик на document, закрывающий меню «по клику снаружи», может сломаться с порталом?
Проверка contains по DOM считает клик в портале внешним: элемента нет внутри DOM-узла меню. Нужно проверять и узел портала.
- Портал выводит разметку в другой DOM-узел.
- Контекст сохраняется.
- События всплывают по дереву React.
- Упомянуты фокус и доступность или
<dialog>.
Спецификация: React: createPortal
В проекте на Next.js с App Router одни компоненты помечены 'use client', другие нет. Чем серверные компоненты отличаются от клиентских и что можно передавать между ними?
Интервьюер может уточнить
- Это то же самое, что SSR?
- Как не дать серверному модулю с секретами случайно попасть в клиентский код?
Разбор
Серверные компоненты (React Server Components) рендерятся на сервере или при сборке, и клиент получает результат, а не их JavaScript. Их сильные стороны — прямой доступ к данным и меньший bundle.
Клиентские компоненты нужны для интерактивности: useState, эффекты, обработчики, браузерные API. Они тоже рендерятся на сервере в HTML, а затем гидратируются.
Правила на границе:
- props должны быть сериализуемыми: строки, числа, обычные объекты, массивы, даты, промисы; функции — только серверные действия (
'use server'); - клиентский компонент не может импортировать серверный, но может получить его как
children; 'use client'ставят как можно ниже по дереву, чтобы не тянуть в браузер лишнее.
Секреты нельзя передавать в props клиентского компонента: они окажутся в данных страницы.
Уточняющие вопросы
Это то же самое, что SSR?
Нет. SSR превращает в HTML и клиентские компоненты, а потом их код всё равно загружается. Серверные компоненты своего кода в браузер не отправляют вовсе.
Как не дать серверному модулю с секретами случайно попасть в клиентский код?
Импортировать в нём пакет server-only: сборка упадёт, если модуль окажется в клиентском bundle.
- Серверный компонент не отправляет свой код в браузер и не имеет состояния.
- Объяснена роль
'use client'как границы. - Названы ограничения props: сериализуемость,
children.
Спецификация: React: Server Components
В приложении есть открытый tooltip, выбранная валюта и содержимое корзины. Нужно ли сразу перенести всё в общий store?
Интервьюер может уточнить
- Когда локальное состояние стоит поднять выше?
- Сохранит ли Redux или Zustand данные после перезагрузки автоматически?
Разбор
Для каждого значения выясните, кто его читает и изменяет, должно ли оно пережить смену страницы и есть ли у него серверный владелец. Если состояние нужно двум соседним компонентам, часто достаточно общего родителя. Общий store полезен при действительно распределённом доступе и сложных переходах.
Отдельно определите хранение: общий store в памяти сам по себе не сохраняет данные после перезагрузки. Серверная корзина требует синхронизации, а её отображаемая копия не должна становиться независимым источником истины.
Уточняющие вопросы
Когда локальное состояние стоит поднять выше?
Когда несколько частей должны согласованно читать или менять одно значение; выбирают ближайшего подходящего владельца.
Сохранит ли Redux или Zustand данные после перезагрузки автоматически?
Нет, для этого нужна отдельная политика persistence или загрузка с сервера.
- Решение основано на потребителях и времени жизни.
- Локальные значения не перенесены в общий store без причины.
- Общее состояние отличено от постоянного хранения.
Спецификация: React: совместное состояние
Тема оформления нужна во многих ветках React. Когда достаточно Context и почему он сам по себе не заменяет все возможности стейт-менеджера?
Интервьюер может уточнить
- Context обязательно должен содержать state?
- Почему один контекст «всё приложение» может быть неудобен?
Разбор
Для темы можно хранить состояние в provider и передавать значение с действием переключения. Когда value меняется, использующие этот Context потребители получают обновление. Новый объект в value при каждом рендере может вызывать лишние обновления.
Разделяйте контексты с разной частотой изменения и областью использования. memo не блокирует обновление используемого контекста. Если нужна подписка на небольшие части большого часто меняющегося состояния, сравните возможности специализированного store.
Уточняющие вопросы
Context обязательно должен содержать state?
Нет: он может передавать неизменяемую конфигурацию или ссылку на сервис. Context отвечает за доступ к значению.
Почему один контекст «всё приложение» может быть неудобен?
Изменение одной части значения затронет многих потребителей и размоет границы ответственности.
- Context описан как передача значения по дереву.
- Объяснены обновления потребителей при смене значения.
- Названы задачи, которые Context сам не решает.
Спецификация: React: useContext
У формы много флагов: загрузка, ошибка, успех. Можно получить одновременно успех и ошибку. Как помогут явные состояния и reducer?
Интервьюер может уточнить
- Где запускать запрос сохранения?
- Что делать с повторным нажатием во время сохранения?
Разбор
Например, форма бывает idle, saving, success или error. Действие начала сохранения убирает предыдущую ошибку, успешный ответ содержит результат, отказ — причину. Переходы можно проверить отдельно от React.
Reducer должен быть чистым: без запросов, таймеров и изменения внешних объектов. useReducer не делает состояние глобальным: оно остаётся у вызвавшего компонента, пока вы явно не передадите его другим. Для гонок нескольких запросов может потребоваться идентификатор актуальной операции.
Уточняющие вопросы
Где запускать запрос сохранения?
В обработчике действия или другом выделенном слое побочных эффектов; затем передать reducer событие результата.
Что делать с повторным нажатием во время сохранения?
Определить контракт: игнорировать, отменить предыдущую операцию или поставить следующую. Это правило перехода, а не случайный набор флагов.
- Описаны допустимые состояния и события.
- Reducer вычисляет переход без побочных эффектов.
- Определено поведение повторного запуска.
Спецификация: React: выделение логики в reducer
Store хранит товары корзины, их число и общую стоимость. После удаления товара стоимость иногда остаётся старой. Как уменьшить риск рассогласования?
Интервьюер может уточнить
- Нужно ли мемоизировать чтение одного поля?
- Почему селектор не должен делать
sortисходного массива?
Разбор
Количество и клиентскую предварительную сумму получают из актуальных позиций. Селектор — функция чтения и преобразования состояния; он не должен менять store. Для дорогих расчётов может понадобиться мемоизация, но сначала оценивают стоимость.
Подтверждённая сервером сумма заказа — другой факт: её нельзя подменять клиентским расчётом, если сервер применяет скидки, доставку или налоги. В интерфейсе различают предварительную и подтверждённую стоимость.
Уточняющие вопросы
Нужно ли мемоизировать чтение одного поля?
Обычно нет: простое чтение дешёвое. Мемоизация полезна для дорогого вычисления или стабильности результата при конкретной модели подписки.
Почему селектор не должен делать sort исходного массива?
Обычный sort меняет store при чтении. Сортируют копию или используют toSorted.
- Производные значения не дублируются без причины.
- Селектор остаётся чистой функцией.
- Предварительный расчёт отличён от подтверждённого сервером.
Спецификация: Redux: производные данные и селекторы
Список товаров с сервера нужен на нескольких экранах. Чем кеш TanStack Query или RTK Query отличается от ручного массива в общем store?
Интервьюер может уточнить
- Где хранить открытую вкладку или текст несохранённого поля?
- Почему не копировать каждый ответ Query ещё и в Redux-срез?
Разбор
Сервер остаётся владельцем данных; клиент хранит временную копию. Ключ запроса должен включать параметры, влияющие на результат, например категорию и страницу. Иначе разные экраны могут ошибочно разделить одну запись кеша.
Кеш не означает «никогда не запрашивать снова». Нужно понимать свежесть, время удаления неиспользуемых данных и условия повторной загрузки. В TanStack Query staleTime отвечает за свежесть, gcTime — за хранение неактивной записи; это разные настройки.
Уточняющие вопросы
Где хранить открытую вкладку или текст несохранённого поля?
Обычно в локальном или клиентском состоянии: это не ответ сервера и не обязательно задача кеша запросов.
Почему не копировать каждый ответ Query ещё и в Redux-срез?
Появятся два хранилища одного факта с отдельными правилами обновления. Если копия нужна как черновик, её жизненный цикл задают явно.
- Различены серверные данные и состояние интерфейса.
- Параметры результата включены в ключ запроса.
- Свежесть отличена от удаления записи кеша.
Спецификация: TanStack Query: настройки кеша
В reducer обычного Redux нельзя менять массив, а в createSlice пишут state.items.push(item). Почему это не противоречие и где заканчивается разрешение на мутацию?
Интервьюер может уточнить
- Почему
state = newStateвнутри reducer не заменяет состояние? - Можно ли сохранить draft и изменить его позже в таймере?
Разбор
Можно изменить draft либо вернуть новое состояние. Не стоит одновременно менять draft и возвращать другой результат. Стрелка state => state.items.push(item) неявно возвращает длину массива; используйте тело в фигурных скобках без возврата.
Reducer остаётся чистым: запросы и таймеры выполняются вне него. Изменяемый синтаксис — удобство подготовки следующего состояния, а не побочный эффект над предыдущим. Обычный Redux reducer без Immer требует самостоятельного копирования изменяемых веток.
Уточняющие вопросы
Почему state = newState внутри reducer не заменяет состояние?
Это переназначение локального параметра. Для полной замены нужно return newState.
Можно ли сохранить draft и изменить его позже в таймере?
Нет: draft относится к текущему вызову подготовки состояния и не должен выходить за его пределы.
- Объяснён draft Immer.
- Различены изменение draft и возврат нового состояния.
- Мутации снаружи reducer и побочные эффекты не разрешены.
Спецификация: Redux Toolkit: reducers с Immer
Компонент Zustand читает весь store и обновляется при каждом изменении чужого поля. Как сузить подписку и правильно обновлять вложенный объект?
Интервьюер может уточнить
- Почему селектор
state => ({ count: state.count })требует внимания? - Когда нужно функциональное обновление
set?
Разбор
Вместо useStore() используйте, например, useStore(state => state.cartCount). Если селектор возвращает новый объект или массив, его ссылочная стабильность требует внимания; для набора полей может подойти useShallow или отдельные селекторы.
Для вложенного имени: set(state => ({ user: { ...state.user, name } })). Поверхностное слияние не сохраняет вложенные поля само. Immer возможен через явное подключение, но не является поведением любого set по умолчанию.
Уточняющие вопросы
Почему селектор state => ({ count: state.count }) требует внимания?
Он создаёт новую ссылку при вызове. Это может вызвать лишние обновления и проблемы стабильности snapshot; результат нужно стабилизировать подходящим способом.
Когда нужно функциональное обновление set?
Когда новое значение вычисляется из предыдущего: функция получает актуальное состояние для этого обновления.
- Подписка ограничена нужным срезом.
- Учтена стабильность ссылок результата селектора.
- Вложенная структура обновляется с явным копированием.
Спецификация: Zustand: селекторы и обновления
Поле в MobX-store меняется, но React-компонент не обновляется. Как связаны observable, observer и чтение значения при рендере?
Интервьюер может уточнить
- Подпишется ли компонент на все поля переданного store?
- Зачем
computedвместо отдельного поля с итогом?
Разбор
Проверьте, что поле действительно наблюдаемое и компонент обёрнут в observer. Передавать ссылку на объект и читать поле внутри наблюдаемого компонента обычно полезнее, чем заранее вычитать примитив вне отслеживаемого рендера.
Вычисляемые значения описывают как computed, изменения группируют в action. После await продолжение уже не принадлежит синхронному действию: при соответствующей политике изменений используют runInAction или flow. Нельзя механически переносить модель неизменяемых reducer на реактивную модель MobX.
Уточняющие вопросы
Подпишется ли компонент на все поля переданного store?
Нет: важны observable-значения, прочитанные при отслеживаемом рендере.
Зачем computed вместо отдельного поля с итогом?
Он выражает производное значение и отслеживает его зависимости, уменьшая ручную синхронизацию.
- Проверены наблюдаемость данных и
observer. - Названо чтение значения в отслеживаемом контексте.
- Для асинхронных изменений учтены границы action.
Спецификация: MobX: интеграция с React
После нажатия «Нравится» счётчик меняется сразу, а сервер иногда отклоняет запрос. Как организовать оптимистичное обновление и что делать с несколькими быстрыми нажатиями?
Интервьюер может уточнить
- Зачем отменять текущую загрузку перед временным изменением кеша?
- Достаточно ли всегда вернуть сохранённый снимок при ошибке?
Разбор
В кеше запросов обычно отменяют конфликтующую загрузку, сохраняют исходное значение, применяют временное изменение, а после завершения инвалидируют или согласуют запись. Интерфейс явно сообщает об отказе.
Простой откат целого снимка опасен при нескольких незавершённых мутациях: ошибка первой может стереть успешную вторую. Можно сериализовать операции по сущности, хранить журнал изменений или использовать версии. Выбор зависит от операции; для критичного действия оптимистичное подтверждение может быть неуместно.
Уточняющие вопросы
Зачем отменять текущую загрузку перед временным изменением кеша?
Её старый ответ может затереть оптимистичный результат. Нужно согласовать конкурирующие обновления.
Достаточно ли всегда вернуть сохранённый снимок при ошибке?
Только если за это время не возникло других изменений. Иначе нужен более точный откат или получение актуального состояния.
- Есть временное обновление и обработка отказа.
- Результат согласуется с сервером.
- Учтена гонка нескольких операций и риск отката старого снимка.
Спецификация: TanStack Query: optimistic updates
Store сохраняют целиком в localStorage. После обновления приложения часть пользователей видит сломанный интерфейс, а после выхода — данные прежнего аккаунта. Как спроектировать сохранение?
Интервьюер может уточнить
- Что делать с неизвестной версией записи?
- Почему нельзя сохранять
isLoading: trueкак обычную настройку?
Разбор
Флаги загрузки, ошибки, функции и экземпляры соединений обычно не являются сохраняемыми данными. Настройки пользователя и черновик требуют отдельного контракта. При смене аккаунта старые записи и кеши очищают или отделяют по владельцу.
Для SSR сервер не может прочитать браузерное хранилище: первый клиентский рендер должен согласоваться с серверным, а восстановление проводится по выбранной стратегии. Недоступность storage не должна ломать базовый интерфейс. Persistence не заменяет серверную проверку прав или безопасное хранение секретов.
Уточняющие вопросы
Что делать с неизвестной версией записи?
Применить поддерживаемую миграцию либо безопасно отклонить запись и сообщить о невозможности восстановить важный черновик.
Почему нельзя сохранять isLoading: true как обычную настройку?
После перезапуска исходного запроса может уже не быть, а интерфейс останется в вечной загрузке.
- Выбран ограниченный сериализуемый набор данных.
- Есть версия, проверка и миграция или сброс.
- Учтены смена аккаунта и восстановление при SSR.
Спецификация: Redux: сериализуемое состояние
Проект содержит TypeScript, CSS и импорты картинок. Что происходит между запуском команды сборки и появлением файлов, которые можно отдать браузеру?
Интервьюер может уточнить
- Чем точка входа отличается от выходного файла?
- Зачем сборщику граф зависимостей?
Разбор
Разрешение импорта определяет, какой файл соответствует имени модуля. Преобразование убирает типы, обрабатывает JSX или CSS согласно конфигурации. По графу зависимостей сборщик решает, какие модули войдут в выходные пакеты и какие ресурсы нужно скопировать.
Проверка типов, линтер и тесты могут быть отдельными шагами: успешная упаковка не гарантирует их прохождение. Результат проверяют как готовое приложение, включая пути к ресурсам и режим сервера.
Уточняющие вопросы
Чем точка входа отличается от выходного файла?
Точка входа запускает обход зависимостей; выходных файлов может быть несколько, включая общие и отложенные chunks.
Зачем сборщику граф зависимостей?
Чтобы включить нужные модули, определить общие зависимости и обновлять затронутые части при изменениях.
- Описаны точки входа и разрешение импортов.
- Названы преобразование, оптимизация и выходные ресурсы.
- Сборка отделена от проверки типов и тестов.
Спецификация: webpack: основные понятия
На ноутбуке проект собирается, а в CI после установки зависимостей падает. Как помогают lockfile и npm ci и что ещё нужно зафиксировать?
Интервьюер может уточнить
- Почему диапазон версии в
package.jsonнедостаточен? - Можно ли установить только production-зависимости перед frontend-сборкой?
Разбор
В отличие от обычной установки, npm ci требует lockfile и завершится ошибкой, если он расходится с package.json. Существующая папка node_modules перед установкой удаляется. Обновление зависимости и lockfile проверяют вместе.
Одинаковое дерево не гарантирует побитово одинаковую сборку: влияют ОС, нативные зависимости, переменные окружения, время и скачивание внешних данных. Сравните логи и окружение, а не удаляйте lockfile как универсальный способ исправления.
Уточняющие вопросы
Почему диапазон версии в package.json недостаточен?
При новом разрешении диапазона может установиться другая подходящая версия и другое дерево транзитивных зависимостей.
Можно ли установить только production-зависимости перед frontend-сборкой?
Не всегда: сборщик и компилятор часто находятся в devDependencies и нужны на этапе сборки, даже если не нужны для отдачи готовых файлов.
- Объяснены назначение lockfile и поведение
npm ci. - Проверены версии инструментов и параметры установки.
- Установка зависимостей отделена от полной воспроизводимости артефакта.
Спецификация: npm: ci
Production-сборка TypeScript прошла, хотя в коде строка передаётся вместо числа. Почему преобразование TypeScript не всегда ловит ошибки типов?
Интервьюер может уточнить
- Заменяет ли линтер проверку типов?
- Почему нельзя считать dev-сервер проверкой production-сборки?
Разбор
Быстрое преобразование отдельного файла может выдать JavaScript, не анализируя совместимость типов между модулями. В Vite проверку типов рекомендуют запускать отдельно от преобразования.
В CI шаги установки, проверки типов, тестирования и сборки должны иметь явные команды и коды завершения. Для фреймворков с особыми файлами выбирают соответствующий checker. Типы всё равно не проверяют структуру входящего JSON во время работы.
Уточняющие вопросы
Заменяет ли линтер проверку типов?
Нет: назначение зависит от правил и конфигурации, обычный линт не является полной проверкой типов проекта.
Почему нельзя считать dev-сервер проверкой production-сборки?
Он использует другой путь обработки и может обслуживать лишь запрошенные модули; часть ошибок обнаружится только в отдельной проверке или сборке.
- Преобразование отделено от семантической проверки.
- Предложен явный шаг typecheck.
- Отмечено отсутствие runtime-проверки внешних данных.
Спецификация: Vite: TypeScript
После настройки старого target код запускается в нужном браузере, но падает на Array.prototype.toSorted. Почему target не гарантирует наличие всех API?
Интервьюер может уточнить
- Добавит ли смена
libреализацию метода в JavaScript? - Почему наличие метода недостаточно проверять только на своём компьютере?
Разбор
Преобразовать синтаксис стрелочной функции и добавить отсутствующий метод массива — разные операции. Даже типы lib в TypeScript лишь описывают доступный API для компилятора, а не устанавливают его в браузер.
Определите поддерживаемые окружения и проверьте конкретные возможности. Полифил должен загрузиться до использования и соответствовать требуемой семантике; некоторые браузерные возможности полноценно воспроизвести нельзя. Не добавляйте все полифилы без оценки их размера.
Уточняющие вопросы
Добавит ли смена lib реализацию метода в JavaScript?
Нет, она меняет доступные объявления типов, а не поведение среды.
Почему наличие метода недостаточно проверять только на своём компьютере?
Пользователь может работать в другой версии браузера или WebView; нужна проверка целевой матрицы окружений.
- Различены синтаксис и API среды.
- Типовые объявления не названы полифилом.
- Предложена проверка целевых браузеров и осмысленный fallback.
Спецификация: Vite: совместимость production-сборки
Ключ внешнего сервиса записали в VITE_API_KEY и считают секретным, потому что файл .env не попадает в Git. Что окажется в клиентской сборке?
Интервьюер может уточнить
- Почему условие по строке
"false"может неожиданно включить функцию? - Можно ли спрятать секрет минификацией?
Разбор
В Vite переменные с префиксом VITE_ предназначены для клиентского доступа. Они обычно читаются как строки: значение "false" не является логическим false. Адрес публичного API может быть конфигурацией клиента, приватный ключ сервиса — нет.
Сборочное значение фиксируется при создании артефакта. Изменение окружения сервера, который только раздаёт готовые файлы, не перепишет этот JavaScript. Для одного артефакта на несколько окружений можно спроектировать загрузку публичной runtime-конфигурации.
Уточняющие вопросы
Почему условие по строке "false" может неожиданно включить функцию?
Непустая строка истинна при логическом приведении; значение нужно явно разобрать по контракту.
Можно ли спрятать секрет минификацией?
Нет: браузер должен получить данные для выполнения кода, а минификация не является шифрованием или контролем доступа.
- Клиентские переменные признаны публичными.
- Build-time конфигурация отделена от runtime.
- Учтён строковый тип значений окружения.
Спецификация: Vite: переменные окружения
Локально сайт работает из корня, а после публикации в /docs/ теряет стили и картинки. Что проверить в сборке и путях ресурсов?
Интервьюер может уточнить
- Почему открыть готовый HTML через файловый протокол недостаточно?
- Как отличить неверный путь скрипта от ошибки самого скрипта?
Разбор
Настройка base помогает сборщику переписать известные ему ссылки. Произвольная строка в коде, например URL из внешних данных, не обязана участвовать в таком преобразовании. Для обрабатываемого ресурса используйте поддерживаемый импорт или другой документированный механизм.
Проверьте готовую сборку именно по будущему префиксу, включая прямое открытие вложенного маршрута. Корректные пути ресурсов и настройка сервера для маршрутов SPA — две отдельные задачи. Убедитесь, что вместо отсутствующего JS сервер не возвращает HTML с кодом 200.
Уточняющие вопросы
Почему открыть готовый HTML через файловый протокол недостаточно?
Модульная загрузка, origin и маршрутизация отличаются от HTTP-размещения; нужен сервер предпросмотра.
Как отличить неверный путь скрипта от ошибки самого скрипта?
Проверить URL, статус, MIME-тип и фактическое тело ответа в Network.
- Проверены base и абсолютные пути.
- Различены обработанные сборщиком ресурсы и произвольные URL.
- Готовый сайт проверен под реальным префиксом.
Спецификация: Vite: обработка статических ресурсов
После установки sideEffects: false bundle уменьшился, но исчезли стили и регистрация плагина. Как это связано с tree shaking?
Интервьюер может уточнить
- Поможет ли именованный импорт, если библиотека выполняет большую инициализацию?
- Почему проблема может не проявиться в dev?
Разбор
Неиспользуемый экспорт и эффект выполнения модуля — разные вещи. Файл может ничего не экспортировать, но регистрировать компонент или подключать стили. Такие файлы необходимо обозначить как имеющие побочные эффекты в соответствии с инструментом.
Статические ES-импорты облегчают анализ, но не гарантируют удаление всего лишнего. Проверьте production-артефакт, настройки оптимизации и реальные сценарии библиотеки. Не копируйте sideEffects: false как универсальный рецепт уменьшения размера.
Уточняющие вопросы
Поможет ли именованный импорт, если библиотека выполняет большую инициализацию?
Не обязательно: важные эффекты нельзя безопасно удалить только потому, что часть экспортов не используется.
Почему проблема может не проявиться в dev?
Production-путь включает более агрессивное исключение кода; dev может исполнять исходные модули иначе.
- Неиспользуемые экспорты отличены от эффектов импорта.
- Поле
sideEffectsописано как обещание сборщику. - Нужные CSS и регистрация сохранены и проверены в production.
Спецификация: webpack: tree shaking и side effects
Одна библиотека поставляет ESM и CommonJS. В Node.js тесты проходят, а браузерная сборка выбирает другой файл и падает. Как понять, какой вариант пакета используется?
Интервьюер может уточнить
- Почему нельзя просто импортировать любой файл из
node_modules? - Где искать доказательство выбора файла?
Разбор
Сопоставьте package.json зависимости, активные условия resolver и итоговый граф сборки. Условные экспорты могут направить разные потребители к разным файлам. Глубокий импорт внутреннего файла может обходить публичный контракт и перестать работать после обновления.
ESM/CommonJS interop не всегда даёт одинаковую форму default- и именованных экспортов. Проверьте фактическое значение импорта в проблемной среде и используйте документированный вход пакета. Две независимо загруженные реализации также могут дублировать состояние библиотеки.
Уточняющие вопросы
Почему нельзя просто импортировать любой файл из node_modules?
Пакет может закрыть внутренние пути через exports; внутреннее устройство не обязано быть стабильным API.
Где искать доказательство выбора файла?
В отчёте resolver, графе модулей или source map итогового артефакта, а не только в названии зависимости.
- Проверены условные экспорты и целевое окружение.
- Учтена форма ESM/CommonJS-импортов.
- Использован публичный вход пакета.
Спецификация: Node.js: пакеты и условные экспорты
В мониторинге ошибка указывает на одну строку минифицированного JS. Как source maps помогают найти исходник и что важно при их публикации?
Интервьюер может уточнить
- Зачем сохранять точный bundle вместе с картой?
- Достаточно ли включить карты только локально?
Разбор
Сохраните идентификатор релиза и согласованный комплект файлов. Карта от похожей, но заново собранной версии может давать неверные строки. Само отсутствие комментария sourceMappingURL не делает публично размещённую карту закрытой.
Карты могут включать исходный текст через sourcesContent, поэтому политика доступа должна быть осознанной. Даже приватные карты не позволяют хранить секреты в клиентском коде. Проверьте расшифровку тестовой ошибки для опубликованного артефакта.
Уточняющие вопросы
Зачем сохранять точный bundle вместе с картой?
Смещения относятся к конкретному выходному файлу; другая минификация или зависимости изменят соответствие.
Достаточно ли включить карты только локально?
Нет: мониторингу нужен доступ к картам именно проблемной production-версии.
- Объяснено соответствие позиций артефакта исходнику.
- Карта привязана к точному релизу.
- Продуманы доступ и проверка расшифровки.
Спецификация: webpack: варианты source maps
После правки компонента HMR сохраняет удобное состояние, но после полной перезагрузки появляется ошибка. Какие гарантии даёт горячее обновление?
Интервьюер может уточнить
- Почему после нескольких обновлений обработчик вызывается много раз?
- Нужно ли считать любой сброс состояния ошибкой HMR?
Разбор
Сначала воспроизведите проблему на чистом запуске, затем сравните с горячим обновлением. Старые обработчики и ресурсы должны освобождаться, если модуль создаёт их вне жизненного цикла компонента. Не каждая правка сохраняет состояние: это зависит от границ обновления и интеграции фреймворка.
Dev-сервер оптимизирует скорость разработки, а production — готовый артефакт. Различаются условия окружения, оптимизация и способ загрузки зависимостей. Проверка HMR не заменяет сборку и запуск её результата.
Уточняющие вопросы
Почему после нескольких обновлений обработчик вызывается много раз?
Новая версия модуля могла добавить подписку без удаления старой; нужна очистка при замене или правильный жизненный цикл.
Нужно ли считать любой сброс состояния ошибкой HMR?
Нет: некоторые изменения требуют полной перезагрузки или пересоздания компонента.
- Горячая замена отделена от холодного запуска.
- Проверена очистка ресурсов модуля.
- Production-артефакт проверяется самостоятельно.
Спецификация: Vite: HMR API
В CI добавили кеш сборки по хешу исходников. После изменения переменной окружения он возвращает старый результат. Что должно входить в ключ кеша?
Интервьюер может уточнить
- Зачем учитывать версию компилятора?
- Нужно ли включать все переменные CI без разбора?
Разбор
Разделяйте кеш скачивания пакетов, промежуточных преобразований и итоговых задач: у них разные входы и цена ошибки. Для monorepo учитывают также зависимости задачи и результаты генерации.
Нельзя кешировать недетерминированный результат как чистую функцию без описания внешних входов. Пропуск кеша должен приводить к обычной корректной сборке. Сравните холодный и повторный запуск, затем измените каждый важный вход и убедитесь, что результат пересчитывается.
Уточняющие вопросы
Зачем учитывать версию компилятора?
Одинаковый исходник может преобразовываться по-разному разными версиями инструментов.
Нужно ли включать все переменные CI без разбора?
Нет: случайные ID запуска уничтожат пользу кеша. Выделяют именно влияющие на результат значения и отдельно защищают секреты.
- Перечислены значимые входы задачи.
- Различены виды кеша.
- Проверена инвалидация при изменении входов и корректность cache miss.
Спецификация: Turborepo: кеширование задач
После выкладки пользователи с давно открытой вкладкой получают 404 при открытии ленивого раздела. Новая главная страница работает. Как связаны релизы и хешированные файлы?
Интервьюер может уточнить
- Почему хеш в имени удобнее перезаписи
app.js? - Почему принудительная перезагрузка не всегда приемлема?
Разбор
Сначала публикуют ресурсы нового релиза, затем переключают HTML или указатель версии. Старые хешированные файлы сохраняют на срок, согласованный с временем жизни клиентов и кешей. Файл с прежним хешем нельзя заменить другим содержимым.
Новый HTML должен ссылаться на целостный набор ресурсов. Предусмотрите обработку ошибки устаревшего клиента и безопасное обновление с сохранением черновика. Одна лишь очистка CDN не обновляет уже работающий JavaScript в открытой вкладке.
Уточняющие вопросы
Почему хеш в имени удобнее перезаписи app.js?
Новое содержимое получает новый URL, что позволяет долго кешировать неизменяемый ресурс и различать версии.
Почему принудительная перезагрузка не всегда приемлема?
Пользователь может потерять несохранённые данные; сначала нужна стратегия их сохранения или предупреждения.
- Объяснены ссылки старой вкладки на прежние chunks.
- Публикация сохраняет согласованный набор файлов.
- Предусмотрены хранение старых ресурсов и восстановление клиента.
Спецификация: Vite: ошибки загрузки при выкладке
Production-очистка CSS удалила класс, который приложение собирает из строки по данным API. Почему анализатор его не увидел и как сохранить нужные стили без отключения оптимизации?
Интервьюер может уточнить
- Почему проблема видна только в production?
- Чем плох список сохранения «все классы»?
Разбор
Вместо произвольного построения имени "badge-" + status задайте карту разрешённых статусов на полные имена классов. При использовании анализатора проверьте, какие файлы и шаблоны он сканирует. Часть классов может поступать из библиотек или серверных шаблонов.
Не разрешайте любой класс из внешних данных без контракта. Для действительно динамических величин, например ширины прогресса, может подойти CSS custom property. После extraction важно также проверить порядок каскада и загрузку стилей отложенных частей.
Уточняющие вопросы
Почему проблема видна только в production?
Удаление неиспользуемых правил может быть включено только в production pipeline.
Чем плох список сохранения «все классы»?
Он практически отменяет оптимизацию и маскирует неверно заданные входы анализатора.
- Объяснено ограничение статического поиска классов.
- Предложена явная карта или ограниченный safelist.
- Проверены источники классов и готовые состояния страницы.
Спецификация: Tailwind CSS: обнаружение классов
Пакет UI в monorepo импортирует библиотеку, которую сам не объявляет. В корневой установке всё работает, а при публикации ломается. Почему?
Интервьюер может уточнить
- Почему недостаточно проверить приложение из того же monorepo?
- Нужно ли превращать все зависимости в peer?
Разбор
Различайте runtime-зависимости, инструменты разработки и peerDependencies — ожидания к совместимой библиотеке окружения потребителя. Например, UI-библиотека может объявлять React как peer, чтобы согласовать общую среду, а локальную версию использовать для разработки.
Проверьте содержимое публикуемого пакета, его exports, собранные файлы и установку в отдельный пример. Workspace-ссылка на исходники не доказывает, что архив публикации содержит всё нужное. Дублирование экземпляров библиотек с общим контекстом тоже может менять поведение.
Уточняющие вопросы
Почему недостаточно проверить приложение из того же monorepo?
Оно может иметь зависимости и пути, которых не будет у внешнего потребителя.
Нужно ли превращать все зависимости в peer?
Нет: peer означает требование к окружению потребителя, а не универсальный способ уменьшения пакета.
- Пакет явно объявляет используемые зависимости.
- Различены dependencies, devDependencies и peerDependencies.
- Проверены публикационный артефакт и отдельный потребитель.
Спецификация: npm: поля package.json
Сборка выросла с минуты до десяти после добавления плагинов и генерации страниц. Как определить, поможет ли смена bundler?
Интервьюер может уточнить
- Почему нельзя сравнивать только тёплую локальную сборку с холодным CI?
- Как понять, что ускорение не получено отключением нужной проверки?
Разбор
Измерьте холодный и повторный запуск на одинаковом окружении. Найдите тяжёлые плагины, чрезмерно широкое сканирование, повторную обработку файлов и последовательные внешние запросы. Профиль и длительность фаз полезнее общего впечатления «сборщик медленный».
Оптимизируйте подтверждённую причину: сузьте входы, устраните повторную работу, корректно кешируйте или ограниченно распараллельте независимые задачи. Проверьте, что итоговый артефакт и проверки не стали хуже. Увеличение параллельности может упереться в память или диск.
Уточняющие вопросы
Почему нельзя сравнивать только тёплую локальную сборку с холодным CI?
Различаются кеши, ресурсы машины и установка зависимостей; такое сравнение смешивает несколько причин.
Как понять, что ускорение не получено отключением нужной проверки?
Зафиксировать состав pipeline и сравнить выполненные этапы и свойства артефакта до и после.
- Время разделено по фазам.
- Измерения учитывают кеш и окружение.
- Оптимизация адресует подтверждённый bottleneck и сохраняет проверки.
Спецификация: Vite: производительность инструментов
Вы поправили стили шапки и заодно файл с настройками для локальной отладки. Как закоммитить только стили? Что такое индекс (staging area) и каким должен быть хороший коммит?
git status
git add src/header.css
git diff --staged
git commit -m "Выровнять меню в шапке на мобильных"
Интервьюер может уточнить
- Чем
git commit -amопасен? - Как исправить сообщение последнего коммита, если он ещё не отправлен?
Разбор
У файла в Git три состояния: изменён в рабочей папке, подготовлен в индексе, сохранён в коммите. Индекс позволяет собрать коммит из части изменений.
git add путь— добавить файл;git add -p— выбрать отдельные фрагменты;git diffпоказывает то, что ещё не в индексе,git diff --staged— то, что попадёт в коммит;git restore --staged путьубирает файл из индекса, не трогая изменения.
Хороший коммит делает одно дело, проект после него собирается, а сообщение в повелительной форме объясняет, что и зачем изменено. Локальные настройки, которые никогда не должны попадать в репозиторий, вносят в .gitignore.
Уточняющие вопросы
Чем git commit -am опасен?
Он добавляет все изменённые отслеживаемые файлы, включая случайные правки. Новые файлы он при этом не добавляет.
Как исправить сообщение последнего коммита, если он ещё не отправлен?
git commit --amend. После отправки в общую ветку переписывать коммит нельзя без договорённости.
- Объяснена роль индекса.
- Добавлен только нужный файл или фрагмент.
- Изменения проверены перед коммитом.
- Названы признаки хорошего коммита.
Спецификация: Pro Git: запись изменений в репозиторий
Нужно сделать новую форму обратной связи, пока коллега правит главную. Как работать в отдельной ветке и как потом влить её в main? Что такое fast-forward?
git switch main
git pull
git switch -c feature/feedback-form
# ...коммиты...
git push -u origin feature/feedback-form
Интервьюер может уточнить
- Чем
git pullотличается отgit fetch? - Что делать с веткой после слияния?
Разбор
Ветка в Git — подвижный указатель на коммит, поэтому создаётся мгновенно. Работа в своей ветке не мешает другим и позволяет показать изменения на ревью до попадания в main.
Слияние:
- fast-forward — если
mainне ушла вперёд, Git просто передвигает её на последний коммит ветки, история остаётся линейной; - трёхстороннее слияние — если обе ветки получили коммиты, Git объединяет изменения относительно общего предка и создаёт коммит слияния с двумя родителями.
Чем короче живёт ветка, тем меньше конфликтов. Свежие изменения main периодически забирают в свою ветку.
Уточняющие вопросы
Чем git pull отличается от git fetch?
fetch только скачивает новые коммиты с сервера, pull после этого ещё сливает их в текущую ветку (или делает rebase, если так настроено).
Что делать с веткой после слияния?
Удалить локально и на сервере: git branch -d имя, git push origin --delete имя. Коммиты останутся в main.
- Ветка создана от актуального
main. - Описан путь слияния: pull request или
git merge. - Объяснён fast-forward и коммит слияния.
Спецификация: Pro Git: ветвление в Git
При слиянии Git сообщил о конфликте в header.css. Что это значит и как его разрешить?
<<<<<<< HEAD
.menu { gap: 16px; }
=======
.menu { gap: 24px; display: flex; }
>>>>>>> feature/menu
Интервьюер может уточнить
- Может ли слияние без конфликтов всё равно сломать код?
- Как уменьшить число конфликтов?
Разбор
Между <<<<<<< HEAD и ======= — версия текущей ветки, ниже до >>>>>>> — версия сливаемой. Порядок действий:
git statusпокажет файлы с конфликтами;- в каждом решить, каким должен быть результат: выбрать вариант или объединить, и удалить маркеры;
- собрать проект и проверить, что работает;
git add файл, затемgit commit(илиgit rebase --continueпри rebase).
Если запутались, git merge --abort вернёт состояние до слияния. Редакторы вроде VS Code показывают конфликт с кнопками выбора, но решение по смыслу принимает человек — при сомнении спрашивают автора другого изменения.
Уточняющие вопросы
Может ли слияние без конфликтов всё равно сломать код?
Да: изменения в разных строках могут быть несовместимы по смыслу — например, одна ветка переименовала функцию, а другая добавила её вызов. Поэтому после слияния запускают сборку и тесты.
Как уменьшить число конфликтов?
Небольшие короткоживущие ветки, частое обновление от main, единый форматтер кода в проекте.
- Объяснено, когда возникает конфликт.
- Названы маркеры и их смысл.
- Описаны шаги: правка, проверка,
git add, завершение. - Назван способ отменить слияние.
Спецификация: Pro Git: основы конфликтов слияния
В общую ветку попал коммит, который сломал сборку. А локально у вас есть ещё не отправленный неудачный коммит. Как отменить каждый из них? Чем отличаются git revert, git reset и git restore?
Интервьюер может уточнить
- Как временно убрать незаконченные правки, чтобы переключиться на срочную задачу?
- Что такое
git reflog?
Разбор
git restore файл— вернуть файл к версии из последнего коммита, незакоммиченные правки пропадут;git reset HEAD~1— передвинуть ветку на коммит назад.--softоставляет изменения подготовленными, по умолчанию (--mixed) — в рабочей папке,--hardстирает их;git revert хеш— создать коммит, отменяющий указанный. История сохраняется, коллегам не нужно ничего переделывать.
Правило: историю, которую уже получили другие, не переписывают. Для общей ветки — revert, для своих локальных коммитов — reset или commit --amend.
Даже после reset --hard коммиты ещё какое-то время можно найти через git reflog.
Уточняющие вопросы
Как временно убрать незаконченные правки, чтобы переключиться на срочную задачу?
git stash сохраняет их в отдельный список, git stash pop возвращает. Либо закоммитить в рабочую ветку как черновик.
Что такое git reflog?
Журнал перемещений HEAD в вашем локальном репозитории. По нему находят «потерянные» коммиты после reset или неудачного rebase.
- Для отправленного коммита выбран
revert. - Для локального —
reset, различены режимы. - Объяснено назначение
restore. - Сформулировано правило о чужой истории.
Спецификация: Pro Git: операции отмены
Вы открываете первый pull request в команде. Что в нём должно быть, чтобы его быстро проверили? Как реагировать на замечания ревьюера?
Интервьюер может уточнить
- Что такое draft pull request?
- Нужно ли сжимать коммиты перед слиянием?
Разбор
Что помогает ревью:
- размер — сотни строк, а не тысячи; большую задачу дробят на несколько PR;
- описание: цель, ключевые решения, как проверить вручную, что не вошло;
- скриншоты или видео для изменений интерфейса;
- без случайных файлов, отладочного кода и несвязанного форматирования;
- пройденные линтер, тесты и сборка в CI.
Замечания — о коде, а не о человеке. На каждое отвечают: исправлено, или вот почему сделано так. Если спор затянулся, лучше обсудить голосом. Мелкие стилевые вопросы должны решать форматтер и линтер, а не ревьюер.
Ревьюируя чужой код, отделяют обязательное от пожеланий и задают вопросы вместо приказов.
Уточняющие вопросы
Что такое draft pull request?
Черновик: показать работу заранее и получить ранний отзыв, не прося о полном ревью и не допуская слияния.
Нужно ли сжимать коммиты перед слиянием?
Зависит от правил команды. Часто используют squash merge: в main попадает один аккуратный коммит на PR.
- Названы размер и описание PR.
- Упомянуты самопроверка и CI.
- Описано конструктивное обсуждение замечаний.
Спецификация: Google: The CL author’s guide
Ваша ветка отстала от main на десять коммитов. Обновить её через merge или через rebase? Чем различаются результаты и когда rebase опасен?
Интервьюер может уточнить
- Почему после rebase
git pushотклоняется и чем--force-with-leaseлучше--force? - Чем squash merge отличается от rebase?
Разбор
Merge честно отражает, как шла работа, ничего не переписывает, но в истории появляются коммиты слияния, и её сложнее читать.
Rebase по очереди применяет ваши коммиты к новой базе. Конфликты могут появиться на каждом коммите. Плюсы — линейная история, удобная для git log и git bisect. Минус — коммиты получают новые хеши: если кто-то основывал работу на старых, у него появятся дубли и конфликты.
Практика:
- свою ветку обновляют через rebase и отправляют с
--force-with-lease; - общие ветки (
main, release) не переписывают; - интерактивный
rebase -iпозволяет перед ревью объединить и переименовать коммиты.
Выбор — в соглашении команды; важнее единообразие, чем сам вариант.
Уточняющие вопросы
Почему после rebase git push отклоняется и чем --force-with-lease лучше --force?
История на сервере больше не является предком вашей. --force-with-lease перезапишет ветку, только если на сервере никто не добавил коммитов с момента вашего последнего получения.
Чем squash merge отличается от rebase?
Squash собирает все изменения ветки в один новый коммит в main; отдельные коммиты ветки в истории не сохраняются.
- Объяснено, что rebase переписывает коммиты.
- Сравнены виды истории.
- Сформулировано, когда rebase опасен.
- Упомянут безопасный force push.
Спецификация: Pro Git: перебазирование
Вчера после релиза сломалась сортировка, а с прошлой рабочей версии в main пришло 200 коммитов. Как быстро найти коммит, который внёс ошибку?
git bisect start
git bisect bad HEAD
git bisect good v2.4.0
# проверить, затем git bisect good или git bisect bad
git bisect run npm test -- sort.test.js
git bisect reset
Интервьюер может уточнить
- Как найти, кто и когда изменил конкретную строку?
- Что делать, если на коммите проект не собирается?
Разбор
Каждая проверка вдвое сокращает диапазон, поэтому шагов — около log₂ числа коммитов. После нахождения Git показывает первый плохой коммит; git bisect reset возвращает исходную ветку.
git bisect run команда автоматизирует поиск: код возврата 0 — хорошо, от 1 до 127 (кроме 125) — плохо, 125 — пропустить коммит, который нельзя проверить.
Bisect работает хорошо, когда коммиты небольшие и каждый собирается. Огромные коммиты и сломанная сборка на промежуточных шагах сильно снижают пользу — ещё одна причина делать аккуратную историю.
Найденный коммит не всегда содержит саму ошибку: он мог лишь проявить старую. После нахождения пишут тест на этот случай.
Уточняющие вопросы
Как найти, кто и когда изменил конкретную строку?
git blame файл или поиск по истории изменений строки: git log -S "текст".
Что делать, если на коммите проект не собирается?
git bisect skip — Git выберет соседний коммит.
- Назван
git bisectи двоичный поиск. - Описаны отметки
goodиbad. - Упомянута автоматизация через
bisect run.
Спецификация: Git: git-bisect
Коллега закоммитил и отправил файл .env с ключом платёжного API. Он предлагает просто удалить файл следующим коммитом. Достаточно ли этого и что нужно сделать?
Интервьюер может уточнить
- Почему
.gitignoreне помог бы, если файл уже отслеживается? - Является ли секретом ключ в переменной
VITE_?
Разбор
Порядок действий:
- Отозвать ключ у поставщика сервиса и выдать новый: считаем, что старый уже скомпрометирован. Это единственный надёжный шаг — бот может найти ключ в публичном репозитории за минуты.
- Проверить журналы использования ключа на подозрительные операции.
- Удалить файл из репозитория (
git rm --cached .env) и добавить в.gitignore; в репозитории оставить пример.env.exampleбез значений. - Если нужно убрать ключ из истории —
git filter-repoи force push с уведомлением всей команды; кеши, форки и клоны всё равно могли его сохранить.
Профилактика: сканеры секретов (gitleaks, проверка на стороне хостинга репозиториев) в pre-commit и CI, хранение секретов в хранилище окружения CI и на сервере, а не в коде.
Уточняющие вопросы
Почему .gitignore не помог бы, если файл уже отслеживается?
.gitignore влияет только на неотслеживаемые файлы. Отслеживаемый нужно сначала убрать из индекса через git rm --cached.
Является ли секретом ключ в переменной VITE_?
Нет: такие переменные встраиваются в клиентский JavaScript. Секретные ключи используют только на сервере.
- Ключ отозван и перевыпущен в первую очередь.
- Объяснено, что ключ остаётся в истории.
- Названы
.gitignoreи очистка истории. - Предложена профилактика: сканер секретов.
Спецификация: GitHub: Removing sensitive data
Команда спорит о процессе: долгие ветки фич с выпуском раз в месяц или частые небольшие слияния в main. Чем отличаются GitFlow и trunk-based development и как выкладывать незаконченную функцию?
Интервьюер может уточнить
- Как в trunk-based исправить ошибку в уже выпущенной версии?
- Чем опасны забытые флаги?
Разбор
GitFlow: develop для текущей работы, ветки release/* для подготовки выпуска, hotfix/* для срочных исправлений. Подходит для продуктов с версиями, например библиотек или мобильных приложений. Минусы — сложность и большие слияния.
Trunk-based: ветки живут часы или дни, обязательны автоматические проверки и ревью, выкладка частая. Конфликтов меньше, обратная связь быстрее.
Feature flags позволяют влить код, но не показывать функцию пользователям: включить её для команды, части аудитории или выключить без отката релиза. Флаги нужно удалять после запуска, иначе код зарастает ветвлениями.
Выбор зависит от частоты выпусков, зрелости тестов и CI, а не от моды.
Уточняющие вопросы
Как в trunk-based исправить ошибку в уже выпущенной версии?
Исправить в main и выложить новую версию; если нужна поддержка старой линии, перенести исправление в ветку выпуска через cherry-pick.
Чем опасны забытые флаги?
Растёт число комбинаций, которые никто не тестирует, а мёртвый код усложняет изменения.
- Описаны GitFlow и trunk-based.
- Названы условия выбора.
- Упомянуты feature flags и их цена.
Спецификация: Trunk Based Development
Пользователь говорит «сайт тормозит»: главная открывается быстро, но фильтр каталога долго не реагирует. С чего начать измерение?
Интервьюер может уточнить
- Почему быстрый первый экран не доказывает быструю работу фильтра?
- Можно ли считать появление спиннера завершением задачи?
Разбор
Зафиксируйте устройство, объём каталога, состояние кеша и шаги воспроизведения. Измеряйте задержку от изменения фильтра до видимого результата и отдельно до первого визуального отклика. Запишите профиль этого действия.
Core Web Vitals описывают загрузку основного содержимого, отзывчивость и стабильность раскладки, но не заменяют метрику конкретной задачи. Ускорение должно улучшать сценарий без потери корректности и доступности, а не только менять число в отчёте.
Уточняющие вопросы
Почему быстрый первый экран не доказывает быструю работу фильтра?
После загрузки главный поток может быть занят обработкой данных и рендерингом; это другая фаза работы страницы.
Можно ли считать появление спиннера завершением задачи?
Нет: это обратная связь. Время до полезного результата измеряют отдельно.
- Определены действие и полезный результат.
- Загрузка отличена от отзывчивости.
- Зафиксированы данные и условия воспроизведения.
Спецификация: web.dev: Web Vitals
Lighthouse на ноутбуке показывает хороший результат, а реальные пользователи жалуются на задержки. Почему лабораторные и полевые измерения расходятся?
Интервьюер может уточнить
- Почему среднее время может скрыть проблему?
- Нужно ли отказаться от лабораторных тестов?
Разбор
Один локальный запуск может не включать позднее открытие меню, большой аккаунт или сторонние скрипты после согласия пользователя. Полевые распределения помогают увидеть медленные устройства и редкие тяжёлые сценарии.
Лаборатория удобна для воспроизведения причины и проверки гипотезы. После изменения проверьте результат и в реальных данных с учётом времени распространения релиза. Отсутствие полевых измерений у страницы не означает отсутствие проблемы.
Уточняющие вопросы
Почему среднее время может скрыть проблему?
Быстрое большинство маскирует медленный хвост; полезны процентили и разбиение по устройствам, маршрутам и версиям.
Нужно ли отказаться от лабораторных тестов?
Нет: они дают контролируемые условия и быстрый сигнал регрессии, дополняя полевые измерения.
- Различены контролируемые и реальные условия.
- Предложены сегменты и распределения вместо одного числа.
- Лаборатория используется для диагностики, поле — для проверки пользовательского эффекта.
Спецификация: web.dev: лабораторные и полевые данные
После изменения кода страница загрузилась на 100 мс быстрее в одном прогоне. Как проверить, что это результат оптимизации, а не случайность?
Интервьюер может уточнить
- Почему два прогона с разным состоянием кеша несопоставимы?
- Что делать, если разброс больше ожидаемого улучшения?
Разбор
Измеряйте до и после на одинаковом сценарии, избегая одновременных изменений нескольких факторов. Холодный запуск и повторное посещение проверяют отдельно. Расширения браузера, фоновые процессы и открытые инструменты могут влиять на результат.
Сохраняйте трассировки и численные результаты, чтобы связать изменение с конкретной фазой работы. Важно оценить заметность эффекта для пользователя и убедиться, что он не получен отключением функциональности.
Уточняющие вопросы
Почему два прогона с разным состоянием кеша несопоставимы?
Во втором часть ресурсов или вычислений уже готова, поэтому ускорение может не иметь отношения к правке.
Что делать, если разброс больше ожидаемого улучшения?
Уточнить условия и увеличить число повторов либо признать, что текущие измерения не позволяют уверенно подтвердить эффект.
- Условия сравнения зафиксированы.
- Есть повторные измерения и оценка разброса.
- Результат связан с фазой работы и пользовательским сценарием.
Спецификация: Chrome DevTools: запись производительности
JavaScript хорошо сжат и быстро скачивается, но на слабом телефоне страница долго не отвечает. Какие затраты остаются после передачи файла?
Интервьюер может уточнить
- Одинаково ли полезно уменьшить JS и картинку на одинаковое число байтов?
- Показывает ли размер gzip объём работы главного потока?
Разбор
Сравните Network с записью главного потока. Если загрузка завершена, а CPU занят инициализацией, уменьшение сетевых байтов само по себе не устранит задержку. Найдите дорогие функции, объём начальных данных и ненужные стартовые действия.
Используйте production-сборку и реалистичное замедление CPU. Отложенный код полезен, если он действительно не нужен сразу; если сразу же запускать весь отложенный код, стоимость выполнения никуда не исчезнет.
Уточняющие вопросы
Одинаково ли полезно уменьшить JS и картинку на одинаковое число байтов?
Нет: ресурсы имеют разную стоимость обработки. JS дополнительно исполняется и может инициировать ещё работу.
Показывает ли размер gzip объём работы главного потока?
Нет: он описывает размер передачи при таком сжатии, а не время выполнения.
- Названы затраты разбора, компиляции и выполнения.
- Сеть сопоставлена с профилем CPU.
- Размер передачи не принят за единственную метрику.
Спецификация: web.dev: затраты на JavaScript
Миниатюра 120×120 показывает исходное фото 6000×4000. Файл сжат неплохо, но галерея потребляет много памяти. Почему уменьшения размера через CSS недостаточно?
Интервьюер может уточнить
- Нужно ли всегда выбирать самое маленькое разрешение?
- Почему нельзя оценивать память только по килобайтам JPEG?
Разбор
Проверьте выбранный браузером ресурс, его пиксельные размеры, переданные байты и поведение галереи при прокрутке. srcset и sizes помогают выбрать подходящий файл; формат сжатия и разрешение решают разные части задачи.
Декодированные изображения могут занимать намного больше памяти, чем файл на диске, а точный расход зависит от браузера и формата. Не делайте все миниатюры полноразмерными только ради возможности будущего увеличения: большой вариант можно получать при открытии просмотра.
Уточняющие вопросы
Нужно ли всегда выбирать самое маленькое разрешение?
Нет: изображение должно оставаться достаточно чётким с учётом размера и плотности экрана. Нужен подходящий вариант.
Почему нельзя оценивать память только по килобайтам JPEG?
JPEG хранит сжатые данные; после декодирования и подготовки к отображению представление другое.
- Различены CSS-размер, размер файла и пиксельное разрешение.
- Предложены варианты ресурса под размер отображения.
- Учтены декодирование и память, а не только сеть.
Спецификация: web.dev: изображения подходящего размера
После подключения чата поддержки и аналитики ухудшилась отзывчивость. Как проверить вклад сторонних скриптов, если основной код приложения не менялся?
Интервьюер может уточнить
- Почему загрузка после
loadне гарантирует хороший INP? - Достаточно ли смотреть только размер собственного bundle?
Разбор
Изучите инициаторы запросов, объём зависимостей и выполняемые задачи. Проверьте, не подключён ли один сервис дважды и не запускается ли тяжёлая инициализация на каждом переходе. Временное блокирование домена в диагностике помогает проверить гипотезу.
Решение может включать загрузку по открытию виджета, более лёгкую интеграцию или согласованное удаление ненужного сервиса. Не отключайте важную функцию только ради балла без проверки пользовательских и продуктовых требований.
Уточняющие вопросы
Почему загрузка после load не гарантирует хороший INP?
Пользователь может взаимодействовать со страницей в момент позднего выполнения стороннего кода.
Достаточно ли смотреть только размер собственного bundle?
Нет: сторонний код может загружаться отдельно и создавать существенную сеть и CPU-работу.
- Вклад проверен сравнением и профилем.
- Учтены отдельные запросы и выполнение стороннего кода.
- Выбрана стратегия загрузки с сохранением нужного сценария.
Спецификация: web.dev: сторонний JavaScript
Обработчик клика занимает 10 мс, но взаимодействие отображается только через полсекунды. Почему короткий обработчик не гарантирует хороший INP?
Интервьюер может уточнить
- Почему бы просто не перенести весь обработчик в промис?
- Включает ли INP полное время сетевой операции до получения результата?
Разбор
На трассе найдите само взаимодействие и разложите задержку на input delay, processing duration и presentation delay. Уменьшение обработчика поможет только если он является существенной частью задержки. Фоновые задачи, синхронная работа других слушателей и большой DOM тоже влияют.
INP оценивает отзывчивость взаимодействий на протяжении посещения, а не только момент первой загрузки. Сохраните быстрый понятный отклик, затем организуйте оставшуюся работу без длинной блокировки. Перенос всего в микрозадачу не гарантирует уступку отрисовке.
Уточняющие вопросы
Почему бы просто не перенести весь обработчик в промис?
Микрозадачи выполняются до возможности отрисовки и могут продолжать удерживать поток.
Включает ли INP полное время сетевой операции до получения результата?
Не обязательно: он связан с задержкой до следующей отрисовки взаимодействия. Полное завершение задачи измеряют отдельно.
- Названы три части задержки взаимодействия.
- Причина локализована по трассе, а не только длительности обработчика.
- INP отличён от полного времени бизнес-операции.
Спецификация: web.dev: оптимизация INP
Главная картинка уже маленькая и быстро скачивается, но LCP остаётся высоким. Какие составляющие LCP проверить до дальнейшего сжатия?
Интервьюер может уточнить
- Может ли после оптимизации LCP стать другой элемент?
- Почему скрытая до завершения анимации картинка может появиться поздно?
Разбор
Сначала убедитесь, какой элемент действительно стал LCP в измерении. Затем сопоставьте получение HTML, обнаружение ресурса, начало и окончание запроса с моментом показа. Причиной может быть позднее создание элемента JavaScript, блокирующие стили или ожидание снятия состояния загрузки.
Оптимизируйте самую значимую фазу. Не приписывайте автоматически медленному серверу задержку, возникшую после получения данных. Для текстового LCP набор ресурсов и причины отличаются, поэтому сначала нужен реальный кандидат.
Уточняющие вопросы
Может ли после оптимизации LCP стать другой элемент?
Да: кандидат зависит от фактического содержимого и времени его отображения; его нужно проверять заново.
Почему скрытая до завершения анимации картинка может появиться поздно?
Загрузка файла и готовность элемента к видимому отображению — разные этапы. Нужно смотреть условия его показа.
- Определён фактический LCP-элемент.
- Различены сеть, обнаружение и задержка отображения.
- Оптимизация выбрана по доминирующей фазе.
Спецификация: web.dev: составляющие LCP
Цикл меняет ширину каждой карточки и сразу читает её offsetHeight. При сотнях карточек интерфейс зависает. Почему и как перестроить работу?
Интервьюер может уточнить
- Всегда ли чтение
offsetHeightдорогое? - Что выбрать, если размер надо получать при его изменении?
Разбор
Запишите профиль и найдите forced layout. Если нужны старые размеры, сначала соберите все измерения, затем примените изменения. Если нужны новые размеры после общей перестройки, сначала выполните записи, затем считайте результаты одной фазой.
Не меняйте порядок вслепую, если вычисление действительно зависит от предыдущего шага: нужно сохранить семантику. Иногда задачу решает CSS-раскладка без ручных измерений. Само помещение прежнего цикла в requestAnimationFrame не устраняет чередование чтений и записей внутри него.
Уточняющие вопросы
Всегда ли чтение offsetHeight дорогое?
Нет: проблема возникает, когда актуальная геометрия ещё не вычислена после изменений. Стоимость подтверждают профилем.
Что выбрать, если размер надо получать при его изменении?
Рассмотреть ResizeObserver, учитывая, что запись размеров из callback тоже может создать цикл обновлений.
- Объяснён принудительный пересчёт после записи.
- Чтения и записи сгруппированы с учётом зависимости данных.
- Исправление проверено по профилю.
Спецификация: web.dev: layout thrashing
Таблица содержит 50 000 строк: данные уже в памяти, но DOM делает прокрутку медленной. Чем виртуализация отличается от пагинации и что она может сломать?
Интервьюер может уточнить
- Почему локальный черновик строки может исчезнуть при прокрутке?
- Зачем запас строк вокруг видимой области?
Разбор
Измерьте число узлов, стоимость рендера и раскладки. При виртуализации невидимые строки отсутствуют, хотя данные могут оставаться загруженными. Это влияет на поиск браузера, выделение текста, печать и работу вспомогательных технологий.
Для переменной высоты строк нужны измерения и корректировка положения. Стабильные ключи, сохранение редактируемого состояния вне временно удаляемой строки и стратегия фокуса обязательны. Виртуализация не ускоряет автоматически фильтрацию всех данных на каждом вводе.
Уточняющие вопросы
Почему локальный черновик строки может исчезнуть при прокрутке?
Строка может размонтироваться за пределами окна; важное состояние нужно сохранять по ID независимо от её DOM.
Зачем запас строк вокруг видимой области?
Он уменьшает риск пустых промежутков при быстрой прокрутке, но слишком большой запас снижает выигрыш.
- Описано ограниченное окно DOM.
- Учтены размеры, стабильные ID и состояние строк.
- Названы ограничения поиска, фокуса и доступности.
Спецификация: web.dev: виртуализация списков
Длинная статья тяжело отрисовывается. Можно ли применить content-visibility: auto вместо удаления невидимых разделов из DOM и что нужно проверить?
Интервьюер может уточнить
- Заменяет ли это свойство сетевую ленивую загрузку?
- Почему важна оценка размера?
Разбор
Применяйте свойство к достаточно самостоятельным разделам, а не механически к каждому элементу. В отличие от виртуализации, DOM и данные не исчезают, поэтому экономия памяти и возможности поиска отличаются.
Неточный резерв размера способен менять длину страницы и положение прокрутки при раскрытии реальной геометрии. Скрипты, постоянно требующие точных измерений пропущенных областей, могут уменьшить пользу. Проверьте переходы по якорям, поиск, фокус, доступность и печать.
Уточняющие вопросы
Заменяет ли это свойство сетевую ленивую загрузку?
Нет: пропуск части рендеринга не является общим запретом загрузки изображений или данных.
Почему важна оценка размера?
Без разумного резерва раскладка и прокрутка могут заметно меняться по мере обработки разделов.
- Пропуск рендеринга отличён от удаления DOM.
- Учтён резерв размера.
- Проверены навигация, измерения и пользовательские сценарии.
Спецификация: web.dev: content-visibility
Чтобы ускорить анимации, всем карточкам добавили will-change: transform. Памяти стало больше, а телефон начал тормозить. Почему подсказка может навредить?
Интервьюер может уточнить
- Гарантирует ли
will-changeотдельный GPU-слой? - Почему большой слой может быть дороже маленького?
Разбор
Сравните работу без подсказки и с ней: главные затраты могут находиться в JavaScript, paint или больших поверхностях, а не в создании слоя. transform часто позволяет избежать раскладки, но не обещает нулевой стоимости отображения.
Если подсказка действительно полезна, включайте её заранее для ограниченного числа активных элементов и убирайте, когда она не нужна. Проверьте память, плавность и визуальное поведение: некоторые значения также влияют на контексты наложения.
Уточняющие вопросы
Гарантирует ли will-change отдельный GPU-слой?
Нет: это подсказка, конкретное решение принимает браузер.
Почему большой слой может быть дороже маленького?
Для него нужны большие поверхности и больше памяти и обработки пикселей; число элементов само по себе не описывает стоимость.
- Подсказка не названа гарантией ускорения.
- Учтены память и стоимость слоёв.
- Применение ограничено измеренным сценарием.
Спецификация: web.dev: рендеринг и кадры
Нужно узнать, сколько занимает открытие отчёта: запрос, преобразование данных и показ таблицы. Как расставить измерения, чтобы не смешать эти этапы?
Интервьюер может уточнить
- Почему не использовать только
Date.now()? - Что измерять при ошибке запроса?
Разбор
performance.mark создаёт отметку, performance.measure измеряет интервал между отметками. Используйте уникальную связь с операцией, чтобы параллельные открытия не смешали начало одного запроса и конец другого. Отдельно фиксируйте отказ и отмену.
Метка после функции рендера означает завершение её JavaScript, но не обязательно показ пикселей: сопоставьте её с трассировкой браузера. Удаляйте ненужные записи при длительной работе. В телеметрию передают метрики и безопасные технические признаки, а не содержимое отчёта.
Уточняющие вопросы
Почему не использовать только Date.now()?
Для измерения длительностей предпочтительнее монотонные часы Performance API, не привязанные к изменениям системного календарного времени.
Что измерять при ошибке запроса?
Длительность неуспешной операции и её исход отдельно от успешных результатов, иначе статистика будет смещена.
- Этапы измеряются раздельно через отметки.
- Параллельные операции и исходы не смешиваются.
- Завершение JS не выдано за показ кадра.
Спецификация: W3C: User Timing
После каждой новой функции страница становится немного тяжелее. Как поставить ограничения в CI, чтобы ловить регрессии, не гоняясь за одним баллом Lighthouse?
Интервьюер может уточнить
- Почему один лимит общего размера сайта неудобен?
- Что делать при шумном измерении времени в CI?
Разбор
Например, отдельно отслеживают начальный JS, критические изображения и задержку открытия каталога на выбранном устройстве. Порог выбирают по целям продукта и базовым измерениям, а не по универсальному числу для любого сайта.
Сравнивайте с базовой веткой или утверждённым бюджетом, сохраняйте отчёты и учитывайте разброс лабораторных измерений. Осознанное увеличение возможно, если польза оправдана и последствия понятны. После релиза проверяйте реальные данные: лабораторный бюджет не описывает всё поведение аудитории.
Уточняющие вопросы
Почему один лимит общего размера сайта неудобен?
Не все ресурсы нужны в первом сценарии; он может скрывать рост начального пакета среди редко загружаемых файлов.
Что делать при шумном измерении времени в CI?
Стабилизировать окружение, повторять прогоны и использовать обоснованный допуск, а не бесконечно повышать лимит.
- Бюджеты связаны с конкретными сценариями.
- Раздельно учитываются ресурсы и время.
- Есть базовое сравнение, допуск и разбор превышения.
Спецификация: web.dev: бюджеты производительности
Поиск по 100 000 записям на каждый символ нормализует строки, сортирует весь массив и создаёт много временных объектов. Что проверить до переноса вычисления в Worker?
Интервьюер может уточнить
- Почему нельзя кешировать результат без учёта версии данных?
- Всегда ли индекс полезнее простого перебора?
Разбор
Запишите профиль вычислений и выделений памяти. Постоянное преобразование одинаковых строк и сортировка всего набора могут стоить больше, чем сам поиск. Для устойчивого набора данных можно подготовить индекс или нормализованное представление с понятным правилом обновления.
Проверьте корректность языка, порядка результатов и инвалидирования кеша. Сравните время и память до и после. Если работа всё ещё тяжёлая, вынесите её в Worker с ограниченным обменом; устаревший результат не должен заменять актуальный запрос.
Уточняющие вопросы
Почему нельзя кешировать результат без учёта версии данных?
После обновления каталога кеш может вернуть устаревшие элементы или порядок.
Всегда ли индекс полезнее простого перебора?
Нет: индекс занимает память, строится и обновляется. Выигрыш зависит от числа запросов, размера и частоты изменения данных.
- Измерены вычисления и повторная работа.
- Предложена оптимизация данных или алгоритма с оценкой памяти.
- Учтены актуальность кеша и стоимость Worker.
Спецификация: Chrome DevTools: анализ работы JavaScript
Разбор описывает один из допустимых ответов; сверяйтесь с пунктами чек-листа, а не с точной формулировкой. Ссылки на уроки сайта и спецификации — в конце каждого разбора.