Перейти к основному содержимому

Реальное собеседование SENIOR FRONTEND на 350к в банк

· 76 мин. чтения

Сегодня мы разберем собеседование на позицию Senior Frontend-разработчика в крупный банк, где кандидат с 7-летним опытом работы демонстрирует уверенное владение вёрсткой, сборкой проектов и библиотекой React. Несмотря на некоторые затруднения в теоретических нюансах JavaScript и экосистеме React, кандидат показывает практический подход к решению задач, подкрепленный реальным опытом создания UI-библиотек и оптимизации производительности приложений.

Вопрос 1. Расскажите о себе и своём опыте работы в разработке.

Таймкод: 00:00:17

Ответ собеседника: Правильный. Фронтенд-разработкой занимается около 7 лет, из них около 6 лет — с JavaScript. Начинал с вёрстки HTML/CSS, затем jQuery, потом перешёл на React и его экосистему (Redux, React Router и т.д.) — с React работает около 5 лет. Имеет небольшой опыт бэкенда (JS, PHP), что помогает при взаимодействии с бэкенд-разработчиками. Работал в нескольких компаниях над различными проектами, в том числе разрабатывал библиотеку компонентов для дочерней компании Сбербанка (селекты, автокомплиты и т.д.), взаимодействовал с бизнесом, выявлял требования. Библиотека позволила сократить сроки MVP-проектов с 4 до 2 месяцев. Изначально библиотека писалась на основе Kendo UI с React, затем полностью переписана с нуля. Сначала использовался Flow, потом перешли на TypeScript. Тестирование включало юнит-тесты на Jest, тест-либ и E2E-тесты на Cypress. Тестовые кейсы описывали тестировщики, а код тестов писали разработчики, позже выделили отдельных тестировщиков.

Правильный ответ:

Ответ собеседника — это пример того, как следует рассказывать о себе на интервью. Он структурирован, конкретен и демонстрирует глубину опыта. Давайте разберём, почему он хорош и что можно было бы добавить для ещё более сильного впечатления.

Ключевые элементы сильного ответа «Расскажите о себе»:

  1. Хронология и эволюция: Кандидат не просто перечисляет технологии, а показывает свой профессиональный рост: от вёрстки до сложных фреймворков. Это демонстрирует способность учиться и адаптироваться.
  2. Конкретные достижения с измеримым результатом: Фраза «библиотека позволила сократить сроки MVP-проектов с 4 до 2 месяцев» — это золотой стандарт. Она показывает, что работа кандидата имела прямое влияние на бизнес-метрики. Всегда старайтесь привязывать свои достижения к цифрам (ускорение, снижение затрат, повышение стабильности).
  3. Глубина в одной области: Подробный рассказ о разработке библиотеки компонентов показывает экспертизу в создании переиспользуемого кода, работе с дизайн-системами и взаимодействии с бизнесом. Это уровень Senior/Lead.
  4. Технический стек и его обоснование: Упоминание перехода от Flow к TypeScript показывает понимание важности типизации для поддержки крупных проектов. Это не просто знание инструмента, а понимание его ценности.
  5. Понимание процесса разработки: Рассказ о тестировании (юнит, E2E) и взаимодействии с тестировщиками показывает, что кандидат видит разработку как целостный процесс, а не только написание кода.

Что можно было бы добавить для усиления ответа (если бы спросили подробнее):

  • Причины перехода технологий: Почему перешли от jQuery к React? Было ли это связано с ростом сложности проекта, требованиями к производительности или командой?
  • Роль в команде: Был ли он единственным разработчиком библиотеки, или руководил небольшой командой? Это покажет лидерские качества.
  • Сложности и их преодоление: Были ли сложности при переписывании библиотеки с нуля? Как решались проблемы совместимости или обучения команды новым подходам?
  • Взаимодействие с бэкендом: Упомянут небольшой опыт бэкенда. Можно было бы кратко привести пример, как это помогло в конкретной ситуации (например, в проектировании API или отладке интеграционных проблем).

Пример идеального расширенного ответа (если бы кандидат захотел добавить деталей):

«...Кроме того, мой опыт бэкенда на Node.js помог мне в одном проекте предложить более эффективную структуру ответа API, что сократило количество запросов с фронтенда на 30% и улучшило время загрузки страницы. При разработке библиотеки компонентов мы столкнулись с проблемой производительности при рендеринге больших списков в автокомплите. Я предложил и реализовал виртуализацию списка, что решило проблему и стало стандартом для всех подобных компонентов в компании...»

Таким образом, ответ кандидата уже очень сильный. Он демонстрирует не просто набор навыков, а инженерное мышление, ориентацию на результат и понимание бизнес-контекста. Это именно то, что ищут в опытных разработчиках.

Вопрос 2. Чем занимались на последнем месте работы?

Таймкод: 00:04:55

Ответ собеседника: Правильный. Работал над зарплатным проектом. Изначально выполнял роль фронтенд-архитектора: создал новый сервис, перепроектировал взаимодействие с бэкендом, обеспечил интеграцию микросервиса в общую архитектуру и связь с другими приложениями. Затем занимался разработкой фич и консультировал мидл-разработчиков.

Правильный ответ:

Ответ собеседца демонстрирует высокий уровень ответственности и архитектурное мышление. Давайте разберём, почему этот ответ сильный, и как можно было бы его дополнить для ещё более полной картины.

Сильные стороны ответа:

  1. Чёткое разделение ролей по времени: Кандидат показывает свой рост — от архитектора до разработчика и ментора. Это демонстрирует гибкость и способность решать задачи разного уровня.
  2. Архитектурные задачи: Создание нового сервиса, перепроектирование взаимодействия с бэкендом, интеграция в микросервисную архитектуру — это задачи уровня Senior+/Lead. Показывает понимание системного дизайна.
  3. Менторство: Консультирование мидл-разработчиков — важный навык, который указывает на лидерские качества и способность делиться знаниями.
  4. Предметная область: Зарплатный проект — это критически важная бизнес-система, где важны надёжность, безопасность и точность расчётов.

Что можно было бы уточнить или дополнить (если бы интервьюер спросил подробнее):

  • Конкретика архитектурных решений:

    • Какие паттерны проектирования использовали для взаимодействия с бэкендом? (REST, GraphQL, gRPC?)
    • Как решали проблему согласованности данных между микросервисами?
    • Использовали ли вы Event-Driven Architecture для связи между приложениями?
  • Технические детали:

    • Какой стек технологий выбрали для нового сервиса и почему?
    • Как обеспечивали типобезопасность при взаимодействии с бэкендом? (TypeScript интерфейсы, OpenAPI генерация типов?)
    • Как организовано состояние приложения? (Redux, Zustand, React Query?)
  • Масштаб и результаты:

    • Сколько пользователей/транзакций обслуживает сервис?
    • Какие метрики улучшились после перепроектирования? (время отклика, количество ошибок, скорость разработки новых фич?)
    • Сколько мидлов консультировал и по каким темам?

Пример расширенного ответа:

«...При создании нового сервиса мы выбрали React + TypeScript на фронтенде и перешли от монолитного REST API к микросервисам с gRPC для внутреннего взаимодействия. Это позволило снизить время отложенной загрузки ключевых виджетов на 40%. Для синхронизации данных между сервисами использовали паттерн Event Sourcing с Kafka. Также я внедрил кодогенерацию TypeScript-типов из OpenAPI-спецификации бэкенда, что сократило количество ошибок интеграции на 60% и ускорило онбординг новых разработчиков...»

Важный момент для интервью:

Если вы упоминаете архитектурные задачи, будьте готовы ответить на уточняющие вопросы о принятых решениях и их альтернативах. Интервьюер может спросить: «Почему выбрали именно этот подход?», «Какие компромиссы были?», «Что бы изменили сейчас?». Это проверяет глубину понимания и способность к рефлексии.

Вопрос 3. Почему ищете другую работу и что хотите найти в новой компании?

Таймкод: 00:06:10

Ответ собеседника: Правильный. Рассматривает различные варианты, общается с интересными компаниями. Знает компанию как один из крупнейших банков. Работал с людьми, которые ранее работали в этой компании, и получил положительные отзывы о технической силе команды. Интересен проект, связанный с единой фронтенд-платформой. Готов заниматься разработкой частей приложения с нуля, а также интересуется архитектурными задачами — настройкой окружения, пайплайнов, архитектуры и последующей разработкой фич.

Правильный ответ:

Ответ собеседника выстроен грамотно: он не критикует текущего работодателя, демонстрирует осведомлённость о компании-работодателе и чётко формулирует свои профессиональные интересы. Разберём структуру и дадим рекомендации по усилению.

Сильные стороны ответа:

  1. Позиционирование как кандидата на рынке: «Рассматриваю варианты» — это нормальная позиция, которая не создаёт ощущение отчаяния. Вы выбираете работодателя, а не бежите от проблем.
  2. Осведомлённость о компании: Упоминание о знакомых, работавших в компании, и их положительных отзывах — это мощный сигнал. Показывает, что вы не бросаете резюме наугад, а целенаправленно выбираете место.
  3. Конкретный интерес к проекту: «Единая фронтенд-платформа» — это не абстрактное «хочу интересные задачи», а понимание конкретного направления. Демонстрирует, что вы изучили вакансию и подготовились.
  4. Гибкость в задачах: Готовность и к разработке с нуля, и к архитектурным задачам — показывает зрелость и понимание, что в реальных проектах нужна разнопловая работа.

Что можно было бы добавить для усиления:

  • Мотивация роста: Можно честно сказать, чего не хватает на текущем месте. Например: «На текущем проекте я уже реализовал архитектуру и выстроил процессы. Сейчас хочу применить этот опыт на более масштабном проекте с большим количеством команд и пользователей».
  • Ценности компании: Если знаете что-то конкретное о культуре компании (технические блоги, open-source процессы, конференции), упомяните это: «Мне импонирует, что компания делится экспертизой через технический блог и участвует в разработке open-source инструментов».
  • Долгосрочная перспектива: Покажите, что вы думаете о развитии: «Вижу себя как технического лида фронтенд-направления через 2-3 года и хочу расти в компании, где есть такая возможность».

Чего следует избегать:

  • Критики текущего работодателя, коллег или менеджмента.
  • Фокуса только на зарплате: «Хочу больше денег» — допустимо, но не должно быть единственной мотивацией.
  • Размытых формулировок: «Хочу что-то новое» без объяснения, что именно.

Пример усиленного ответа:

«...Кроме того, на текущем месте я прошёл путь от проектирования архитектуры до её стабилизации. Сейчас проект перешёл в фазу поддержки, и я ищу возможность применить накопленный опыт на проекте с большим масштабом и техническими вызовами. Единая фронтенд-платформа — это именно такой проект, где архитектурные решения влияют на множество команд и миллионы пользователей. Также для меня важно работать в сильной технической культуре, где можно учиться у коллег и делиться своим опытом...»

Важный совет:

Ответ на этот вопрос — это не только о том, что вы хотите получить, но и о том, что вы можете дать. Всегда связывайте свои ожидания с ценностью, которую вы принесёте компании.

Вопрос 4. Как была организована разработка на последнем проекте — от получения задачи до деплоя?

Таймкод: 00:09:50

Ответ собеседника: Правильный. Использовались двухнедельные спринты с артефактами из Jira. Процесс начинался с груминга, где команда знакомилась с задачами и обсуждала их. Через день проводилось планирование с выставлением стори-поинтов и дообсуждением вопросов. После разработки фичи создавался pull request, который ревьюили разработчики из смежных команд. PR проходил пайплайн: проверка TypeScript, тесты на Jest. После принятия PR всё собиралось в Team City, развёртывалось на стенд в OpenShift. Затем задача переводилась в Jira в статус для тестировщиков.

Правильный ответ:

Ответ собеседника даёт хорошую общую картину процесса. Давайте разберём его детальнее и дополним тем, что интервьюер мог бы ожидать услышать от кандидата уровня Senior/Lead.

Сильные стороны ответа:

  1. Структурированность: Кандидат последовательно описывает весь цикл — от планирования до деплоя. Это показывает понимание полного цикла разработки.
  2. Конкретные инструменты: Упоминание Jira, Team City, OpenShift демонстрирует опыт работы с enterprise-инструментами CI/CD.
  3. Кросс-командное ревью: Ревью от смежных команд — это зрелая практика, которая улучшает качество кода и способствует обмену знаниями.
  4. Автоматизация проверок: TypeScript и Jest в пайплайне — стандарт для современного фронтенда.

Что можно было бы дополнить для более полной картины:

  • Размер и состав команды: Сколько человек в команде? Есть ли выделенный QA, дизайнер, аналитик?
  • Code Review практики:
    • Сколько апрувов требовалось для мержа?
    • Были ли чек-листы для ревью?
    • Как обрабатывались конфликты мнений?
  • Стратегия ветвления: Git Flow, Trunk-Based Development, Feature Flags?
  • Тестирование:
    • Были ли E2E тесты? Какой фреймворк (Cypress, Playwright)?
    • Какой был code coverage?
    • Были ли визуальные регрессионные тесты?
  • Деплой:
    • Какой стратегии деплоя придерживались (blue-green, canary)?
    • Были ли автоматические откаты при проблемах?
    • Как мониторили здоровье приложения после деплоя?
  • Метрики и улучшения:
    • Как измеряли эффективность процесса (lead time, cycle time)?
    • Были ли ретроспективы и как внедряли улучшения?

Пример расширенного ответа:

«...Наша команда состояла из 6 фронтенд-разработчиков, 2 бэкендеров, QA-инженера и продуктового аналитика. Мы использовали Trunk-Based Development с feature flags для долгоживущих фич. Для каждого PR требовалось минимум 2 апрува, один из которых — от разработчика смежной команды, чтобы обеспечить кросс-функциональное понимание кодовой базы.

Пайплайн включал несколько этапов: линтинг (ESLint, Prettier), проверка типов TypeScript, юнит-тесты с Jest (мы поддерживали покрытие не ниже 80%), сборку и запуск интеграционных тестов. После мержа в main автоматически запускался деплой на staging-стенд в OpenShift, где наши QA-инженеры проводили ручное тестирование. Для продакшена использовали canary-деплой — сначала 5% трафика, мониторинг ошибок через Sentry, и если метрики в норме, постепенное увеличение до 100%.

Мы отслеживали ключевые метрики: lead time (от создания задачи до деплоя) в среднем составлял 3 дня, change failure rate был менее 2%. На ретроспективах обсуждали узкие места и внедряли улучшения — например, автоматизировали генерацию changelog из commit messages...»

Важный момент:

Если вы описываете процессы, будьте готовы ответить на вопрос «Что бы вы улучшили?». Это показывает критическое мышление и стремление к совершенствованию. Например: «Я бы добавил автоматические визуальные регрессионные тесты для компонентов дизайн-системы, чтобы ловить непреднамеренные изменения внешнего вида».

Вопрос 5. Был ли настроен CI/CD на проекте?

Таймкод: 00:11:34

Ответ собеседника: Правильный. Да, CI/CD был реализован через Team City. Код хранился в Bitbucket, где настраивались хуки для сборки. Пайплайн включал проверка TypeScript, запуск тестов на Jest. После успешного мёржа запускалась сборка в Team City, и фича выкатывалась на стенд через OpenShift.

Правильный ответ:

Ответ корректный, но довлакий. Для кандидата уровня Senior/Lead интервьюер ожидает более глубокого понимания CI/CD процессов. Давайте разберём, что стоит добавить.

Что уже хорошо покрыто:

  • Конкретные инструменты: Team City, Bitbucket, OpenShift
  • Базовые этапы пайплайна: проверка типов, тесты, деплой
  • Понимание триггеров: хуки в Bitbucket при мёрже

Что стоит раскрыть подробнее:

Этапы CI пайплайна (Continuous Integration):

  1. Install — установка зависимостей с кэшированием
  2. Lint — статический анализ кода (ESLint, Stylelint)
  3. Type Check — проверка типов TypeScript
  4. Unit Tests — юнит-тесты с Jest, отчёт о покрытии
  5. Build — сборка приложения, проверка размера бандла
  6. Integration Tests — интеграционные тесты

Этапы CD пайплайна (Continuous Delivery/Deployment):

  1. Deploy to Staging — автоматический деплой на staging-окружение
  2. Smoke Tests — базовые проверки работоспособности
  3. E2E Tests — сквозные тесты (Cypress, Playwright)
  4. Deploy to Production — деплой в продакшен (ручной или автоматический)
  5. Post-deploy Verification — проверка метрик после деплоя

Пример расширенного ответа:

«...CI/CD пайплайн был реализован через Team City с хранением кода в Bitbucket. Процесс был разделён на несколько этапов:

CI-часть запускалась автоматически при создании или обновлении PR:

  • Установка зависимостей с кэшированием node_modules для ускорения
  • Линтинг через ESLint и Prettier с проверкой форматирования
  • Проверка типов TypeScript — это был обязательный гейт, без которого PR нельзя было мержить
  • Юнит-тесты на Jest с порогом покрытия в 80% — при падении ниже пайплайн падал
  • Сборка приложения с анализом размера бандла через bundle-analyzer — мы следили, чтобы новый код не увеличивал бандл более чем на 5%

CD-часть запускалась после мёржа в main:

  • Автоматический деплой на staging-стенд в OpenShift
  • Запуск smoke-тестов для проверки базовой работоспособности
  • Уведомление тестировщиков в Slack
  • Деплой на продакшен был полуавтоматическим: требовался ручной апрув от ответственного, после чего происходил canary-деплой — 5% трафика, мониторинг 15 минут, затем полный ролл-аут

Для отката использовали механизм roll-back в OpenShift — при обнаружении проблем через мониторинг Sentry можно было откатиться на предыдущую версию за пару минут...»

Дополнительные темы для обсуждения:

  • Environment Management: Как управлялись окружениями (dev, staging, prod)? Использовали ли вы feature flags?
  • Secrets Management: Как хранились секреты (ключи API, пароли)?
  • Infrastructure as Code: Использовали ли Terraform, Helm charts для описания инфраструктуры?
  • Monitoring: Какие метрики отслеживали после деплоя (error rate, latency, CPU)?
  • Notifications: Как команда узнавала о проблемах (Slack, PagerDuty)?

Важный момент:

Если вы настраивали CI/CD самостоятельно, подчеркните это. Если только пользовались готовым — расскажите, как предлагали улучшения. Интервьюер оценивает не только знание инструментов, но и инициативу в улучшении процессов.

Вопрос 6. Были ли настроены редакторы кода (ESLint, Prettier, Husky) на проекте?

Таймкод: 00:12:36

Ответ собеседника: Правильный. ESLint и Prettier были настроены. Husky (хуки) на этом проекте не использовались, но есть опыт работы с ними на других проектах. Также был опыт настройки конфигураций ESLint и Prettier.

Правильный ответ:

Ответ хороший — кандидат честно указал, что Husky не использовался на текущем проекте, но опыт с ним есть. Для Senior уровня стоит раскрыть тему глубже.

Что стоит добавить о ESLint:

Популярные конфигурации и плагины:

  • eslint-config-airbnb — строгий набор правил
  • eslint-config-prettier — отключение правил, конфликтующих с Prettier
  • eslint-plugin-import — проверка импортов
  • eslint-plugin-react и eslint-plugin-react-hooks — правила для React
  • eslint-plugin-jsx-a11y — доступность
  • @typescript-eslint — правила для TypeScript

Пример конфигурации ESLint:

// .eslintrc.js
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2021,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:import/typescript',
'prettier', // Должен быть последним
],
plugins: ['@typescript-eslint', 'react', 'jsx-a11y', 'import'],
rules: {
'react/react-in-jsx-scope': 'off', // Не нужно в React 17+
'@typescript-eslint/explicit-module-boundary-types': 'off',
'import/order': ['error', {
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
'newlines-between': 'always',
}],
},
settings: {
react: {
version: 'detect',
},
},
};

Что стоит добавить о Prettier:

Пример конфигурации:

// .prettierrc
{
"semi": true,
"trailingComma": "all",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"bracketSpacing": true,
"arrowParens": "always",
"endOfLine": "lf"
}

Что стоит добавить о Husky и lint-staged:

Husky позволяет запускать проверки перед коммитом и пушем, что предотвращает попадание «грязного» кода в репозиторий.

Настройка Husky + lint-staged:

# Установка
npm install --save-dev husky lint-staged

# Инициализация Husky
npx husky install

# Добавление хука pre-commit
npx husky add .husky/pre-commit "npx lint-staged"
// package.json
{
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md,yml}": [
"prettier --write"
]
}
}

Дополнительные инструменты качества кода:

  • commitlint — проверка формата коммитов (Conventional Commits)
  • editorconfig — единые настройки редактора для всех разработчиков
  • stylelint — линтинг CSS/SCSS

Пример расширенного ответа:

«...ESLint и Prettier были настроены с конфигурацией Airbnb. Мы использовали @typescript-eslint для строгой проверки типов и плагин import/order для автоматической сортировки импортов. Конфигурация была вынесена в отдельный npm-пакет, который подключался во всех проектах команды — это обеспечивало единообразие.

Husky на текущем проекте не использовался, но на предыдущем я настроил Husky с lint-stinted для автоматического форматирования и линтинга перед коммитом. Это значительно сократило количество «косметических» коммитов типа «fix formatting». Также настроил commitlint для проверки формата коммитов по Conventional Commits — это упростило автоматическую генерацию changelog...»

Важный момент:

Если вы настраивали эти инструменты самостоятельно, упомяните конкретные проблемы, которые решали. Например: «Настроил автофикс импортов при сохранении в VS Code, что сэкономило команде время на ручную сортировку».

Вопрос 7. Был ли опыт настройки сборки приложения? Какие сборщики использовались?

Таймкод: 00:13:14

Ответ собеседника: Правильный. Работал с Webpack версий 3 и 4, с пятой версией пока не работал. Знаком с Vite, слышал о нём как о новом сборщике. Отметил, что принципы работы сборщиков плюс-минус похожи.

Правильный ответ:

Ответ показывает опыт работы с Webpack, но для Senior уровня стоит раскрыть тему значительно глубже. Давайте разберём ключевые аспекты настройки сборки.

Webpack — ключевые концепции:

1. Entry и Output:

// webpack.config.js
module.exports = {
entry: {
main: './src/index.tsx',
admin: './src/admin/index.tsx',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
clean: true,
},
};

2. Loaders — трансформация файлов:

module.exports = {
module: {
rules: [
// TypeScript
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
// CSS с модулями
{
test: /\.module\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]--[hash:base64:5]',
},
},
},
],
},
// Изображения
{
test: /\.(png|jpg|gif|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024, // 8kb — inline как base64
},
},
},
],
},
};

3. Plugins — расширение функциональности:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
}),
// Анализ размера бандла (только для анализа)
process.env.ANALYZE && new BundleAnalyzerPlugin(),
].filter(Boolean),
};

4. Code Splitting и оптимизация:

module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
runtimeChunk: 'single',
},
};

Webpack 5 vs Webpack 4 — ключевые отличия:

  • Module Federation — микрофронтенды без фреймворков
  • Asset Modules — встроенная поддержка ассетов (замена file-loader, url-loader)
  • Persistent Caching — кэширование между сборками
  • Tree Shaking — улучшена поддержка ESM
  • Top Level Await — поддержка await на верхнем уровне

Vite — современная альтернатива:

Vite использует ESM нативно и esbuild для транспиляции, что даёт огромный прирост скорости в dev-режиме.

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
utils: ['lodash', 'date-fns'],
},
},
},
},
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
});

Сравнение сборщиков:

ХарактеристикаWebpack 5ViteesbuildParcel
Dev-серверМедленныйОчень быстрыйБыстрый
Production buildБыстрыйБыстрый (Rollup)Очень быстрыйСредний
КонфигурацияГибкаяПростаяМинимальнаяZero-config
ПлагиныОгромная экосистемаРастущаяМинимальныеСредняя
Tree ShakingДаДаДаДа
Module FederationДаНетНетНет

Пример расширенного ответа:

«...Работал с Webpack 3 и 4, настраивал конфигурации с нуля: loaders для TypeScript, CSS-модулей, изображений; plugins для извлечения CSS, анализа бандла, генерации HTML. Реализовал code splitting для ленивой загрузки роутов через React.lazy и dynamic imports, что сократило размер начального бандла на 40%.

С Webpack 5 пока не работал в production, но изучал документацию и экспериментировал с Module Federation для микрофронтенд-архитектуры. Vite использовал в pet-проектах — впечатлила скорость dev-сервера благодаря нативному ESM и esbuild для горячей перезагрузки. Для production-сборки Vite использует Rollup, что даёт отличную оптимизацию...»

Важный момент:

Если вы настраивали сборку, будьте готовы ответить на вопросы об оптимизации: «Как уменьшали размер бандла?», «Как ускоряли сборку?», «Как настраивали кэширование?». Это покажет практический опыт, а не только теоретические знания.

Вопрос 8. Как устроен конфигурационный файл Webpack? Что он включает в себя?

Таймкод: 00:13:41

Ответ собеседника: Правильный. Конфигурационный файл Webpack включает: лоадеры (loaders) для обработки различных типов файлов (шрифты, изображения, CSS, стили и т.д.), указание пути для билда (папка назначения), дополнительные настройки (прокси, обработка падений на страницы). Также указываются расширения файлов для сборки (TSX, JSX, JS) и подключаются плагины.

Правильный ответ:

Ответ покрывает основные элементы, но для Senior уровня стоит дать более структурированное и детальное описание.

Структура конфигурации Webpack:

1. Entry Points (Точки входа):

module.exports = {
entry: {
main: './src/index.tsx',
vendor: './src/vendor.ts',
},
// Или динамический entry
entry: () => {
return {
main: './src/index.tsx',
admin: './src/admin.tsx',
};
},
};

2. Output (Выход):

module.exports = {
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].chunk.js',
publicPath: '/',
clean: true, // Webpack 5: очистка папки перед сборкой
},
};

3. Mode (Режим):

module.exports = {
mode: 'production', // 'development' | 'production' | 'none'
// development: быстрая сборка, source maps, отладка
// production: минификация, tree shaking, оптимизация
};

4. Module Rules (Лоадеры):

module.exports = {
module: {
rules: [
// TypeScript
{
test: /\.tsx?$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true, // Для скорости — без проверки типов
},
},
],
exclude: /node_modules/,
},
// CSS Modules
{
test: /\.module\.scss$/,
use: [
process.env.NODE_ENV === 'production'
? MiniCssExtractPlugin.loader
: 'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
},
},
'sass-loader',
],
},
// Обычный CSS
{
test: /\.css$/,
exclude: /\.module\.css$/,
use: ['style-loader', 'css-loader'],
},
// Изображения
{
test: /\.(png|jpe?g|gif|svg|webp)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10 * 1024, // 10kb
},
},
generator: {
filename: 'images/[name].[hash:8][ext]',
},
},
// Шрифты
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
generator: {
filename: 'fonts/[name].[hash:8][ext]',
},
},
],
},
};

5. Resolve (Разрешение модулей):

module.exports = {
resolve: {
extensions: ['.tsx', '.ts', '.js', '.jsx'],
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
'@utils': path.resolve(__dirname, 'src/utils'),
},
modules: [path.resolve(__dirname, 'src'), 'node_modules'],
},
};

6. Plugins (Плагины):

const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const DefinePlugin = webpack.DefinePlugin;
const CopyPlugin = require('copy-webpack-plugin');

module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
favicon: './public/favicon.ico',
minify: process.env.NODE_ENV === 'production',
}),
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
}),
new DefinePlugin({
'process.env.API_URL': JSON.stringify(process.env.API_URL),
}),
new CopyPlugin({
patterns: [
{ from: 'public/locales', to: 'locales' },
],
}),
],
};

7. DevServer (Сервер разработки):

module.exports = {
devServer: {
port: 3000,
hot: true,
open: true,
historyApiFallback: true, // Для SPA
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: { '^/api': '' },
},
},
client: {
overlay: {
errors: true,
warnings: false,
},
},
},
};

8. Optimization (Оптимизация):

module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
},
},
}),
new CssMinimizerPlugin(),
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
const packageName = module.context.match(
/[\\/]node_modules[\\/](.*?)([\\/]|$)/
)[1];
return `vendor.${packageName.replace('@', '')}`;
},
},
},
},
runtimeChunk: 'single',
},
};

9. Source Maps:

module.exports = {
devtool: process.env.NODE_ENV === 'production'
? 'source-map'
: 'eval-cheap-module-source-map',
};

10. Devtool и Performance:

module.exports = {
performance: {
hints: 'warning',
maxEntrypointSize: 512000, // 500kb
maxAssetSize: 512000,
},
stats: {
modules: false,
children: false,
},
};

Полный пример конфигурации:

// webpack.config.js
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

const isProduction = process.env.NODE_ENV === 'production';

module.exports = {
mode: isProduction ? 'production' : 'development',
entry: './src/index.tsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: isProduction ? '[name].[contenthash].js' : '[name].js',
publicPath: '/',
clean: true,
},
devtool: isProduction ? 'source-map' : 'eval-cheap-module-source-map',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.css$/,
use: [
isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
'css-loader',
],
},
{
test: /\.(png|jpe?g|gif|svg)$/i,
type: 'asset',
parser: { dataUrlCondition: { maxSize: 10 * 1024 } },
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
plugins: [
new HtmlWebpackPlugin({ template: './public/index.html' }),
isProduction && new MiniCssExtractPlugin(),
new webpack.DefinePlugin({
'process.env.API_URL': JSON.stringify(process.env.API_URL),
}),
process.env.ANALYZE && new BundleAnalyzerPlugin(),
].filter(Boolean),
optimization: {
splitChunks: { chunks: 'all' },
},
devServer: {
port: 3000,
hot: true,
historyApiFallback: true,
},
};

Пример расширенного ответа:

«...Конфигурация Webpack включает несколько ключевых секций: entry для точек входа, output для указания папки сборки и именования файлов, module.rules для лоадеров (TypeScript, CSS, ассеты), resolve для алиасов и расширений, plugins для расширения функциональности (HtmlWebpackPlugin, MiniCssExtractPlugin, DefinePlugin), optimization для code splitting и минификации, devServer для локальной разработки с HMR и проксированием API. Также настраиваю source maps в зависимости от окружения и performance hints для контроля размера бандла...»

Вопрос 9. Чем плагины Webpack отличаются от лоадеров?

Таймкод: 00:14:59

Ответ собеседника: Правильный. Лоадеры пропускают через себя определённые типы файлов (CSS, изображения и т.д.) и обрабатывают/преобразуют их. Плагины же расширяют функциональность самого Webpack и позволяют выполнять пост-обработку бандла после загрузки всех модулей.

Правильный ответ:

Ответ корректный и хорошо передаёт суть различий. Давайте дополним его деталями для более полного понимания.

Лоадеры (Loaders):

Лоадеры — это трансформаторы, которые применяются к исходному коду файлов перед тем, как они попадут в бандл. Они работают на уровне отдельных файлов.

Ключевые характеристики лоадеров:

  • Применяются к файлам до добавления в граф зависимостей
  • Выполняются последовательно, справа налево (снизу вверх)
  • Каждый лоадер отвечает за один тип трансформации
  • Конфигурируются в module.rules

Пример цепочки лоадеров:

{
test: /\.scss$/,
use: [
'style-loader', // 3. Внедряет CSS в DOM
'css-loader', // 2. Разрешает @import и url()
'sass-loader', // 1. Компилирует SCSS в CSS
],
}

Популярные лоадеры:

  • ts-loader / babel-loader — транспиляция TypeScript/JavaScript
  • css-loader — обработка CSS
  • style-loader — внедрение стилей в DOM
  • sass-loader / less-loader — компиляция препроцессоров
  • file-loader / asset modules — обработка файлов

Плагины (Plugins):

Плагины — это расширения, которые подключаются к жизненному циклу Webpack и могут выполнять действия на любом этапе сборки. Они работают на уровне всего бандла.

Ключевые характеристики плагинов:

  • Имеют доступ ко всему процессу сборки через систему хуков
  • Могут выполнять действия на любом этапе (compile, emit, done и т.д.)
  • Конфигурируются в массиве plugins
  • Могет влиять на весь бандл, а не на отдельные файлы

Пример плагина:

const webpack = require('webpack');

class MyCustomPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('MyCustomPlugin', (compilation, callback) => {
// Доступ ко всем файлам бандла
for (const filename in compilation.assets) {
console.log(`Processing: ${filename}`);
}
callback();
});
}
}

Популярные плагины:

  • HtmlWebpackPlugin — генерация HTML с подключением бандлов
  • MiniCssExtractPlugin — извлечение CSS в отдельные файлы
  • DefinePlugin — определение глобальных констант
  • CleanWebpackPlugin — очистка папки сборки
  • BundleAnalyzerPlugin — визуализация размера бандла
  • CopyWebpackPlugin — копирование файлов
  • CompressionPlugin — сжатие gzip/brotli

Сравнительная таблица:

ХарактеристикаЛоадерыПлагины
Уровень работыОтдельные файлыВесь бандл
Когда выполняютсяДо добавления в графНа любом этапе сборки
НазначениеТрансформация файловРасширение функциональности
Конфигурацияmodule.rulesplugins
Порядок выполненияСправа налевоПо порядку в массиве
Доступ к графу зависимостейНетДа (через хуки)

Аналогия для понимания:

Представьте конвейер на фабрике:

  • Лоадеры — это станки, которые обрабатывают отдельные детали (шлифуют, красят, сверлят)
  • Плагины — это управляющие системы, которые контролируют весь конвейер (запуск, остановка, упаковка готовой продукции, отчётность)

Пример расширенного ответа:

«...Лоадеры работают на уровне отдельных файлов — они трансформируют исходный код перед добавлением в граф зависимостей. Например, ts-loader компилирует TypeScript в JavaScript, а css-loader обрабатывает @import и url() в CSS-файлах. Лоадеры выполняются цепочкой, справа налево.

Плагины работают на уровне всего процесса сборки. Они подключаются к системе хуков Webpack и могут выполнять действия на любом этапе — от начала компиляции до записи файлов на диск. Например, HtmlWebpackPlugin генерирует HTML-файл с автоматической подссылкой на собранные бандлы, а DefinePlugin заменяет переменные окружения на этапе компиляции.

Ключевое отличие: лоадер преобразует файл, плагин расширяет возможности самого Webpack...»

Вопрос 10. Как CSS поддерживает адатптивность и кроссбраузерность вёрстки?

Таймкод: 00:16:13

Ответ собеседника: Правильный. Адаптивность поддерживается через принцип Mobile First — сначала вёрстка для телефонов, затем для планшетов и компьютеров. Используются медиазапросы для указания ширины экрана и соответствующих стилей. Также помогают CSS-фреймворки (Bootstrap с грид-системой). Кроссбраузерность обеспечивается через Autoprefixer, который добавляет вендорные префиксы (-webkit-, -moz- и т.д.) для различных CSS-свойств. Подключение происходит на этапе сборки через Webpack.

Правильный ответ:

Ответ хороший, покрывает основные аспекты. Давайте расширим его для более полного понимания темы.

Адаптивность (Responsive Design):

1. Принцип Mobile First:

Mobile First — подход, при котором стили сначала пишутся для мобильных устройств, а затем расширяются для больших экранов через min-width медиазапросы.

/* Базовые стили — для мобильных */
.container {
padding: 16px;
font-size: 14px;
}

/* Планшеты */
@media (min-width: 768px) {
.container {
padding: 24px;
font-size: 16px;
}
}

/* Десктоп */
@media (min-width: 1024px) {
.container {
padding: 32px;
max-width: 1200px;
margin: 0 auto;
}
}

2. Стандартные брейкпоинты:

/* Маленькие телефоны */
@media (min-width: 320px) { }

/* Большие телефоны */
@media (min-width: 480px) { }

/* Планшеты */
@media (min-width: 768px) { }

/* Малые десктопы */
@media (min-width: 1024px) { }

/* Большие десктопы */
@media (min-width: 1200px) { }

/* Экстра большие экраны */
@media (min-width: 1440px) { }

3. Современные CSS-технологии для адаптивности:

CSS Grid:

.grid-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}

Flexbox:

.flex-container {
display: flex;
flex-wrap: wrap;
gap: 16px;
}

.flex-item {
flex: 1 1 300px; /* grow shrink basis */
}

Container Queries (современный подход):

.card-container {
container-type: inline-size;
container-name: card;
}

@container card (min-width: 400px) {
.card {
display: flex;
flex-direction: row;
}
}

@container card (max-width: 399px) {
.card {
display: flex;
flex-direction: column;
}
}

Логические свойства:

/* Вместо margin-left / margin-right */
.element {
margin-inline: 16px;
margin-block: 8px;
padding-inline-start: 20px;
}

4. Адаптивные изображения:

img {
max-width: 100%;
height: auto;
}

/* Или через picture */
<picture>
<source media="(min-width: 1024px)" srcset="large.webp" type="image/webp">
<source media="(min-width: 768px)" srcset="medium.webp" type="image/webp">
<img src="small.jpg" alt="Description" loading="lazy">
</picture>

Кроссбраузерность (Cross-browser Compatibility):

1. Autoprefixer:

Автоматически добавляет вендорные префиксы на основе данных Can I Use.

/* Исходный код */
.element {
display: flex;
user-select: none;
backdrop-filter: blur(10px);
}

/* После Autoprefixer */
.element {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
}

Настройка через browserslist:

// package.json
{
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
]
}

2. Normalize.css / Reset CSS:

Единообразное отображение элементов во всех браузерах.

/* Пример Reset */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}

body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}

3. Feature Queries (@supports):

Проверка поддержки свойств браузером.

.element {
/* Fallback */
display: block;
}

@supports (display: grid) {
.element {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
}

@supports not (display: grid) {
.element {
display: flex;
flex-wrap: wrap;
}
}

4. Polyfills:

Для поддержки старых браузеров.

// Пример: Intersection Observer polyfill
if (!('IntersectionObserver' in window)) {
await import('intersection-observer');
}

5. CSS-переменные с fallback:

.element {
color: #333; /* Fallback */
color: var(--text-color, #333);
}

Настройка Autoprefixer в Webpack:

// webpack.config.js
const autoprefixer = require('autoprefixer');

module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
autoprefixer,
],
},
},
},
],
},
],
},
};

Пример расширенного ответа:

«...Адаптивность реализую через Mobile First подход с использованием медиазапросов. Для сложных макетов применяю CSS Grid с auto-fit и minmax(), что позволяет создавать адаптивные сетки без медиазапросов. Для компонентов, которые должны адаптироваться к размеру контейнера, а не viewport, использую Container Queries.

Кроссбраузерность обеспечиваю через Autoprefixer с настройкой browserslist в package.json. Для критичных CSS-свойств использую @supports с fallback-стилями. Также подключаю Normalize.css для единообразного отображения элементов. При необходимости добавляю polyfills для старых браузеров через условный импорт...»

Вопрос 11. Как сделать кастомный чекбокс с использованием CSS?

Таймкод: 00:18:36

Ответ собеседника: Правильный. Стандартный чекбокс скрывается (делается невидимым), а поверх него с помощью псевдоэлементов ::before или ::after добавляется кастомный элемент, который стилизуется. Состояние включён/выключен отслеживается через псевдокласс :checked.

Правильный ответ:

Ответ верный. Давайте дополним его практическими примерами и рассмотрим разные подходы.

Подход 1: Скрытие нативного чекбокса + label

<div class="checkbox-wrapper">
<input type="checkbox" id="custom-checkbox" class="checkbox-input">
<label for="custom-checkbox" class="checkbox-label">
Согласен с условиями
</label>
</div>
/* Скрываем нативный чекбокс */
.checkbox-input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}

/* Контейнер для кастомного чекбокса */
.checkbox-label {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
user-select: none;
}

/* Кастомный квадрат */
.checkbox-label::before {
content: '';
width: 20px;
height: 20px;
border: 2px solid #ccc;
border-radius: 4px;
background: white;
transition: all 0.2s ease;
flex-shrink: 0;
}

/* Состояние checked */
.checkbox-input:checked + .checkbox-label::before {
background: #007bff;
border-color: #007bff;
}

/* Галочка через псевдоэлемент */
.checkbox-input:checked + .checkbox-label::after {
content: '';
position: absolute;
left: 7px;
top: 3px;
width: 6px;
height: 12px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}

/* Состояние focus для доступности */
.checkbox-input:focus-visible + .checkbox-label::before {
outline: 2px solid #007bff;
outline-offset: 2px;
}

/* Состояние disabled */
.checkbox-input:disabled + .checkbox-label {
opacity: 0.5;
cursor: not-allowed;
}

/* Состояние hover */
.checkbox-input:not(:disabled) + .checkbox-label:hover::before {
border-color: #007bff;
}

Подход 2: Галочка через SVG background

.checkbox-label::before {
content: '';
width: 20px;
height: 20px;
border: 2px solid #ccc;
border-radius: 4px;
background-color: white;
background-repeat: no-repeat;
background-position: center;
background-size: 12px;
transition: all 0.2s ease;
}

.checkbox-input:checked + .checkbox-label::before {
background-color: #007bff;
border-color: #007bff;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z'/%3E%3C/svg%3E");
}

Подход 3: Анимированный чекбокс

.checkbox-animated .checkbox-label::before {
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}

.checkbox-animated .checkbox-input:checked + .checkbox-label::before {
transform: scale(1.1);
}

.checkbox-animated .checkbox-input:checked + .checkbox-label::after {
animation: checkmark 0.3s ease forwards;
}

@keyframes checkmark {
0% {
width: 0;
height: 0;
opacity: 0;
}
50% {
width: 6px;
height: 0;
opacity: 1;
}
100% {
width: 6px;
height: 12px;
opacity: 1;
}
}

Подход 4: Toggle Switch (переключатель)

<div class="toggle-wrapper">
<input type="checkbox" id="toggle" class="toggle-input">
<label for="toggle" class="toggle-label">
<span class="toggle-text">Выкл</span>
</label>
</div>
.toggle-input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}

.toggle-label {
display: flex;
align-items: center;
width: 60px;
height: 30px;
background: #ccc;
border-radius: 15px;
cursor: pointer;
position: relative;
transition: background 0.3s ease;
}

.toggle-label::before {
content: '';
position: absolute;
width: 26px;
height: 26px;
background: white;
border-radius: 50%;
top: 2px;
left: 2px;
transition: transform 0.3s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}

.toggle-input:checked + .toggle-label {
background: #4caf50;
}

.toggle-input:checked + .toggle-label::before {
transform: translateX(30px);
}

.toggle-input:focus-visible + .toggle-label {
outline: 2px solid #4caf50;
outline-offset: 2px;
}

Важные моменты для доступности (a11y):

  1. Не используйте display: none для скрытия чекбокса — это делает его недоступным для screen readers и клавиатурной навигации.

  2. Используйте opacity: 0 или clip-path для визуального скрытия с сохранением доступности.

  3. Добавьте :focus-visible для видимого фокуса при клавиатурной навигации.

  4. Свяжите label с input через for/id или вложенность.

  5. Добавьте ARIA-атрибуты при необходимости:

<input
type="checkbox"
id="agree"
aria-describedby="agree-description"
>
<label for="agree">Согласен</label>
<p id="agree-description" class="sr-only">
Отметьте, если согласны с условиями использования
</p>

Утилитарный класс для скрытия (визуально скрытый, но доступный):

.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}

Пример расширенного ответа:

«...Для кастомного чекбокса скрываю нативный input через opacity: 0 с абсолютным позиционированием, а стилизованный элемент создаю через псевдоэлемент ::before на label. Состояние checked отслеживаю через селектор :checked + .checkbox-label. Галочку реализую через трансформацию border или SVG background.

Важно сохранить доступность: не использую display: none, добавляю :focus-visible для клавиатурной навигации и правильно связываю label с input через for/id. Для сложных случаев добавляю ARIA-атрибуты...»

Вопрос 12. Как можно поддерживать новые конструкции JavaScript (ES6+) в старых браузерах?

Таймкод: 00:20:12

Ответ собеседника: Правильный. С помощью полифилов. В опыте был случай, когда при разработке библиотеки компонентов нужно было поддерживать Android Explorer (9 или 10 версия). Полифил подключался и добавлялся в проект для обеспечения совместимости.

Правильный ответ:

Ответ верный, но для Senior уровня стоит раскрыть тему глубже, показав понимание различных подходов и инструментов.

Транспиляция vs Полифилы:

Важно различать два подхода:

  • Транспиляция — преобразование синтаксиса (стрелочные функции, классы, деструктуризация)
  • Полифилы — реализация новых API (Promise, Array.from, Object.assign)

1. Babel — транспилятор JavaScript:

Babel преобразует современный синтаксис в совместимый со старыми браузерами.

// Исходный код (ES6+)
const greet = (name) => `Hello, ${name}!`;
const users = ['Alice', 'Bob'];
const [first, ...rest] = users;

// После Babel (ES5)
var greet = function greet(name) {
return 'Hello, ' + name + '!';
};
var users = ['Alice', 'Bob'];
var first = users[0];
var rest = users.slice(1);

Настройка Babel:

// babel.config.json
{
"presets": [
["@babel/preset-env", {
"targets": "> 0.5%, last 2 versions, not dead",
"useBuiltIns": "usage",
"corejs": 3
}]
],
"plugins": [
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-nullish-coalescing-operator"
]
}

2. Core-js — полифилы для новых API:

// Импорт конкретных полифилов
import 'core-js/features/promise';
import 'core-js/features/array/from';
import 'core-js/features/object/assign';
import 'core-js/features/symbol';

// Или весь core-js (не рекомендуется для production)
import 'core-js';

3. @babel/polyfill (устаревший, заменён core-js):

// Было (Babel 7.4-)
import '@babel/polyfill';

// Стало (Babel 7.4+)
import 'core-js/stable';
import 'regenerator-runtime/runtime';

4. useBuiltIns: "usage" — автоматическое подключение полифилов:

// babel.config.json
{
"presets": [
["@babel/preset-env", {
"useBuiltIns": "usage",
"corejs": 3
}]
]
}

При таком подходе Babel автоматически анализирует код и подключает только необходимые полифилы на основе browserslist.

5. Полифилы для Web API:

// Intersection Observer
import 'intersection-observer';

// Resize Observer
import 'resize-observer-polyfill';

// Fetch API
import 'whatwg-fetch';

// Web Components
import '@webcomponents/webcomponentsjs';

// Custom Elements
import '@webcomponents/custom-elements';

6. Условная загрузка полифилов:

// Проверяем поддержку и загружаем полифил при необходимости
if (!('IntersectionObserver' in window)) {
await import('intersection-observer');
}

// Или через dynamic import с feature detection
const loadPolyfills = async () => {
const polyfills = [];

if (!('Promise' in window)) {
polyfills.push(import('core-js/features/promise'));
}

if (!('fetch' in window)) {
polyfills.push(import('whatwg-fetch'));
}

if (!('IntersectionObserver' in window)) {
polyfills.push(import('intersection-observer'));
}

await Promise.all(polyfills);
};

await loadPolyfills();

7. Polyfill.io — сервис для автоматической загрузки полифилов:

<script src="https://polyfill.io/v3/polyfill.min.js?features=Promise,fetch,IntersectionObserver"></script>

8. Настройка browserslist:

// package.json
{
"browserslist": [
"> 0.5%",
"last 2 versions",
"Firefox ESR",
"not dead",
"not ie 11"
]
}

9. Интеграция с Webpack:

// webpack.config.js
module.exports = {
entry: ['core-js/stable', 'regenerator-runtime/runtime', './src/index.js'],
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: 3,
}]
]
}
}
}
]
}
};

10. Распространённые полифилы:

APIПолифил
Promisecore-js/features/promise
Array.fromcore-js/features/array/from
Object.assigncore-js/features/object/assign
Symbolcore-js/features/symbol
Map/Setcore-js/features/map, core-js/features/set
Fetchwhatwg-fetch
IntersectionObserverintersection-observer
ResizeObserverresize-observer-polyfill
Custom Elements@webcomponents/custom-elements

Пример расширенного ответа:

«...Для поддержки ES6+ в старых браузерах использую комбинацию Babel и полифилов. Babel транспилирует синтаксис (стрелочные функции, классы, деструктуризация) в ES5. Для новых API (Promise, Array.from и т.д.) подключаю полифилы из core-js.

Настраиваю Babel с пресетом @babel/preset-env и useBuiltIns: "usage" — это позволяет автоматически подключать только те полифилы, которые реально используются в коде, на основе browserslist. Для Web API (IntersectionObserver, Fetch) использую отдельные полифилы с условной загрузкой через динамический import, чтобы не нагружать современные браузеры.

В одном проекте нужно было поддерживать старый Android Browser — подключил core-js с полным набором полифилов и добавил полифилы для IntersectionObserver и Custom Elements...»

Вопрос 13. Какие типы данных есть в JavaScript? Какие коллекции данных вы знаете?

Таймкод: 00:21:13

Ответ собеседника: Правильный. Типы данных делятся на примитивы и непримитивы. Примитивы: число, строка, null, boolean, undefined, символ, bigint. Непримитивы: объект. Из коллекций данных знакомы Map и Set. Map — это коллекция ключ-значение, отличающаяся от объекта тем, что ключи могут быть любого типа. Основан на хэш-таблице, что обеспечивает быстрый доступ к данным (O(1)). Set — структура данных для хранения только уникальных значений.

Правильный ответ:

Ответ хороший, но есть небольшая неточность: объект — это не примитив, а ссылочный тип. Давайте дополним и расширим ответ.

Типы данных JavaScript:

Примитивы (8 типов):

  1. number — числа (целые и дробные)

    let int = 42;
    let float = 3.14;
    let nan = NaN; // тоже number
    let infinity = Infinity;
  2. string — строки

    let str = 'Hello';
    let template = `Value: ${value}`;
  3. boolean — логические значения

    let isTrue = true;
    let isFalse = false;
  4. undefined — неопределённое значение

    let x;
    console.log(x); // undefined
  5. null — отсутствие значения

    let empty = null;
  6. symbol — уникальные идентификаторы (ES6)

    const id = Symbol('id');
    const id2 = Symbol('id');
    console.log(id === id2); // false
  7. bigint — большие целые числа (ES2020)

    const big = 9007199254740991n;
    const big2 = BigInt('12345678901234567890');
  8. object — объекты (включая массивы, функции, даты и т.д.)

    const obj = &#123; key: 'value' &#125;;
    const arr = [1, 2, 3];
    const fn = () =&gt; &#123;&#125;;

Коллекции данных:

1. Object — обычный объект:

const obj = {
name: 'John',
age: 30,
};

// Ограничения:
// - Ключи только строки или Symbols
// - Нет гарантии порядка (до ES2015)
// - Нет свойства length

2. Map — коллекция ключ-значение:

const map = new Map();

// Ключи любого типа
map.set('string', 'value');
map.set(42, 'number key');
map.set({ id: 1 }, 'object key');
map.set(() => {}, 'function key');

// Методы
map.get('string'); // 'value'
map.has('string'); // true
map.delete('string');
map.size; // 3

// Итерация
for (const [key, value] of map) {
console.log(key, value);
}

map.forEach((value, key) => {
console.log(key, value);
});

// Преимущества перед Object:
// - Ключи любого типа
// - Порядок вставки сохраняется
// - Быстрый доступ O(1)
// - Есть свойство size
// - Лучше производительность при частом добавлении/удалении

3. Set — коллекция уникальных значений:

const set = new Set();

set.add(1);
set.add(2);
set.add(2); // Дубликат игнорируется
set.add(3);

set.size; // 3
set.has(2); // true
set.delete(2);

// Уникальность по ссылке для объектов
const obj1 = { id: 1 };
const obj2 = { id: 1 };
set.add(obj1);
set.add(obj2); // Добавится, т.к. разные ссылки

// Удаление дубликатов из массива
const arr = [1, 2, 2, 3, 3, 4];
const unique = [...new Set(arr)]; // [1, 2, 3, 4]

// Операции над множествами
const setA = new Set([1, 2, 3]);
const setB = new Set([2, 3, 4]);

// Пересечение
const intersection = new Set([...setA].filter(x => setB.has(x)));

// Объединение
const union = new Set([...setA, ...setB]);

// Разность
const difference = new Set([...setA].filter(x => !setB.has(x)));

4. WeakMap — Map со слабыми ссылками:

const weakMap = new WeakMap();

// Только объекты как ключи
const key = {};
weakMap.set(key, 'value');

// Нет итерации, нет size
// Автоматическая сборка мусора при удалении ключа

// Использование: приватные данные, кэширование
class PrivateData {
#data = new WeakMap();

set(obj, value) {
this.#data.set(obj, value);
}

get(obj) {
return this.#data.get(obj);
}
}

5. WeakSet — Set со слабыми ссылками:

const weakSet = new WeakSet();

// Только объекты
const obj = {};
weakSet.add(obj);

// Нет итерации, нет size
// Автоматическая сборка мусора

// Использование: маркировка объектов
const processed = new WeakSet();

function process(obj) {
if (processed.has(obj)) {
return; // Уже обработан
}
processed.add(obj);
// Обработка...
}

6. Array — массивы:

const arr = [1, 2, 3];

// Основные методы
arr.push(4); // Добавить в конец
arr.pop(); // Удалить с конца
arr.unshift(0); // Добавить в начало
arr.shift(); // Удалить с начала

// Функциональные методы
arr.map(x => x * 2);
arr.filter(x => x > 1);
arr.reduce((acc, x) => acc + x, 0);
arr.find(x => x === 2);
arr.findIndex(x => x === 2);
arr.some(x => x > 2);
arr.every(x => x > 0);
arr.forEach(x => console.log(x));

// Сортировка
arr.sort((a, b) => a - b);

// Spread и деструктуризация
const newArr = [...arr, 4, 5];
const [first, second, ...rest] = arr;

Сравнение коллекций:

ХарактеристикаObjectArrayMapSetWeakMapWeakSet
Тип ключейString, SymbolЧисловой индексЛюбойТолько объектыТолько объекты
ПорядокС 2015ДаДаДаНетНет
Итерацияfor...infor...offor...offor...ofНетНет
РазмерObject.keys().lengthlengthsizesizeНетНет
Слабые ссылкиНетНетНетНетДаДа

Пример расширенного ответа:

«...В JavaScript 8 типов данных: 7 примитивов (number, string, boolean, undefined, null, symbol, bigint) и object (включая массивы, функции, даты).

Из коллекций использую Map и Set. Map — коллекция ключ-значение, где ключи могут быть любого типа, в отличие от обычного объекта. Сохраняет порядок вставки и имеет свойство size. Set — коллекция уникальных значений, удобна для удаления дубликатов.

Также знаком с WeakMap и WeakSet — они используют слабые ссылки, что позволяет сборщику мусора автоматически удалять элементы. WeakMap удобен для хранения приватных данных объектов, WeakSet — для маркировки обработанных объектов...»

Вопрос 14. Какие методы работы с массивами вы наиболее часто используете? Для чего используется цикл for...of и в чём его отличие от for...in?

Таймкод: 00:25:27

Ответ собеседника: Правильный. Чаще всего используются: reduce, forEach, filter. Цикл for...of проходит по итерируемому объекту (массивы, строки, Set, Map). for...in используется для перебора свойств объекта. Для определения итерируемости коллекции можно использовать Symbol.iterator — функцию, которая отвечает за то, как итерируются элементы объекта.

Правильный ответ:

Ответ хороший, но есть неточность: for...of не работает с обычными объектами (они не итерируемы). Давайте расширим и дополним.

Наиболее используемые методы массивов:

1. Трансформация:

// map — преобразование каждого элемента
const numbers = [1, 2, 3, 4];
const doubled = numbers.map(x => x * 2); // [2, 4, 6, 8]

// flatMap — map + flat(1)
const sentences = ['Hello world', 'Good morning'];
const words = sentences.flatMap(s => s.split(' '));
// ['Hello', 'world', 'Good', 'morning']

2. Фильтрация:

// filter — отбор элементов по условию
const numbers = [1, 2, 3, 4, 5, 6];
const evens = numbers.filter(x => x % 2 === 0); // [2, 4, 6]

// find — первый элемент по условию
const users = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
];
const user = users.find(u => u.id === 2); // { id: 2, name: 'Jane' }

// findIndex — индекс первого элемента по условию
const index = users.findIndex(u => u.name === 'Jane'); // 1

3. Агрегация:

// reduce — свёртка в одно значение
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((acc, x) => acc + x, 0); // 15

// Группировка через reduce
const users = [
{ name: 'John', role: 'admin' },
{ name: 'Jane', role: 'user' },
{ name: 'Bob', role: 'admin' },
];
const grouped = users.reduce((acc, user) => {
(acc[user.role] = acc[user.role] || []).push(user);
return acc;
}, {});
// { admin: [{ name: 'John' }, { name: 'Bob' }], user: [{ name: 'Jane' }] }

4. Проверка:

// some — хотя бы один элемент удовлетворяет условию
const numbers = [1, 2, 3];
const hasEven = numbers.some(x => x % 2 === 0); // true

// every — все элементы удовлетворяют условию
const allPositive = numbers.every(x => x > 0); // true

// includes — массив содержит элемент
const fruits = ['apple', 'banana', 'orange'];
fruits.includes('banana'); // true

5. Итерация:

// forEach — перебор элементов
users.forEach((user, index) => {
console.log(index, user.name);
});

for...of vs for...in:

for...of — перебор значений итерируемых объектов:

// Массив
const arr = [10, 20, 30];
for (const value of arr) {
console.log(value); // 10, 20, 30
}

// Строка
const str = 'Hello';
for (const char of str) {
console.log(char); // H, e, l, l, o
}

// Set
const set = new Set([1, 2, 3]);
for (const value of set) {
console.log(value); // 1, 2, 3
}

// Map
const map = new Map([['a', 1], ['b', 2]]);
for (const [key, value] of map) {
console.log(key, value); // a 1, b 2
}

// NodeList (DOM)
// for (const node of document.querySelectorAll('div')) { }

// Обычный объект — ОШИБКА!
// const obj = { a: 1, b: 2 };
// for (const x of obj) { } // TypeError: obj is not iterable

for...in — перечисление свойств объекта:

// Объект
const obj = { a: 1, b: 2, c: 3 };
for (const key in obj) {
console.log(key, obj[key]); // a 1, b 2, c 3
}

// Массив (не рекомендуется!)
const arr = ['a', 'b', 'c'];
arr.custom = 'property';
for (const index in arr) {
console.log(index, arr[index]);
// 0 a, 1 b, 2 c, custom property
}

// for...in перебирает ВСЕ перечислимые свойства,
// включая унаследованные от прототипа

Ключевые отличия:

Характеристикаfor...offor...in
Что перебираетЗначенияКлючи/индексы
Для чегоИтерируемые объектыПеречислимые свойства
ПрототипНетДа (все перечислимые)
break/continueДаДа
awaitДаНет

Symbol.iterator — протокол итератора:

// Создание итерируемого объекта
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}

[Symbol.iterator]() {
let current = this.start;
const end = this.end;

return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { done: true };
}
};
}
}

const range = new Range(1, 5);
for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
}

// Spread тоже работает
const arr = [...new Range(1, 5)]; // [1, 2, 3, 4, 5]

Пример расширенного ответа:

«...Из методов массивов чаще всего использую map для трансформации, filter для фильтрации, reduce для агрегации. Также find для поиска элемента, some/every для проверки условий.

for...of перебирает значения итерируемых объектов: массивы, строки, Set, Map. for...in перечисляет перечислимые свойства объекта, включая унаследованные от прототипа — поэтому для массивов его лучше не использовать.

Итерируемость определяется наличием Symbol.iterator — это функция, возвращающая объект с методом next(). Можно создать свой итерируемый объект, реализовав этот протокол...»

Вопрос 15. Как отправить сетевой запрос на сервер, получить данные и вывести их в консоль с использованием промисов?

Таймкод: 00:28:02

Ответ собеседника: Правильный. Самый простой способ — использовать fetch. Указываем URL, fetch возвращает response. Затем можно вызвать .json() для получения данных и вывести их через console.log. Если конкретно с промисом — создаём промис, где два метода: resolve (при успешном ответе) и reject (при ошибке).

Правильный ответ:

Ответ корректный. Давайте дополним его практическими примерами и рассмотрим разные подходы.

1. Fetch API с промисами:

// Базовый пример
fetch('https://api.example.com/users')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('Users:', data);
})
.catch(error => {
console.error('Error:', error);
});

2. Fetch с async/await:

async function fetchUsers() {
try {
const response = await fetch('https://api.example.com/users');

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();
console.log('Users:', data);
return data;
} catch (error) {
console.error('Error:', error);
throw error;
}
}

fetchUsers();

3. Fetch с параметрами запроса:

async function fetchWithParams() {
const params = new URLSearchParams({
page: 1,
limit: 10,
sort: 'name',
});

const response = await fetch(
`https://api.example.com/users?${params}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123',
},
}
);

const data = await response.json();
console.log(data);
}

4. POST-запрос:

async function createUser(userData) {
try {
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData),
});

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();
console.log('Created user:', data);
return data;
} catch (error) {
console.error('Error:', error);
}
}

createUser({ name: 'John', email: 'john@example.com' });

5. XMLHttpRequest с промисами:

function fetchWithXHR(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);

xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.response));
} else {
reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`));
}
};

xhr.onerror = () => reject(new Error('Network error'));
xhr.ontimeout = () => reject(new Error('Timeout'));

xhr.send();
});
}

fetchWithXHR('https://api.example.com/users')
.then(data => console.log(data))
.catch(error => console.error(error));

6. Обёртка для fetch с таймаутом:

async function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);

try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Request timeout');
}
throw error;
} finally {
clearTimeout(timeoutId);
}
}

// Использование
fetchWithTimeout('https://api.example.com/users', {}, 3000)
.then(data => console.log(data))
.catch(error => console.error(error));

7. Обёртка с retry:

async function fetchWithRetry(url, options = {}, retries = 3, delay = 1000) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, options);

if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}

return await response.json();
} catch (error) {
console.log(`Attempt ${i + 1} failed: ${error.message}`);

if (i === retries - 1) {
throw error;
}

await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
}
}
}

fetchWithRetry('https://api.example.com/users', {}, 3, 1000)
.then(data => console.log(data))
.catch(error => console.error('All retries failed:', error));

8. Axios (популярная альтернатива):

import axios from 'axios';

async function fetchWithAxios() {
try {
const response = await axios.get('https://api.example.com/users', {
params: { page: 1, limit: 10 },
headers: { Authorization: 'Bearer token123' },
timeout: 5000,
});

console.log(response.data);
return response.data;
} catch (error) {
if (error.response) {
// Сервер ответил с ошибкой
console.error('Server error:', error.response.status);
} else if (error.request) {
// Запрос отправлен, но ответа нет
console.error('No response:', error.request);
} else {
console.error('Error:', error.message);
}
}
}

Пример расширенного ответа:

«...Для сетевых запросов использую Fetch API, который возвращает промис. Базовый вызов: fetch(url).then(response => response.json()).then(data => console.log(data)).catch(error => console.error(error)).

Важно проверять response.ok — fetch не отклоняет промис при HTTP-ошибках (4xx, 5xx), только при сетевых проблемах. Для более сложных сценариев создаю обёртки: с таймаутом через AbortController, с retry-логикой при временных сбоях.

async/await делает код чище: const response = await fetch(url); const data = await response.json(). Но не забываю оборачивать в try/catch...»

Вопрос 16. В чём отличие куки от Local Storage? Какой способ хранения токена авторизации наиболее безопасен?

Таймкод: 00:30:43

Ответ собеседника: Правильный. Local Storage доступен только на клиенте, и любой человек может зайти в инструменты разработчика и прочитать/изменить данные. Куки можно читать и на сервере, и на клиенте. Для безопасности куки можно защитить флагом HttpOnly, чтобы к ней нельзя было обратиться из JavaScript. Самый безопасный вариант хранения токена — кука с флагом HttpOnly и передача по протоколу HTTPS.

Правильный ответ:

Ответ отличный, покрывает все ключевые аспекты. Давайте дополним его деталями.

Сравнение хранилищ:

ХарактеристикаCookieLocal StorageSession Storage
Размер~4KB~5-10MB~5-10MB
Доступ с сервераДаНетНет
Срок жизниНастраиваемыйБессроковоДо закрытия вкладки
Отправка с запросамиАвтоматическиНетНет
Доступ из JSДа (без HttpOnly)ДаДа
APIdocument.cookielocalStoragesessionStorage

Флаги куки:

// Установка куки с флагами
document.cookie = 'name=value; expires=Thu, 01 Jan 2025 00:00:00 UTC; path=/; Secure; HttpOnly; SameSite=Strict';

Ключевые флаги безопасности:

  1. HttpOnly — запрещает доступ из JavaScript (защита от XSS)

    // Сервер устанавливает заголовок
    Set-Cookie: token=abc123; HttpOnly; Secure; SameSite=Strict
    // document.cookie не увидит эту куку
  2. Secure — кука отправляется только по HTTPS

    Set-Cookie: token=abc123; Secure
  3. SameSite — защита от CSRF

    // Strict — кука не отправляется при кросс-доменных запросах
    Set-Cookie: token=abc123; SameSite=Strict

    // Lax — кука отправляется только при GET-запросах с другого домена
    Set-Cookie: token=abc123; SameSite=Lax

    // None — кука отправляется всегда (требует Secure)
    Set-Cookie: token=abc123; SameSite=None; Secure

Способы хранения токена (от менее к более безопасному):

1. Local Storage (небезопасно):

// Сохранение
localStorage.setItem('token', 'eyJhbGciOiJIUzI1NiIs...');

// Получение
const token = localStorage.getItem('token');

// Отправка с запросом
fetch('/api/users', {
headers: {
'Authorization': `Bearer ${token}`,
},
});

Проблемы: Доступен из любого JavaScript-кода, уязвим к XSS-атакам.

2. Cookie без HttpOnly (средняя безопасность):

// Установка из JS
document.cookie = 'token=abc123; path=/; Secure; SameSite=Lax';

// Чтение
const token = document.cookie
.split('; ')
.find(row => row.startsWith('token='))
?.split('=')[1];

Проблемы: Доступен из JavaScript, уязвим к XSS.

3. Cookie с HttpOnly (рекомендуется):

// Сервер устанавливает куку
// Set-Cookie: token=abc123; HttpOnly; Secure; SameSite=Strict; Path=/

// Кука автоматически отправляется с каждым запросом
fetch('/api/users', {
credentials: 'include', // Важно для кросс-доменных запросов
});

Преимущедства: Недоступна из JavaScript, защищена от XSS.

4. Разделение токена (максимальная безопасность):

// Access Token — короткоживущий, в памяти JS
let accessToken = null;

// Refresh Token — долгоживущий, в HttpOnly cookie
// Set-Cookie: refresh=xyz789; HttpOnly; Secure; SameSite=Strict; Path=/auth

// Функция для получения access token
async function getAccessToken() {
if (accessToken) return accessToken;

// Refresh token автоматически отправляется с запросом
const response = await fetch('/auth/refresh', {
method: 'POST',
credentials: 'include',
});

const data = await response.json();
accessToken = data.accessToken;

// Автоматическое обновление перед истечением
setTimeout(() => {
accessToken = null;
}, data.expiresIn * 1000 - 60000);

return accessToken;
}

// Использование
async function fetchUsers() {
const token = await getAccessToken();

const response = await fetch('/api/users', {
headers: {
'Authorization': `Bearer ${token}`,
},
});

return response.json();
}

Пример расширенного ответа:

«...Cookie отличается от Local Storage тем, что автоматически отправляется с каждым HTTP-запросом и доступна серверу. Local Storage хранит больше данных, но доступен только клиенту.

Для хранения токена авторизации самый безопасный вариант — HttpOnly cookie. Она недоступна из JavaScript, что защищает от XSS-атак. Добавляю флаги Secure (только HTTPS) и SameSite=Strict (защита от CSRF).

Ещё более безопасный подход — разделение токенов: access token хранится в памяти JS (живёт мало), refresh token — в HttpOnly cookie. Это ограничивает ущерб при компромитации любого из токенов...»

Вопрос 17. Что такое React Fragment, зачем он нужен и где используется?

Таймкод: 00:33:18

Ответ собеседника: Правильный. React Fragment — это пустой блок в JSX, который не добавляет лишний элемент в DOM. Используется, когда нужно вернуть несколько элементов без обёртки в div, чтобы не нарушать структуру вёрстки (например, в таблицах с жёсткой структурой table/thead/tbody). Fragment не видно в разметке. Ему можно задать атрибут key при использовании в списках.

Правильный ответ:

Ответ отличный, покрывает все ключевые аспекты. Давайте дополним его примерами.

Что такое Fragment:

Fragment — это специальный компонент, который позволяет группировать несколько элементов без добавления дополнительного узла в DOM.

Синтаксис:

// Полный синтаксис
import { Fragment } from 'react';

function Component() {
return (
<Fragment>
<h1>Заголовок</h1>
<p>Параграф</p>
</Fragment>
);
}

// Сокращённый синтаксис (рекомендуется)
function Component() {
return (
<>
<h1>Заголовок</h1>
<p>Параграф</p>
</>
);
}

Пробрема без Fragment:

// Ошибка: JSX должен иметь один корневой элемент
function Component() {
return (
<h1>Заголовок</h1>
<p>Параграф</p>
);
}

// Решение без Fragment — лишний div
function Component() {
return (
<div>
<h1>Заголовок</h1>
<p>Параграф</p>
</div>
);
}

Случаи использования:

1. Таблицы:

function Table() {
return (
<table>
<thead>
<tr>
<TableHeader />
</tr>
</thead>
<tbody>
<TableBody />
</tbody>
</table>
);
}

function TableHeader() {
// Без Fragment пришлось бы оборачивать в div, что сломает таблицу
return (
<>
<th>Имя</th>
<th>Возраст</th>
<th>Город</th>
</>
);
}

function TableBody() {
const users = [
{ id: 1, name: 'John', age: 30, city: 'NYC' },
{ id: 2, name: 'Jane', age: 25, city: 'LA' },
];

return (
<>
{users.map(user => (
<tr key={user.id}>
<td>{user.name}</td>
<td>{user.age}</td>
<td>{user.city}</td>
</tr>
))}
</>
);
}

2. Списки с условиями:

function UserList({ users, showAdmins }) {
return (
<ul>
{users
.filter(user => !showAdmins || user.isAdmin)
.map(user => (
<Fragment key={user.id}>
<li>{user.name}</li>
{user.isAdmin && <li className="badge">Admin</li>}
</Fragment>
))}
</ul>
);
}

3. Семантическая вёрстка:

function Article() {
return (
<article>
<h1>Заголовок статьи</h1>
{/* Не хотим оборачивать в div для стилизации */}
<>
<p>Первый параграф</p>
<p>Второй параграф</p>
<blockquote>Цитата</blockquote>
</>
<footer>Автор: Иван</footer>
</article>
);
}

4. Flexbox/Grid контейнеры:

function CardList() {
return (
<div style={{ display: 'flex', gap: '16px' }}>
<Card />
<Card />
<Card />
</div>
);
}

function Card() {
return (
<>
<h3>Заголовок</h3>
<p>Описание</p>
<button>Действие</button>
</>
);
}

Fragment с key (для списков):

function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => (
<Fragment key={todo.id}>
<li>{todo.title}</li>
{todo.description && <li className="description">{todo.description}</li>}
</Fragment>
))}
</ul>
);
}

Важное ограничение:

// Работает только с полным синтаксисом
<Fragment key={item.id}>...</Fragment>

// Сокращённый синтаксис НЕ поддерживает key
<>...</> // Ошибка: key не передать

Когда использовать Fragment vs div:

Используйте FragmentИспользуйте div
Не нужна обёртка в DOMНужна обёртка для стилей
Семантика важнаНужен контейнер для layout
Таблицы, спискиFlexbox/Grid контейнер
Не нужны обработчики событийНужен onClick и т.д.

Пример расширенного ответа:

«...React Fragment — это компонент, который не создаёт элемент в DOM. Использую его, когда нужно вернуть несколько элементов из компонента без лишней обёртки в div.

Основные случаи: таблицы (где div сломает структуру), списки, семантическая вёрстка. Сокращённый синтаксис <>...</> удобнее, но не поддерживает key — для списков нужен полный <Fragment key={id}>...</Fragment>.

Fragment не добавляет узел в DOM, что улучшает производительность и сохраняет чистоту разметки...»

Вопрос 18. В чём отличие управляемого и неуправляемого инпута в React? Когда лучше использовать каждый из них?

Таймкод: 00:34:43

Ответ собеседника: Правильный. Управляемый инпут контролируется через value и onChange — состояние хранится в React и обновляется через setState. Неуправляемый инпут сам управляет своим значением внутри, и получить его можно при сабмите. Управляемые лучше использовать, когда нужно точно знать значение (селекты, автокомплиты, сложные объектные структуры). Неуправляемые — когда значение нужно только при сабмите.

Правильный ответ:

Ответ отличный, покрывает все ключевые аспекты и даже упоминает проблему производительности. Давайте дополним его примерами.

Управляемый (Controlled) инпут:

import { useState } from 'react';

function ControlledForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');

const handleSubmit = (e) => {
e.preventDefault();
console.log({ name, email });
};

return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Имя"
/>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<button type="submit">Отправить</button>
</form>
);
}

Преимущества управляемых инпутов:

  • Мгновенный доступ к значению
  • Валидация на каждом изменении
  • Условное отображение на основе значения
  • Программное изменение значения
  • Форматирование ввода

Неуправляемый (Uncontrolled) инпут:

import { useRef } from 'react';

function UncontrolledForm() {
const nameRef = useRef(null);
const emailRef = useRef(null);

const handleSubmit = (e) => {
e.preventDefault();
console.log({
name: nameRef.current.value,
email: emailRef.current.value,
});
};

return (
<form onSubmit={handleSubmit}>
<input
type="text"
ref={nameRef}
defaultValue=""
placeholder="Имя"
/>
<input
type="email"
ref={emailRef}
defaultValue=""
placeholder="Email"
/>
<button type="submit">Отправить</button>
</form>
);
}

Сравнение:

ХарактеристикаУправляемыйНеуправляемый
Хранение состоянияReact stateDOM
Доступ к значениюМгновенныйЧерез ref
  • Ре-рендер при каждом вводе | Да | Нет | | Валидация в реальном времени | Легко | Сложнее | | Производительность | Хуже при многих инпутах | Лучше | | Тестирование | Легче | Сложнее |

Когда использовать управляемые:

// 1. Валидация в реальном времени
function ValidatedInput() {
const [email, setEmail] = useState('');
const [error, setError] = useState('');

const handleChange = (e) => {
const value = e.target.value;
setEmail(value);

if (value && !value.includes('@')) {
setError('Введите корректный email');
} else {
setError('');
}
};

return (
<div>
<input
type="email"
value={email}
onChange={handleChange}
className={error ? 'error' : ''}
/>
{error && <span className="error-text">{error}</span>}
</div>
);
}

// 2. Форматирование ввода
function PhoneInput() {
const [phone, setPhone] = useState('');

const handleChange = (e) => {
const value = e.target.value.replace(/\D/g, '');
const formatted = value.replace(
/(\d{3})(\d{3})(\d{4})/,
'($1) $2-$3'
);
setPhone(formatted);
};

return (
<input
type="tel"
value={phone}
onChange={handleChange}
placeholder="(123) 456-7890"
/>
);
}

// 3. Зависимые поля
function DependentFields() {
const [country, setCountry] = useState('');
const [city, setCity] = useState('');

// Сброс города при смене страны
const handleCountryChange = (e) => {
setCountry(e.target.value);
setCity(''); // Программный сброс
};

return (
<>
<select value={country} onChange={handleCountryChange}>
<option value="">Выберите страну</option>
<option value="ru">Россия</option>
<option value="us">США</option>
</select>
<input
type="text"
value={city}
onChange={(e) => setCity(e.target.value)}
disabled={!country}
/>
</>
);
}

Когда использовать неуправляемые:

// 1. Простые формы с отправкой
function SimpleSearch() {
const inputRef = useRef();

const handleSubmit = (e) => {
e.preventDefault();
search(inputRef.current.value);
};

return (
<form onSubmit={handleSubmit}>
<input type="text" ref={inputRef} />
<button type="submit">Найти</button>
</form>
);
}

// 2. Загрузка файлов
function FileUpload() {
const fileRef = useRef();

const handleSubmit = (e) => {
e.preventDefault();
const file = fileRef.current.files[0];
uploadFile(file);
};

return (
<form onSubmit={handleSubmit}>
<input type="file" ref={fileRef} />
<button type="submit">Загрузить</button>
</form>
);
}

// 3. Интеграция с не-React кодом
function ThirdPartyIntegration() {
const inputRef = useRef();

useEffect(() => {
// Инициализация jQuery плагина
$(inputRef.current).datepicker();
}, []);

return <input type="text" ref={inputRef} />;
}

Оптимизация управляемых инпутов:

// 1. Вынос инпута в отдельный компонент
function OptimizedForm() {
return (
<form>
<InputField name="firstName" />
<InputField name="lastName" />
<InputField name="email" />
{/* Каждый инпут ре-рендерит только себя */}
</form>
);
}

const InputField = memo(function InputField({ name }) {
const [value, setValue] = useState('');

return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder={name}
/>
);
});

// 2. Debounce для частых обновлений
function DebouncedInput() {
const [inputValue, setInputValue] = useState('');
const [debouncedValue, setDebouncedValue] = useState('');

useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(inputValue);
}, 300);

return () => clearTimeout(timer);
}, [inputValue]);

return (
<div>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<p>Debounced: {debouncedValue}</p>
</div>
);
}

Пример расширенного ответа:

«...Управляемый инпут контролируется React через value и onChange — состояние хранится в компоненте. Неуправляемый использует ref для доступа к DOM-элементу напрямую.

Управляющие инпуты использую для: валидации в реальном времени, форматирования ввода, зависимых полей, сложных форм. Неуправляемые — для простых форм, загрузки файлов, интеграции с не-React библиотеками.

Проблема управляемых инпутов — ре-рендер при каждом вводе. Оптимизирую через вынос в отдельные компоненты с memo или debounce для частых обновлений...»

Вопрос 19. Как организовать валидацию формы с большим количеством инпутов без ущерба для производительности?

Таймкод: 00:37:11

Ответ собеседника: Правильный. Можно использовать библиотеку Formik, которая хорошо валидирует формы. Если писать кастомное решение — создать хук useValidation, который принимает поля и схему валидации, и возвращает валидированные данные. Также можно вынести управление состоянием каждого инпута в отдельный компонент (ValidationInput), где инпут сам контролирует своё значение и валидирует себя.

Правильный ответ:

Ответ хороший, предлагает несколько подходов. Давайте дополним его примерами и рассмотрим другие решения.

1. React Hook Form (рекомендуется):

import { useForm } from 'react-hook-form';

function LargeForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm();

const onSubmit = (data) => {
console.log(data);
};

return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register('firstName', { required: 'Имя обязательно' })}
placeholder="Имя"
/>
{errors.firstName && <span>{errors.firstName.message}</span>}

<input
{...register('email', {
required: 'Email обязателен',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Некорректный email',
},
})}
placeholder="Email"
/>
{errors.email && <span>{errors.email.message}</span>}

{/* Ещё 38 инпутов */}

<button type="submit">Отправить</button>
</form>
);
}

2. Кастомный хук с изоляцией состояния:

// useField.js
function useField(validate) {
const [value, setValue] = useState('');
const [error, setError] = useState('');
const [touched, setTouched] = useState(false);

const handleChange = (e) => {
const newValue = e.target.value;
setValue(newValue);

if (touched) {
setError(validate(newValue));
}
};

const handleBlur = () => {
setTouched(true);
setError(validate(value));
};

return {
value,
error,
touched,
handleChange,
handleBlur,
};
}

// FormInput.js
const FormInput = memo(function FormInput({ label, validate, ...props }) {
const field = useField(validate);

return (
<div>
<label>{label}</label>
<input
value={field.value}
onChange={field.handleChange}
onBlur={field.handleBlur}
{...props}
/>
{field.touched && field.error && (
<span className="error">{field.error}</span>
)}
</div>
);
});

// LargeForm.js
function LargeForm() {
const formRef = useRef({});

const register = (name, ref) => {
formRef.current[name] = ref;
};

const handleSubmit = (e) => {
e.preventDefault();

const data = {};
const errors = {};

Object.entries(formRef.current).forEach(([name, ref]) => {
data[name] = ref.getValue();
const error = ref.validate();
if (error) errors[name] = error;
});

if (Object.keys(errors).length === 0) {
console.log(data);
}
};

return (
<form onSubmit={handleSubmit}>
<FormInput
label="Имя"
validate={(v) => !v && 'Имя обязательно'}
ref={(ref) => register('name', ref)}
/>
<FormInput
label="Email"
validate={(v) => !v.includes('@') && 'Некорректный email'}
ref={(ref) => register('email', ref)}
/>
{/* Ещё 38 инпутов */}
<button type="submit">Отправить</button>
</form>
);
}

3. Zod + React Hook Form:

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const schema = z.object({
firstName: z.string().min(1, 'Имя обязательно'),
lastName: z.string().min(1, 'Фамилия обязательна'),
email: z.string().email('Некорректный email'),
age: z.number().min(18, 'Минимум 18 лет'),
password: z.string().min(8, 'Минимум 8 символов'),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: 'Пароли не совпадают',
path: ['confirmPassword'],
});

function ValidatedForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm({
resolver: zodResolver(schema),
});

const onSubmit = async (data) => {
await submitForm(data);
};

return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('firstName')} placeholder="Имя" />
{errors.firstName && <span>{errors.firstName.message}</span>}

<input {...register('email')} placeholder="Email" />
{errors.email && <span>{errors.email.message}</span>}

<input {...register('age')} type="number" placeholder="Возраст" />
{errors.age && <span>{errors.age.message}</span>}

<input {...register('password')} type="password" placeholder="Пароль" />
{errors.password && <span>{errors.password.message}</span>}

<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Отправка...' : 'Отправить'}
</button>
</form>
);
}

4. Debounced валидация:

function useDebouncedValidation(value, validate, delay = 300) {
const [error, setError] = useState('');
const [isValidating, setIsValidating] = useState(false);

useEffect(() => {
if (!value) {
setError('');
return;
}

setIsValidating(true);

const timer = setTimeout(() => {
const validationError = validate(value);
setError(validationError);
setIsValidating(false);
}, delay);

return () => clearTimeout(timer);
}, [value, validate, delay]);

return { error, isValidating };
}

function DebouncedInput({ validate, ...props }) {
const [value, setValue] = useState('');
const { error, isValidating } = useDebouncedValidation(value, validate);

return (
<div>
<input
value={value}
onChange={(e) => setValue(e.target.value)}
{...props}
/>
{isValidating && <span>Проверка...</span>}
{error && <span className="error">{error}</span>}
</div>
);
}

5. Ленивая валидация (только при blur и submit):

function LazyValidationForm() {
const [values, setValues] = useState({});
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});

const validate = (name, value) => {
switch (name) {
case 'email':
return !value.includes('@') ? 'Некорректный email' : '';
case 'password':
return value.length < 8 ? 'Минимум 8 символов' : '';
default:
return !value ? 'Поле обязательно' : '';
}
};

const handleChange = (name, value) => {
setValues((prev) => ({ ...prev, [name]: value }));

// Валидируем только если поле уже было тронуто
if (touched[name]) {
setErrors((prev) => ({ ...prev, [name]: validate(name, value) }));
}
};

const handleBlur = (name) => {
setTouched((prev) => ({ ...prev, [name]: true }));
setErrors((prev) => ({ ...prev, [name]: validate(name, values[name]) }));
};

const handleSubmit = (e) => {
e.preventDefault();

// Валидируем все поля
const newErrors = {};
Object.keys(values).forEach((name) => {
const error = validate(name, values[name]);
if (error) newErrors[name] = error;
});

setErrors(newErrors);
setTouched(Object.keys(values).reduce((acc, key) => {
acc[key] = true;
return acc;
}, {}));

if (Object.keys(newErrors).length === 0) {
console.log(values);
}
};

return (
<form onSubmit={handleSubmit}>
{['firstName', 'lastName', 'email', 'password'].map((name) => (
<div key={name}>
<input
value={values[name] || ''}
onChange={(e) => handleChange(name, e.target.value)}
onBlur={() => handleBlur(name)}
placeholder={name}
/>
{touched[name] && errors[name] && (
<span className="error">{errors[name]}</span>
)}
</div>
))}
<button type="submit">Отправить</button>
</form>
);
}

Сравнение подходов:

ПодходПлюсыМинусы
React Hook FormПроизводительность, мало кодаЗависимость
FormikПопулярный, гибкийМедленнее при многих полях
Кастомный хукПолный контрольБольше кода
Zod + RHFТипобезопасностьКривая обучения

Пример расширенного ответа:

«...Для форм с большим количеством инпутов рекомендую React Hook Form — он использует неуправляемые инпуты через refs, что минимизирует ре-рендеры. Для валидации подключаю Zod через zodResolver.

Если пишу кастомное решение — выношу каждый инпут в отдельный компонент с локальным состоянием. Форма получает значения только при сабмите. Также использую debounced валидацию для полей, требующих проверки на сервере (например, уникальность email).

Ключевые принципы: изолировать состояние инпутов, валидировать лениво (при blur/submit), использовать memo для предотвращения лишних ре-рендеров...»

Вопрос 20. Что такое рефы (refs) в React, зачем их используют и в чём их особенность?

Таймкод: 00:40:24

Ответ собеседника: Правильный. Рефы используются для доступа к реальной DOM-узлу (ноде), а не к JSX-элементу. Например, для подключения к инпуту или управления его состоянием напрямую. Также рефы можно использовать для хранения значений между перерендерами компонента (в отличие от state, значение в рефе не вызывает ре-рендер при изменении).

Правильный ответ:

Ответ отличный, покрывает ключевые аспекты. Давайте дополним его примерами.

Создание и использование рефов:

import { useRef, useEffect } from 'react';

function RefExample() {
const inputRef = useRef(null);

useEffect(() => {
// Фокус при монтировании
inputRef.current.focus();
}, []);

const handleClick = () => {
// Доступ к DOM-элементу
inputRef.current.select();
};

return (
<div>
<input ref={inputRef} type="text" />
<button onClick={handleClick}>Выделить</button>
</div>
);
}

Основные случаи использования:

1. Доступ к DOM-элементам:

function TextInputWithFocus() {
const inputRef = useRef(null);

const focusInput = () => {
inputRef.current.focus();
};

const selectText = () => {
inputRef.current.select();
};

const getInputValue = () => {
console.log(inputRef.current.value);
};

return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Фокус</button>
<button onClick={selectText}>Выделить</button>
<button onClick={getInputValue}>Получить значение</button>
</div>
);
}

2. Управление медиа:

function VideoPlayer() {
const videoRef = useRef(null);

const play = () => videoRef.current.play();
const pause = () => videoRef.current.pause();

return (
<div>
<video ref={videoRef} src="/video.mp4" />
<button onClick={play}>Play</button>
<button onClick={pause}>Pause</button>
</div>
);
}

3. Измерение элементов:

function MeasureElement() {
const divRef = useRef(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });

useEffect(() => {
const observer = new ResizeObserver((entries) => {
const { width, height } = entries[0].contentRect;
setDimensions({ width, height });
});

observer.observe(divRef.current);

return () => observer.disconnect();
}, []);

return (
<div ref={divRef}>
<p>Ширина: {dimensions.width}px</p>
<p>Высота: {dimensions.height}px</p>
</div>
);
}

4. Хранение значений без ре-рендера:

function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef(null);

const startTimer = () => {
intervalRef.current = setInterval(() => {
setCount((c) => c + 1);
}, 1000);
};

const stopTimer = () => {
clearInterval(intervalRef.current);
};

useEffect(() => {
return () => clearInterval(intervalRef.current);
}, []);

return (
<div>
<p>{count}</p>
<button onClick={startTimer}>Старт</button>
<button onClick={stopTimer}>Стоп</button>
</div>
);
}

5. Хранение предыдущего значения:

function usePrevious(value) {
const ref = useRef();

useEffect(() => {
ref.current = value;
}, [value]);

return ref.current;
}

function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);

return (
<div>
<p>Текущее: {count}</p>
<p>Предыдущее: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}

6. Интеграция с сторонними библиотеками:

function DatePickerIntegration() {
const inputRef = useRef(null);
const pickerRef = useRef(null);

useEffect(() => {
// Инициализация jQuery DatePicker
pickerRef.current = $(inputRef.current).datepicker({
dateFormat: 'yy-mm-dd',
onSelect: (date) => {
console.log('Выбрана дата:', date);
},
});

return () => {
$(inputRef.current).datepicker('destroy');
};
}, []);

return <input ref={inputRef} type="text" />;
}

7. ForwardRef — передача рефа дочернему компоненту:

import { forwardRef } from 'react';

const FancyInput = forwardRef(function FancyInput(props, ref) {
return (
<div className="fancy-input">
<input ref={ref} {...props} />
</div>
);
});

function Parent() {
const inputRef = useRef(null);

const focusInput = () => {
inputRef.current.focus();
};

return (
<div>
<FancyInput ref={inputRef} placeholder="Введите текст" />
<button onClick={focusInput}>Фокус</button>
</div>
);
}

Ref vs State:

ХарактеристикаuseRefuseState
Ре-рендер при измененииНетДа
Когда обновляетсяСинхронноАсинхронно
Для чегоХранение DOM-ссылок, мутабельных значенийUI-состояние
Начальное значениеuseRef(initialValue)useState(initialValue)

Паттерны с рефами:

// 1. Счётчик рендеров
function RenderCounter() {
const renderCount = useRef(0);
renderCount.current += 1;

return <p>Рендеров: {renderCount.current}</p>;
}

// 2. Флаг первого рендера
function useIsFirstRender() {
const isFirst = useRef(true);

useEffect(() => {
isFirst.current = false;
}, []);

return isFirst.current;
}

// 3. Хранение callback без пересоздания
function useEventCallback(callback) {
const callbackRef = useRef(callback);

useEffect(() => {
callbackRef.current = callback;
}, [callback]);

return useCallback((...args) => {
return callbackRef.current(...args);
}, []);
}

Пример расширенного ответа:

«...Refs предоставляют доступ к DOM-элементам напрямую. Использую их для: управления фокусом, измерения элементов, интеграции с не-React библиотеками, хранения мутабельных значений без ре-рендера.

Ключевое отличие от state — изменение ref не вызывает ре-рендер. Это полезно для хранения таймеров, предыдущих значений, флагов.

Для передачи рефа дочернему компоненту использую forwardRef. Для кастомных хуков с рефами — useCallback с ref вместо зависимости...»

Вопрос 21. Какие основные хуки React вы знаете и используете в работе? Для чего нужен forwardRef?

Таймкод: 00:42:21

Ответ собеседника: Правильный. Основные хуки: useState, useEffect, useRef, useCallback, useMemo. Также упомянут useLayoutEffect. forwardRef нужен для прокидывания рефа от родительского компонента к дочернему функциональному компоненту.

Правильный ответ:

Ответ хороший. Давайте расширим его, добавив примеры и дополнительные хуки.

Основные хуки React:

1. useState — управление состоянием:

function Counter() {
const [count, setCount] = useState(0);

// Ленивая инициализация
const [expensiveValue] = useState(() => {
return computeExpensiveValue();
});

// Функциональное обновление
const increment = () => setCount((prev) => prev + 1);

return <button onClick={increment}>{count}</button>;
}

2. useEffect — побочные эффекты:

function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);

// Загрузка данных
useEffect(() => {
let cancelled = false;

setLoading(true);
fetchUser(userId)
.then((data) => {
if (!cancelled) {
setUser(data);
setLoading(false);
}
});

return () => {
cancelled = true; // Очистка при размонтировании
};
}, [userId]);

// Подписка на события
useEffect(() => {
const handleResize = () => {
console.log(window.innerWidth);
};

window.addEventListener('resize', handleResize);

return () => {
window.removeEventListener('resize', handleResize);
};
}, []);

if (loading) return <Spinner />;
return <div>{user.name}</div>;
}

3. useLayoutEffect — синхронный эффект перед отрисовкой:

function Tooltip({ children, content }) {
const [tooltipHeight, setTooltipHeight] = useState(0);
const ref = useRef(null);

// Выполняется ДО отрисовки — нет мерцания
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setTooltipHeight(height);
}, []);

return (
<div ref={ref}>
{children}
<div style={{ top: tooltipHeight }}>{content}</div>
</div>
);
}

4. useCallback — мемоизация функций:

function Parent() {
const [count, setCount] = useState(0);

// Функция не пересоздаётся при каждом рендере
const handleClick = useCallback(() => {
console.log('Clicked');
}, []);

return (
<div>
<button onClick={() => setCount(count + 1)}>{count}</button>
<Child onClick={handleClick} />
</div>
);
}

const Child = memo(function Child({ onClick }) {
console.log('Child render');
return <button onClick={onClick}>Click me</button>;
});

5. useMemo — мемоизация значений:

function ExpensiveComponent({ items, filter }) {
// Вычисление кэшируется
const filteredItems = useMemo(() => {
console.log('Filtering...');
return items.filter(filter);
}, [items, filter]);

return (
<ul>
{filteredItems.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}

6. useRef — доступ к DOM и хранение значений:

function FocusInput() {
const inputRef = useRef(null);
const renderCount = useRef(0);

renderCount.current += 1;

useEffect(() => {
inputRef.current.focus();
}, []);

return (
<div>
<input ref={inputRef} />
<p>Рендеров: {renderCount.current}</p>
</div>
);
}

forwardRef — передача рефа:

// Без forwardRef — ошибка
function FancyButton(props) {
return <button className="fancy" {...props} />;
}

// С forwardRef
const FancyButton = forwardRef(function FancyButton(props, ref) {
return (
<button ref={ref} className="fancy" {...props}>
{props.children}
</button>
);
});

// Использование
function Parent() {
const buttonRef = useRef(null);

const focusButton = () => {
buttonRef.current.focus();
};

return (
<div>
<FancyButton ref={buttonRef}>Click me</FancyButton>
<button onClick={focusButton}>Focus</button>
</div>
);
}

Кастомные хуки:

// useLocalStorage
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});

useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);

return [value, setValue];
}

// useDebounce
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);

useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);

return () => clearTimeout(timer);
}, [value, delay]);

return debouncedValue;
}

// useFetch
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
const controller = new AbortController();

setLoading(true);
fetch(url, { signal: controller.signal })
.then((res) => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));

return () => controller.abort();
}, [url]);

return { data, loading, error };
}

Дополнительные хуки:

ХукНазначение
useReducerСложное состояние
useContextДоступ к контексту
useImperativeHandleКастомный API для ref
useIdУникальный ID (React 18)
useTransitionНесрочные обновления (React 18)
useDeferredValueОтложенное значение (React 18)

Пример расширенного ответа:

«...Основные хуки: useState для состояния, useEffect для побочных эффектов, useRef для доступа к DOM и хранения значений, useCallback и useMemo для оптимизации.

useLayoutEffect отличается от useEffect тем, что выполняется синхронно до отрисовки — полезно для измерений, где важно избежать мерцания.

forwardRef позволяет передать ref дочернему функциональному компоненту. Без него ref не будет работать, так как функциональные компоненты не имеют экземпляров. Использую для UI-библиотек компонентов, где родителю нужен доступ к DOM-узлу дочернего элемента...»

Вопрос 22. Расскажите об особенностях использования useEffect. Что он заменяет из жизненного цикла классовых компонентов?

Таймкод: 00:43:26

Ответ собеседника: Правильный. useEffect заменяет три жизненных цикла классовых компонентов: componentDidMount, componentDidUpdate и componentWillUnmount. Принимает два аргумента: тело функции и массив зависимостей. Если массив пустой — эффект срабатывает один раз при монтировании. Если указаны зависимости — эффект срабатывает при изменении этих зависимостей. Для очистки используется функция, возвращаемая из эффекта (return function), которая вызывается при размонтировании или перед повторным выполнением эффекта.

Правильный ответ:

Ответ отличный, покрывает все ключевые аспекты. Давайте дополним его примерами и деталями.

Соответствие жизненным циклам классов:

// Классовый компонент
class UserProfile extends React.Component {
componentDidMount() {
// Загрузка данных
this.fetchUser(this.props.userId);
// Подписка на события
window.addEventListener('resize', this.handleResize);
}

componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.fetchUser(this.props.userId);
}
}

componentWillUnmount() {
// Отписка от событий
window.removeEventListener('resize', this.handleResize);
// Отмена запроса
this.controller.abort();
}
}

// Функциональный компонент с useEffect
function UserProfile({ userId }) {
// componentDidMount
useEffect(() => {
console.log('Компонент смонтирован');
}, []);

// componentDidMount + componentDidUpdate (при изменении userId)
useEffect(() => {
console.log('userId изменился:', userId);
}, [userId]);

// componentDidMount + componentWillUnmount
useEffect(() => {
window.addEventListener('resize', handleResize);

return () => {
window.removeEventListener('resize', handleResize);
};
}, []);

// Все три жизненных цикла вместе
useEffect(() => {
const controller = new AbortController();

fetchUser(userId, controller.signal);

return () => {
controller.abort(); // componentWillUnmount
};
}, [userId]); // componentDidUpdate
}

Массив зависимостей — детали:

function Example({ userId, filter }) {
// 1. Без массива зависимостей — выполняется после КАЖДОГО рендера
useEffect(() => {
console.log('После каждого рендера');
});

// 2. Пустой массив — выполняется один раз при монтировании
useEffect(() => {
console.log('Только при монтировании');
}, []);

// 3. С зависимостями — при монтировании и при изменении зависимостей
useEffect(() => {
console.log('userId или filter изменился');
}, [userId, filter]);

// 4. Несколько useEffect для разных целей
useEffect(() => {
// Логика, связанная с userId
}, [userId]);

useEffect(() => {
// Логика, связанная с filter
}, [filter]);
}

Функция очистки (cleanup):

function ChatRoom({ roomId }) {
useEffect(() => {
// Настройка (setup)
const connection = createConnection(roomId);
connection.connect();

// Очистка (cleanup)
return () => {
connection.disconnect();
};
}, [roomId]);

// Порядок выполнения при изменении roomId:
// 1. Рендер с новым roomId
// 2. React выполняет cleanup предыдущего эффекта (disconnect)
// 3. React выполняет новый эффект (connect)
}

Практические примеры:

// 1. Подписка на WebSocket
function useWebSocket(url) {
const [messages, setMessages] = useState([]);

useEffect(() => {
const ws = new WebSocket(url);

ws.onmessage = (event) => {
setMessages((prev) => [...prev, JSON.parse(event.data)]);
};

ws.onerror = (error) => {
console.error('WebSocket error:', error);
};

return () => {
ws.close();
};
}, [url]);

return messages;
}

// 2. Таймер с очисткой
function Timer() {
const [seconds, setSeconds] = useState(0);

useEffect(() => {
const interval = setInterval(() => {
setSeconds((s) => s + 1);
}, 1000);

return () => clearInterval(interval);
}, []);

return <div>{seconds}</div>;
}

// 3. Загрузка данных с AbortController
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
const controller = new AbortController();

setLoading(true);

fetch(url, { signal: controller.signal })
.then((res) => {
if (!res.ok) throw new Error(res.statusText);
return res.json();
})
.then((data) => {
setData(data);
setLoading(false);
})
.catch((err) => {
if (err.name !== 'AbortError') {
setError(err);
setLoading(false);
}
});

return () => controller.abort();
}, [url]);

return { data, loading, error };
}

// 4. Слушатель событий
function useEventListener(eventName, handler, element = window) {
useEffect(() => {
element.addEventListener(eventName, handler);

return () => {
element.removeEventListener(eventName, handler);
};
}, [eventName, handler, element]);
}

// 5. Документ title
function useDocumentTitle(title) {
useEffect(() => {
document.title = title;

return () => {
document.title = 'Default Title';
};
}, [title]);
}

Частые ошибки:

// Ошибка: пропущена зависимость
function BadExample({ userId }) {
useEffect(() => {
fetchUser(userId); // userId не в зависимостях!
}, []); // Эффект не обновится при изменении userId
}

// Правильно
function GoodExample({ userId }) {
useEffect(() => {
fetchUser(userId);
}, [userId]);
}

// Ошибка: объект как зависимость (новый объект при каждом рендере)
function BadExample({ config }) {
useEffect(() => {
doSomething(config);
}, [config]); // Бесконечный цикл!
}

// Правильно: деструктуризация или useMemo
function GoodExample({ config }) {
const { url, method } = config;

useEffect(() => {
doSomething({ url, method });
}, [url, method]);
}

useLayoutEffect vs useEffect:

function Tooltip({ children }) {
const [height, setHeight] = useState(0);
const ref = useRef(null);

// useLayoutEffect — синхронно ДО отрисовки
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setHeight(height);
}, []);

return (
<div ref={ref}>
{children}
<div style={{ top: height }}>Tooltip</div>
</div>
);
}

Пример расширенного ответа:

«...useEffect объединяет componentDidMount, componentDidUpdate и componentWillUnmount. Массив зависимостей контролирует, когда эффект выполняется: пустой — только при монтировании, с зависимостями — при их изменении.

Функция очистки (cleanup) возвращается из эффекта и вызывается перед повторным выполнением эффекта или при размонтировании. Это важно для отписки от событий, отмены запросов, очистки таймеров.

Важно включать все используемые значения в массив зависимостей. Для объектов и функций использую useCallback и useMemo, чтобы избежать бесконечных циклов...»

Вопрос 23. Какие методы оптимизации веб-приложений вы используете на практике? Расскажите подробнее о code splitting.

Таймкод: 00:47:19

Ответ собеседника: Правильный. Для оптимизации React-приложений используются хуки мемоизации (useCallback, useMemo). Из инструментов — React DevTools Profiler для отслеживания рендеров и узких мест. Также можно откладывать загрузку скриптов, размещая их внизу страницы. Из Webpack — code splitting (разделение бандла на чанки), что ускоряет загрузку приложения.

Правильный ответ:

Ответ хороший, покрывает основные аспекты. Давайте расширим его детальными примерами.

Code Splitting в React:

1. React.lazy + Suspense:

import { lazy, Suspense } from 'react';

// Ленивый импорт компонента
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Profile = lazy(() => import('./pages/Profile'));

function App() {
return (
<Router>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</Suspense>
</Router>
);
}

2. Разбиение по роутам:

// routes.js
import { lazy } from 'react';

const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));

export const routes = [
{ path: '/', element: <Home /> },
{ path: '/about', element: <About /> },
{ path: '/contact', element: <Contact /> },
];

// App.js
function App() {
return (
<Suspense fallback={<PageLoader />}>
<Routes>
{routes.map((route) => (
<Route key={route.path} path={route.path} element={route.element} />
))}
</Routes>
</Suspense>
);
}

3. Разбиение по компонентам:

// Тяжёлый компонент загружается только при необходимости
const HeavyChart = lazy(() => import('./components/HeavyChart'));
const RichTextEditor = lazy(() => import('./components/RichTextEditor'));

function Dashboard() {
const [showChart, setShowChart] = useState(false);

return (
<div>
<h1>Dashboard</h1>
<button onClick={() => setShowChart(true)}>Показать график</button>

{showChart && (
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart />
</Suspense>
)}
</div>
);
}

Prefetch и Preload:

// Предзагрузка при наведении мыши
function NavLink({ to, children }) {
const prefetch = () => {
// Динамический импорт для предзагрузки
import(`./pages/${to}`);
};

return (
<Link to={to} onMouseEnter={prefetch}>
{children}
</Link>
);
}

// Или через webpack magic comments
const Dashboard = lazy(() => import(
/* webpackChunkName: "dashboard" */
/* webpackPrefetch: true */
'./pages/Dashboard'
));

Webpack — настройка code splitting:

// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
// Отдельный чанк для vendor-библиотек
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10,
},
// Общий чанк для часто используемого кода
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true,
},
},
},
runtimeChunk: 'single',
},
};

Другие методы оптимизации:

1. Мемоизация:

// React.memo для компонентов
const ExpensiveComponent = memo(function ExpensiveComponent({ data }) {
return <div>{/* Тяжёлые вычисления */}</div>;
});

// useMemo для значений
function FilteredList({ items, filter }) {
const filteredItems = useMemo(() => {
return items.filter(filter);
}, [items, filter]);

return <List items={filteredItems} />;
}

// useCallback для функций
function Parent() {
const [count, setCount] = useState(0);

const handleClick = useCallback(() => {
console.log('Clicked');
}, []);

return <Child onClick={handleClick} />;
}

2. Виртуализация списков:

import { FixedSizeList } from 'react-window';

function VirtualizedList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
);

return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</FixedSizeList>
);
}

3. Оптимизация изображений:

// Ленивая загрузка изображений
function LazyImage({ src, alt }) {
return (
<img
src={src}
alt={alt}
loading="lazy"
decoding="async"
/>
);
}

// Использование WebP с fallback
<picture>
<source srcSet="image.webp" type="image/webp" />
<source srcSet="image.jpg" type="image/jpeg" />
<img src="image.jpg" alt="Description" loading="lazy" />
</picture>

4. Web Workers для тяжёлых вычислений:

// worker.js
self.onmessage = function(e) {
const result = heavyComputation(e.data);
self.postMessage(result);
};

// useWorker.js
function useWorker(workerScript) {
const [result, setResult] = useState(null);
const workerRef = useRef(null);

useEffect(() => {
workerRef.current = new Worker(workerScript);

workerRef.current.onmessage = (e) => {
setResult(e.data);
};

return () => workerRef.current.terminate();
}, [workerScript]);

const postMessage = useCallback((data) => {
workerRef.current.postMessage(data);
}, []);

return { result, postMessage };
}

5. Service Worker для кэширования:

// service-worker.js
const CACHE_NAME = 'app-cache-v1';
const urlsToCache = ['/', '/styles.css', '/app.js'];

self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(urlsToCache))
);
});

self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});

Пример расширенного ответа:

«...Для оптимизации использую code splitting через React.lazy и Suspense — это позволяет загружать компоненты только когда они нужны. Разбиваю по роутам, чтобы каждая страница загружалась отдельно.

Для мемоизации — React.memo, useMemo, useCallback. Для длинных списков — виртуализация через react-window. Изображения — lazy loading и WebP.

В Webpack настраиваю splitChunks для выделения vendor-кода в отдельный чанк и runtimeChunk для кэширования. Использую webpackPrefetch для предзагрузки критических маршрутов...»

Вопрос 24. Расскажите подробнее об опыте оптимизации загрузки приложения с использованием lazy loading и prefetching.

Таймкод: 00:49:46

Ответ собеседника: Правильный. Был опыт рефакторинга раздела приложения, где загрузка занимала 25-30 секунд. Проблема была решена путём разбиения запроса: первый запрос загружался за 3 секунды и показывал пользователю начальные данные, а на втором экране подгружались остальные данные. Использовались prefetch и lazy loading с динамическими импортами (React.lazy).

Правильный ответ:

Ответ демонстрирует реальный опыт оптимизации. Давайте дополним его техническими деталями и инструментами.

Инструменты для анализа и оптимизации:

1. React DevTools Profiler:

// Обёртка для замера времени рендера
import { Profiler } from 'react';

function onRenderCallback(
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime
) {
console.log({
id, // Идентификатор компонента
phase, // 'mount' | 'update' | 'nested-update'
actualDuration, // Время рендера
baseDuration, // Время рендера без оптимизаций
});
}

function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<MainContent />
</Profiler>
);
}

2. Webpack Bundle Analyzer:

npm install --save-dev webpack-bundle-analyzer
// webpack.config.js
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: 'bundle-report.html',
openAnalyzer: false,
}),
],
};

3. Lighthouse (Chrome DevTools):

# Запуск через CLI
npm install -g lighthouse
lighthouse https://example.com --view

4. Performance API:

// Замер времени загрузки компонента
function usePerformanceMeasure(componentName) {
useEffect(() => {
const start = performance.now();

return () => {
const end = performance.now();
console.log(`${componentName}: ${end - start}ms`);
};
}, [componentName]);
}

// Или через PerformanceObserver
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(`${entry.name}: ${entry.duration}ms`);
}
});

observer.observe({ entryTypes: ['measure', 'navigation'] });

Практические техники оптимизации:

1. Прогрессивная загрузка данных:

function Dashboard() {
// Загружаем критические данные первыми
const { data: criticalData } = useQuery({
queryKey: ['critical'],
queryFn: fetchCriticalData,
});

// Некритические данные загружаем после
const { data: secondaryData } = useQuery({
queryKey: ['secondary'],
queryFn: fetchSecondaryData,
enabled: !!criticalData, // Только после загрузки критических
});

if (!criticalData) return <CriticalSkeleton />;

return (
<div>
<CriticalSection data={criticalData} />
{secondaryData ? (
<SecondarySection data={secondaryData} />
) : (
<SecondarySkeleton />
)}
</div>
);
}

2. Prefetch при наведении мыши:

function PrefetchLink({ to, children }) {
const prefetchRoute = useCallback(() => {
// Предзагрузка компонента
const component = import(`./pages/${to}`);

// Предзагрузка данных
queryClient.prefetchQuery({
queryKey: [to],
queryFn: () => fetchPageData(to),
});
}, [to]);

return (
<Link to={to} onMouseEnter={prefetchRoute}>
{children}
</Link>
);
}

3. Приоритизация ресурсов:

<!-- Preload критических ресурсов -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/critical.css" as="style">

<!-- Prefetch для следующих страниц -->
<link rel="prefetch" href="/dashboard.js">
<link rel="prefetch" href="/settings.js">

<!-- Preconnect к внешним доменам -->
<link rel="preconnect" href="https://api.example.com">
<link rel="dns-prefetch" href="https://cdn.example.com">

4. Оптимизация изображений:

function OptimizedImage({ src, alt, width, height }) {
const [loaded, setLoaded] = useState(false);

return (
<div style={{ position: 'relative', width, height }}>
{/* Placeholder во время загрузки */}
{!loaded && (
<div
style={{
position: 'absolute',
background: '#f0f0f0',
width: '100%',
height: '100%',
}}
/>
)}

<picture>
<source
srcSet={`${src}.webp`}
type="image/webp"
/>
<source
srcSet={`${src}.jpg`}
type="image/jpeg"
/>
<img
src={`${src}.jpg`}
alt={alt}
width={width}
height={height}
loading="lazy"
decoding="async"
onLoad={() => setLoaded(true)}
style={{
opacity: loaded ? 1 : 0,
transition: 'opacity 0.3s',
}}
/>
</picture>
</div>
);
}

5. Оптимизация шрифтов:

/* Font Display Swap — текст виден сразу */
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom.woff2') format('woff2');
font-display: swap;
unicode-range: U+0000-00FF; /* Только латиница */
}

/* Preload критических шрифтов */
<link
rel="preload"
href="/fonts/main.woff2"
as="font"
type="font/woff2"
crossorigin
>

6. Виртуализация для больших списков:

import { useVirtualizer } from '@tanstack/react-virtual';

function VirtualList({ items }) {
const parentRef = useRef(null);

const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
overscan: 5,
});

return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div
style={{
height: virtualizer.getTotalSize(),
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: virtualItem.size,
transform: `translateY(${virtualItem.start}px)`,
}}
>
{items[virtualItem.index].name}
</div>
))}
</div>
</div>
);
}

7. React Query для кэширования:

import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';

const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 минут
cacheTime: 10 * 60 * 1000, // 10 минут
refetchOnWindowFocus: false,
},
},
});

function DataComponent({ id }) {
const { data, isLoading } = useQuery({
queryKey: ['item', id],
queryFn: () => fetchItem(id),
placeholderData: () => {
// Используем данные из списка как placeholder
return queryClient
.getQueryData(['items'])
?.find((item) => item.id === id);
},
});

if (isLoading) return <Skeleton />;
return <div>{data.name}</div>;
}

Метрики производительности (Core Web Vitals):

МетрикаОписаниеЦель
LCPLargest Contentful Paint< 2.5s
FIDFirst Input Delay< 100ms
CLSCumulative Layout Shift< 0.1
TTFBTime to First Byte< 800ms
FCPFirst Contentful Paint< 1.8s

Пример расширенного ответа:

«...Для анализа использую React DevTools Profiler, Webpack Bundle Analyzer и Lighthouse. В одном проекте загрузка страницы занимала 30 секунд — решил проблему прогрессивной загрузкой: критические данные загружаются за 3 секунды, остальные — после.

Использую React.lazy с Suspense для code splitting по роутам. Prefetch при наведении мыши на ссылки. Для изображений — lazy loading, WebP с fallback, preload критических ресурсов.

Для больших списков — виртуализация через react-virtual. Для кэширования данных — React Query с staleTime и placeholderData. Шрифты — font-display: swap и preload...»

Вопрос 25. Какие Utility Types в TypeScript вы знаете и используете?

Таймкод: 00:51:43

Ответ собеседника: Правильный. Названы следующие Utility Types: Record, Partial, Required, Pick, Omit, Extract. Также упомянут редкий тип.

Правильный ответ:

Ответ хороший, но можно значительно расширить. Давайте рассмотрим все основные Utility Types с примерами.

Основные Utility Types:

1. Partial<T> — все свойства необязательные:

interface User {
id: number;
name: string;
email: string;
}

// Все свойства становятся необязательными
type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string; }

function updateUser(id: number, updates: Partial<User>) {
// Можно передать только часть свойств
}

updateUser(1, { name: 'John' }); // OK
updateUser(1, { email: 'john@example.com' }); // OK

2. Required<T> — все свойства обязательные:

interface Config {
apiUrl?: string;
timeout?: number;
}

// Все свойства становятся обязательными
type RequiredConfig = Required<Config>;
// { apiUrl: string; timeout: number; }

const config: RequiredConfig = {
apiUrl: 'https://api.example.com',
timeout: 5000,
};

3. Readonly<T> — все свойства только для чтения:

interface User {
id: number;
name: string;
}

type ReadonlyUser = Readonly<User>;

const user: ReadonlyUser = { id: 1, name: 'John' };
user.name = 'Jane'; // Error: Cannot assign to 'name' because it is a read-only property

4. Record<K, T> — объект с ключами K и значениями T:

// Объект с ключами-строками и значениями типа string
type StringMap = Record<string, string>;
const colors: StringMap = {
red: '#ff0000',
green: '#00ff00',
};

// Объект с конкретными ключами
type Role = 'admin' | 'user' | 'guest';
type RolePermissions = Record<Role, string[]>;

const permissions: RolePermissions = {
admin: ['read', 'write', 'delete'],
user: ['read', 'write'],
guest: ['read'],
};

5. Pick<T, K> — выбрать только указанные свойства:

interface User {
id: number;
name: string;
email: string;
password: string;
}

// Только id и name
type UserPreview = Pick<User, 'id' | 'name'>;
// { id: number; name: string; }

const preview: UserPreview = { id: 1, name: 'John' };

6. Omit<T, K> — исключить указанные свойства:

interface User {
id: number;
name: string;
email: string;
password: string;
}

// Все свойства кроме password
type PublicUser = Omit<User, 'password'>;
// { id: number; name: string; email: string; }

const publicUser: PublicUser = {
id: 1,
name: 'John',
email: 'john@example.com',
};

7. Extract<T, U> — извлечь типы, которые есть в обоих:

type A = 'a' | 'b' | 'c';
type B = 'b' | 'c' | 'd';

type Common = Extract<A, B>; // 'b' | 'c'

// Практический пример
type Status = 'pending' | 'success' | 'error' | 'loading';
type FinalStatus = Extract<Status, 'success' | 'error'>; // 'success' | 'error'

8. Exclude<T, U> — исключить типы, которые есть в U:

type A = 'a' | 'b' | 'c';
type B = 'b' | 'c';

type OnlyA = Exclude<A, B>; // 'a'

// Практический пример
type Status = 'pending' | 'success' | 'error' | 'loading';
type NonFinalStatus = Exclude<Status, 'success' | 'error'>; // 'pending' | 'loading'

9. NonNullable<T> — исключить null и undefined:

type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>; // string

function processString(value: NonNullable<MaybeString>) {
// value гарантированно не null и не undefined
return value.toUpperCase();
}

10. ReturnType<T> — тип возвращаемого значения функции:

function getUser() {
return { id: 1, name: 'John', email: 'john@example.com' };
}

type User = ReturnType<typeof getUser>;
// { id: number; name: string; email: string; }

// Практический пример
async function fetchUsers() {
const response = await fetch('/api/users');
return response.json();
}

type Users = Awaited<ReturnType<typeof fetchUsers>>;

11. Parameters<T> — типы параметров функции:

function createUser(name: string, age: number, email: string) {
// ...
}

type CreateUserParams = Parameters<typeof createUser>;
// [string, number, string]

// Практический пример
type UserParams = Parameters<typeof createUser>;
const params: UserParams = ['John', 30, 'john@example.com'];
createUser(...params);

12. Awaited<T> — извлечь тип из Promise:

type PromiseType = Promise<{ id: number; name: string }>;
type Resolved = Awaited<PromiseType>; // { id: number; name: string }

// Работает с вложенными Promise
type DeepPromise = Promise<Promise<string>>;
type DeepResolved = Awaited<DeepPromise>; // string

Дополнительные Utility Types:

// InstanceType — тип экземпляра класса
class User {
id: number;
name: string;
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
}

type UserInstance = InstanceType<typeof User>;
// User

// ThisType — тип this в объекте
interface Methods {
getValue(): number;
setValue(value: number): void;
}

interface State {
value: number;
}

const obj: State & ThisType<Methods> = {
value: 0,
getValue() {
return this.value; // this имеет тип Methods
},
setValue(value) {
this.value = value;
},
};

// OmitThisParameter — убрать параметр this
function toHex(this: Number) {
return this.toString(16);
}

const fiveToHex: OmitThisParameter<typeof toHex> = toHex.bind(5);
console.log(fiveToHex()); // '5'

// ThisParameterType — извлечь тип this
function toHex2(this: Number) {
return this.toString(16);
}

type ThisType = ThisParameterType<typeof toHex2>; // Number

Практические примеры комбинирования:

// API Response типы
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}

interface User {
id: number;
name: string;
email: string;
createdAt: Date;
}

// Тип для создания пользователя (без id и createdAt)
type CreateUserDto = Omit<User, 'id' | 'createdAt'>;

// Тип для обновления (все поля необязательные, кроме id)
type UpdateUserDto = Partial<Omit<User, 'id'>> & Pick<User, 'id'>;

// Публичный профиль (без email)
type PublicProfile = Omit<User, 'email'>;

// Словарь пользователей по ID
type UserMap = Record<number, User>;

// Только для чтения
type ReadonlyUserMap = Readonly<UserMap>;

Пример расширенного ответа:

«...Использую следующие Utility Types: Partial для обновления объектов, Required для конфигураций, Readonly для иммутабельности, Record для маппинга, Pick и Omit для выбора/исключения свойств, Extract и Exclude для работы с union типами, NonNullable для исключения null, ReturnType и Parameters для извлечения типов функций, Awaited для работы с Promise.

Часто комбинирую их: Omit<User, 'id'> & Pick<User, 'id'> для DTO, Record<Role, Permissions> для маппинга прав. Это делает типы более точными и переиспользуемыми...»