Связывайте ответ с браузером, доступностью, производительностью и пользовательским состоянием — именно там видна зрелость frontend-инженера.
Вопросы и ответы
10 подробных ответов
01Что делает бандлер и чем Vite отличается от webpack? Почему Vite быстрый в dev-режиме?
middle
Короткий ответ: Бандлер разрешает граф зависимостей и собирает модули (JS, CSS, ассеты) в оптимизированные файлы для браузера. webpack бандлит весь граф и в dev, и в prod; Vite в dev не бандлит — раздаёт исходники через нативный ESM-сервер, транспилируя каждый модуль по запросу на esbuild, а для прода собирает бандл через Rollup.
Подробно:
- Зачем бандлер — браузеру нужно мало запросов и валидный код: бандлер склеивает модули, прогоняет CSS/ассеты через лоадеры, делает code splitting и минификацию.
- Почему Vite быстр в dev — нет предварительной сборки всего проекта: dev-сервер раздаёт ESM, браузер сам запрашивает нужные модули, а esbuild (на Go) транспилирует в разы быстрее Babel.
- HMR — Vite инвалидирует только изменённый модуль, поэтому горячая замена почти мгновенна и не зависит от размера проекта.
| webpack | Vite | |
|---|---|---|
| Dev | бандлит весь граф | нативный ESM, без бандла |
| Транспиляция | Babel / ts-loader | esbuild |
| Prod-сборка | webpack | Rollup |
| Старт dev-сервера | растёт с проектом | почти постоянный |
⚠️ Частая ошибка: считать, что Vite вообще не бандлит. В проде он собирает бандл через Rollup — без бандлинга прод страдал бы от водопада сетевых запросов.
02Что такое транспиляция? Зачем нужны Babel/SWC/tsc и как таргетировать конкретные браузеры?
middle
Короткий ответ: Транспиляция — это перевод современного JS/TS в синтаксис, понятный целевым браузерам. Babel и SWC понижают синтаксис, tsc дополнительно проверяет типы; набор целевых браузеров задаётся через browserslist, а недостающие API добавляются полифилами (core-js).
Подробно:
- Зачем транспилировать — новый синтаксис (optional chaining, классы) и TypeScript не исполняются в старых движках; транспилятор понижает синтаксис до поддерживаемого.
- Синтаксис ≠ API — транспиляция меняет только синтаксис; новые API (
Promise,fetch,Array.flat) нужно добавлять полифилами, иначе в старом браузере будетReferenceError. - Babel vs SWC vs tsc — Babel гибок (плагины и пресеты), SWC написан на Rust и кратно быстрее, tsc медленнее, но единственный проверяет типы.
{
"browserslist": [
">0.5%",
"last 2 versions",
"not dead"
]
}
⚠️ Частая ошибка: думать, что транспиляция сама добавит полифилы. Понижение синтаксиса и полифилы API — это разные шаги; без core-js/таргета в browserslist код упадёт на отсутствующем методе.
03Какие оптимизации делает прод-сборка? Объясните tree-shaking, code splitting, минификацию и content-hash.
middle
Короткий ответ: Прод-сборка удаляет мёртвый код (tree-shaking), бьёт бандл на части (code splitting), сжимает код (минификация) и подставляет хеш контента в имена файлов для долгого кеширования. Tree-shaking работает только с ESM и требует честно проставленного sideEffects.
Подробно:
- Tree-shaking — статический анализ
import/exportвыкидывает неиспользуемые экспорты; нужен ESM (не CJS) и полеsideEffects, чтобы бандлер знал, какие модули безопасно удалять. - Code splitting — динамический
import()выносит код в отдельные чанки, грузящиеся по требованию (роуты, тяжёлые виджеты). - Минификация — Terser/esbuild убирают пробелы, сокращают имена, удаляют недостижимый код.
- Content-hash —
app.[hash].js: при неизменном содержимом имя стабильно, поэтому можно ставитьCache-Control: immutable, а при изменении хеш меняется и кеш сбрасывается.
{
"name": "ui-kit",
"type": "module",
"sideEffects": ["*.css"],
"exports": { ".": "./dist/index.mjs" }
}
⚠️ Частая ошибка: ставить "sideEffects": false при наличии импортов с побочными эффектами (CSS, полифилы, регистрация) — бандлер вырежет их, и в проде пропадут стили или сломается инициализация.
04Чем отличаются dependencies, devDependencies и peerDependencies? Что значат semver-диапазоны и зачем lock-файл?
middle
Короткий ответ: dependencies нужны в рантайме, devDependencies — только для сборки и тестов, peerDependencies — то, что предоставляет хост-приложение (типично для библиотек, например React). Semver-диапазон (^, ~) задаёт допустимые версии, а lock-файл фиксирует точное дерево для воспроизводимости.
Подробно:
- Где что лежит —
dependenciesедут в прод-бандл;devDependencies(vite, vitest, eslint, типы) не нужны в рантайме;peerDependenciesнельзя дублировать — библиотека требует, но не ставит React сама. - Semver —
^1.4.0разрешает minor+patch (<2.0.0),~1.4.0— только patch (<1.5.0), точная1.4.0— без обновлений. - Lock-файл —
package-lock.json/pnpm-lock.yamlпинит конкретные версии всего дерева; коммитится в репозиторий, иначе у CI и коллег будут разные сборки.
{
"dependencies": { "react": "^18.3.0" },
"devDependencies": { "vite": "~5.4.0", "vitest": "^2.0.0" },
"peerDependencies": { "react": ">=18" }
}
pnpm экономит диск через симлинки из общего store; npm и yarn копируют пакеты в каждый node_modules.
⚠️ Частая ошибка: класть @types/* или сборочные пакеты в dependencies, или не коммитить lock-файл — тогда «работает у меня» расходится с CI.
05ESLint и Prettier — зачем нужны оба и как форсить их в CI и pre-commit?
middle
Короткий ответ: ESLint ловит ошибки и проблемные паттерны (корректность кода), Prettier приводит код к единому форматированию (стиль). Это разные задачи, поэтому держат оба; чтобы не конфликтовали, форматирование целиком отдают Prettier. Форсят их в CI и через pre-commit-хук (husky + lint-staged).
Подробно:
- ESLint = корректность — находит
no-unused-vars,react-hooks/exhaustive-deps, потенциальные баги; часть правил чинится--fix. - Prettier = форматирование — отступы, кавычки, переносы; убирает споры о стиле в ревью, форматируя детерминированно.
- Связка —
eslint-config-prettierотключает стилистические правила ESLint, чтобы они не воевали с Prettier. - Enforcement — pre-commit (lint-staged прогоняет только staged-файлы) + жёсткая проверка в CI с
--max-warnings=0иprettier --check.
| ESLint | Prettier | |
|---|---|---|
| Цель | корректность, баги | форматирование |
| Пример | неиспользуемая переменная | отступы, кавычки |
| Конфигурируемость | много правил | почти нет опций |
| В CI | --max-warnings=0 |
--check |
⚠️ Частая ошибка: заставлять ESLint форматировать код (стилистические правила) — он конфликтует с Prettier; правила форматирования нужно отключить через eslint-config-prettier.
06Что такое пирамида тестирования во фронтенде? Чем различаются unit-, integration- и e2e-тесты?
middle
Короткий ответ: Пирамида тестирования — это рекомендация по балансу: много дешёвых и быстрых юнит-тестов в основании, меньше интеграционных в середине и совсем немного медленных дорогих e2e на вершине. Чем выше, тем больше уверенность, но тем дороже и медленнее тест.
Подробно:
- Unit — изолированная функция/хук/компонент без сети; миллисекунды, запускаются на каждое сохранение.
- Integration — несколько модулей вместе (компонент + стор + замоканный API); главный слой во фронтенде, ловит реальные баги взаимодействия.
- E2E — реальный браузер проходит сценарий целиком; максимальная уверенность, но медленно и более хрупко.
/\
/e2e\ мало · медленно · дорого
/------\
/ интегра-\ среднее число
/ ционные \
/-------------\
/ юнит \ много · быстро · дёшево
/-----------------\
⚠️ Частая ошибка: «перевёрнутая пирамида» (ice-cream cone) — куча медленных e2e и мало юнит-тестов; набор становится медленным и flaky, и ему перестают доверять.
07Как тестировать компоненты с Vitest/Jest + React Testing Library? Что значит «тестировать поведение, а не реализацию»?
middle
Короткий ответ: RTL рендерит компонент и проверяет то, что видит и делает пользователь, а не внутренности (state, имена методов). Элементы ищут по доступной роли (getByRole), взаимодействие имитируют через user-event, а затем проверяют видимый результат.
Подробно:
- Поведение, а не реализация — не лезьте в
state/инстанс; проверяйте, что на экране после действия. Тогда рефактор внутренностей не ломает тест. - Запросы по роли —
getByRole('button', { name: ... })отражает доступность;getByTestId— крайняя мера. user-eventвместоfireEvent— он точнее моделирует реальные действия (фокус, ввод, клавиатуру).- Async — после клика используйте
findBy*/await, чтобы дождаться обновления.
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Counter } from './Counter'
test('увеличивает счётчик по клику', async () => {
const user = userEvent.setup()
render(<Counter />)
await user.click(screen.getByRole('button', { name: /добавить/i }))
expect(screen.getByText('Счёт: 1')).toBeInTheDocument()
})
⚠️ Частая ошибка: проверять внутреннее состояние или имена приватных методов вместо видимого результата — тест ломается при рефакторинге, хотя поведение не изменилось.
08Когда оправданы e2e-тесты и как бороться с flaky-тестами в Playwright/Cypress?
senior
Короткий ответ: E2E проверяют реальный пользовательский сценарий в браузере целиком, поэтому они дороги и медленны — держите их только для критичных путей (логин, оплата). Flaky уменьшают авто-ожиданиями Playwright (web-first assertions), изоляцией тестов и моками сети, а не ручными паузами.
Подробно:
- Когда оправданы — сквозные критичные сценарии, которые юнит/интеграция не покрывают; не дублируйте в e2e то, что дешевле проверить ниже.
- Авто-ожидание —
expect(locator).toBeVisible()ретраится до таймаута; не нужныsleep. - Изоляция — каждый тест со своим состоянием (свежий контекст, сид данных), чтобы порядок не влиял.
- Мок сети —
page.routeстабилизирует ответы и убирает зависимость от бэкенда.
import { test, expect } from '@playwright/test'
test('пользователь логинится', async ({ page }) => {
await page.route('**/api/me', r => r.fulfill({ json: { name: 'Аня' } }))
await page.goto('/login')
await page.getByLabel('Email').fill('a@b.com')
await page.getByRole('button', { name: 'Войти' }).click()
// авто-ожидание: ретраит, пока не появится
await expect(page.getByText('Привет, Аня')).toBeVisible()
})
⚠️ Частая ошибка: page.waitForTimeout(...) — фиксированные паузы главный источник flaky; используйте web-first assertions, которые сами ретраятся.
09Как выглядит CI/CD-пайплайн для фронтенда и зачем нужны preview-окружения?
middle
Короткий ответ: Типичный пайплайн на каждый PR прогоняет шаги по порядку: установка зависимостей → линт → проверка типов → тесты → сборка → деплой preview. Падение любого шага блокирует merge, а preview-окружение разворачивает ветку на уникальном URL, чтобы ревьюер кликал живую сборку, а не читал диф.
Подробно:
- Порядок и fail-fast — дешёвые проверки (линт, типы) раньше дорогих (e2e), чтобы быстро падать.
- Воспроизводимость — ставьте из lock-файла (
pnpm install --frozen-lockfile) и кешируйтеnode_modules/store, чтобы CI был быстрым и детерминированным. - Preview-деплой — каждый PR получает свой URL (Vercel/Netlify/свой стенд); QA и дизайн смотрят реальное поведение до merge.
- CD — после merge в main та же сборка едет в прод.
push / PR
│
▼
[install] → [lint] → [typecheck] → [test] → [build] → [preview deploy]
pnpm i eslint tsc vitest vite уникальный URL
⚠️ Частая ошибка: гонять CI без зафиксированного lock-файла или без кеша — сборки невоспроизводимы и медленны; всегда ставьте через --frozen-lockfile.
10Чем отличаются ESM, CJS и UMD, как они резолвятся в браузере и Node, и как правильно публиковать библиотеку?
senior
Короткий ответ: ESM (import/export) — статичный стандарт, который умеет tree-shaking; CJS (require/module.exports) — исторический формат Node; UMD работает и в браузере (<script>), и в Node. Современную библиотеку публикуют с полем exports, отдавая ESM и CJS варианты по условиям резолвинга.
Подробно:
- ESM — статические импорты, асинхронная загрузка, нативен в браузере и Node; основа tree-shaking.
- CJS — синхронный
require, динамичен, плохо шейкается; до сих пор в экосистеме Node. - UMD — обёртка-универсал, цепляется к
window; нужен для подключения через<script>без бандлера. - Публикация — поле
exportsс условиямиimport/require/types; так потребитель на ESM и на CJS получит правильный файл.
| ESM | CJS | UMD | |
|---|---|---|---|
| Синтаксис | import/export |
require |
обёртка |
| Среда | браузер + Node | Node | браузер + Node |
| Tree-shaking | да | нет | нет |
| Загрузка | статич./async | синхронная | глобал |
{
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
}
}
⚠️ Частая ошибка: указать только main/module без exports или перепутать условия import/require — потребитель на CJS получит ESM-файл и упадёт с ERR_REQUIRE_ESM.
Источники
Источники и редакционная политика
Материалы RecallDeck сопоставлены с официальной документацией и открытыми публикациями компаний, когда первичный источник доступен. Мы не связаны с упомянутыми работодателями, не публикуем конфиденциальные задания и не продаём места в подборках. Формат найма может меняться — уточняйте его у рекрутера.
От чтения к воспроизведению
Отрепетируйте полный цикл интервью.
RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.