Frontend: карта трека и как устроена современная веб-разработка
Что такое фронтенд на самом деле
Определение «фронтенд — это то, что видит пользователь» описывает результат, а не работу. Точнее так: фронтенд — это инженерия клиентского приложения, которое доставляется по сети как исходный текст и исполняется в чужом рантайме, на чужом железе, при чужой сети, без права на установку и без гарантий по ресурсам. Каждая часть этого определения порождает целый класс задач.
- Доставляется по сети как исходный текст. У вас нет дистрибутива: каждый байт JS и CSS — это время на канале плюс время на парсинг. Отсюда бандлеры, code splitting, tree-shaking, кэш-заголовки, приоритеты загрузки.
- Исполняется в чужом рантайме. Вы не выбираете версию движка, вы пишете под контракт веб-стандартов. Отсюда прогрессивное улучшение, полифилы, browserslist, проверки вида
if ('X' in window). - На чужом железе. Тот же JS, который на ноутбуке разработчика парсится 40 мс, на бюджетном Android парсится 400 мс. Отсюда бюджеты производительности и throttling в DevTools.
- Без права на установку. Пользователь не ставит приложение, он открывает ссылку — первый заход всегда холодный старт. Отсюда SSR/SSG, стриминг HTML, сервис-воркеры и вся тема Core Web Vitals.
- Один поток на всё. Главный поток выполняет ваш JS, считает стили, делает layout, рисует и обрабатывает ввод. Долгая функция — это не «медленно», это зависший интерфейс: клики буквально не обрабатываются.
Отсюда главный тезис: фронтенд — это не «вёрстка плюс React», а работа с распределённой системой, клиентская половина которой заново запускается у каждого посетителя.
Исторически ответственность ходит маятником между сервером и клиентом: статический HTML и CGI (до 2005) → AJAX и jQuery → браузерные MVC → эпоха SPA (2015–2019, гигантские бандлы) → возврат к серверу (Next, Nuxt, SvelteKit, 2020+) → гранулярный гибрид сегодня (серверные компоненты, острова, сигналы). Отсюда три вывода. Ни одна модель не победила — побеждает умение выбрать модель под задачу. Инструменты меняются быстрее принципов: Webpack сменился Vite, Redux — Zustand, но «меньше кода на главном потоке» верно и в 2015-м, и в 2026-м. Сложность не исчезла, а переехала: раньше болела кроссбраузерность, теперь — сборка, кэширование и границы сервер/клиент.
Трек — про платформу, UI и экосистему. Язык разбирается отдельно в треке TypeScript, доступность интерфейсов — в треке про accessibility. Здесь мы их используем как данность.
Слои платформы: где живёт ваш код
Прежде чем говорить о фреймворках, надо понимать, на чём они стоят. Карта слоёв нужна, чтобы при любой проблеме («тормозит», «мигает», «не кликается») вы могли спросить себя: на каком слое это происходит?
Практическая ценность картинки — в диагностике. Симптом «подтормаживает при скролле» может жить на любом слое:
| Слой | Симптом | Инструмент диагностики |
|---|---|---|
| Код приложения | ререндер всего списка на каждый скролл | React DevTools Profiler |
| Рантайм фреймворка | долгая гидратация после загрузки | Performance panel, метка Hydrate |
| DOM и Web API | getBoundingClientRect в обработчике скролла |
Performance → Forced reflow |
| Движок браузера | тяжёлый box-shadow на анимируемом слое |
Rendering → Paint flashing |
| Сеть и ОС | картинки грузятся во время скролла | Network, throttling Slow 4G |
Ключевая асимметрия: чем ниже слой, тем дороже ошибка и тем меньше рычагов. Поэтому оптимизацию всегда начинают сверху — с вопроса «а нужен ли этот код вообще», и только потом лезут в микрооптимизации отрисовки. Подробно про парсинг, DOM, CSSOM, layout, paint и композитинг — в статье Как работает браузер.
Путь от URL до пикселя
Тот самый вопрос с собеседований — и он действительно полезен: почти любая проблема производительности есть точка на этой диаграмме.
- До первого байта уже потрачено время. DNS, TCP/QUIC, TLS — это 1–3 RTT; на мобильной сети RTT легко 100 мс, то есть 300 мс до начала любой полезной работы. Лечится HTTP/3,
preconnectи близким edge-узлом. - HTML — единственный ресурс, который парсится потоково. Если сервер отдаёт HTML стримом, браузер строит DOM с первых килобайт. Это фундамент стриминг-SSR.
- CSS блокирует отрисовку по определению: браузер не рисует, пока не знает финальные стили — отсюда критический CSS инлайном и
mediaна некритичных файлах. JS блокирует парсер, если у него нетdefer,asyncилиtype="module": классическая ошибка — синхронный<script>в<head>.
<!-- Разметка, которая правильно расставляет приоритеты загрузки -->
<link rel="preconnect" href="https://api.example.com" crossorigin>
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/critical.css">
<link rel="stylesheet" href="/rest.css" media="print" onload="this.media='all'">
<script type="module" src="/app.js"></script>
<img src="/hero.avif" width="1200" height="675" fetchpriority="high" alt=""><!-- LCP: без lazy -->
<img src="/card.avif" width="400" height="300" loading="lazy" decoding="async" alt=""><!-- ниже экрана -->
Стратегии рендеринга и гидратация
Главный источник путаницы во фронтенде. Есть два независимых вопроса, которые постоянно смешивают:
- Где формируется HTML? В браузере (CSR), на сервере по запросу (SSR), на этапе сборки (SSG) или заранее с фоновым обновлением (ISR).
- Что делает JS после того, как HTML показан? Ничего (статика), оживляет всё дерево (полная гидратация), оживляет отдельные куски (острова) или подгружает обработчики по требованию (resumability).
Что такое гидратация и почему она дорогая
Сервер прислал готовый HTML — пользователь его видит. Но HTML «мёртвый»: у кнопок нет обработчиков, у компонентов нет состояния. Гидратация — процесс, в котором клиентский фреймворк повторно выполняет код компонентов, строит своё внутреннее дерево и прикрепляет его к уже существующим DOM-узлам вместо создания новых. Три следствия, которые надо знать наизусть:
- Работа делается дважды: на сервере ради HTML и на клиенте ради дерева компонентов. Поэтому SSR ускоряет первую отрисовку, но не готовность к вводу.
- Весь код компонентов всё равно едет в браузер. Если страница на 90% статична, вы платите байтами и временем CPU за оживление того, что оживлять не нужно. Это аргумент за острова и серверные компоненты.
- Разметка сервера и клиента должна совпасть, иначе hydration mismatch. Причины:
Date.now(),Math.random(),windowв теле компонента, локаль и таймзона, расширения браузера.
// server.tsx — минимальный SSR без фреймворка, чтобы увидеть суть
import { renderToString } from 'react-dom/server';
export function handleRequest(url: string, data: unknown): string {
const html = renderToString(<App url={url} data={data} />);
// Состояние сериализуем в страницу, чтобы клиент не запрашивал его повторно
const state = JSON.stringify(data).replace(/</g, '\\u003c'); // защита от XSS
return `<!doctype html><html lang="ru"><head><link rel="stylesheet" href="/app.css"></head>
<body><div id="root">${html}</div><script>window.__DATA__ = ${state};</script>
<script type="module" src="/client.js"></script></body></html>`;
}
// client.tsx — гидратация: не createRoot, а hydrateRoot поверх готовой разметки
import { hydrateRoot } from 'react-dom/client';
hydrateRoot(document.getElementById('root')!, <App url={location.pathname} data={window.__DATA__} />);
// Как НЕ надо: значение отличается на сервере и на клиенте -> mismatch
const Bad = () => <span>{new Date().toLocaleTimeString()}</span>;
// Как надо: стабильный плейсхолдер на сервере, уточнение после гидратации
function Good() {
const [time, setTime] = useState<string | null>(null);
useEffect(() => setTime(new Date().toLocaleTimeString()), []);
return <span suppressHydrationWarning>{time ?? '--:--:--'}</span>;
}
Как выбрать стратегию
для всех пользователей?} B -->|Да| C{Часто меняется?} C -->|Редко| D[SSG
собрать при билде
отдать с CDN] C -->|Регулярно| E[ISR или ревалидация
кэш на edge с TTL] B -->|Нет| F{Нужен SEO или
быстрый первый экран?} F -->|Нет, это личный кабинет| G[CSR
обычный SPA] F -->|Да| H{Много интерактива?} H -->|Мало, это контент| I[SSR со стримингом
плюс острова] H -->|Много, это приложение| J[SSR с полной гидратацией
плюс split по роутам] D & E & G & I & J --> K[В любом случае: бюджет байтов
и измерение в поле]
| Стратегия | Сильная сторона | Чем платите |
|---|---|---|
| CSR (SPA) | простой деплой статики, богатый интерактив, дешёвая навигация после старта | плохой первый экран, SEO требует усилий, всё зависит от бандла |
| SSR | быстрый FCP и LCP, SEO из коробки, работает при слабом клиенте | нужен живой сервер, стоимость рендера, гидратация не ускоряется |
| SSG | предельная скорость и цена, тривиальный хостинг | пересборка при изменении контента, нет персонализации |
| ISR | компромисс SSG и SSR, свежесть без полной пересборки | сложная инвалидация, возможен показ устаревших данных |
| Острова и RSC | минимум JS в браузере, интерактив только там, где нужен | новая ментальная модель, граница сервер/клиент в коде, беднее экосистема |
Вглубь — Роутинг и стратегии рендеринга.
Почему тормозит и как это измерять
Самый частый способ «оптимизации» — обвесить компоненты useMemo без единого замера. Это культ карго. Начинать надо с модели: у вас есть три бюджета, и тормоза — всегда превышение одного из них. Первый — байты в сети (когда код доедет; меряются в КБ после brotli, а не в исходниках). Второй — миллисекунды главного потока (когда страница начнёт отвечать; парсинг и выполнение JS на слабом устройстве дороже, чем его скачивание). Третий — кадровый бюджет: 16.7 мс на кадр при 60 Гц, всё что дольше — пропущенный кадр и видимый рывок.
Core Web Vitals
Google свёл пользовательский опыт к трём числам, и это редкий случай, когда индустриальный стандарт оказался инженерно осмысленным (web.dev/vitals):
- LCP (Largest Contentful Paint) — когда отрисовался самый большой элемент первого экрана. Порог «хорошо»: ≤ 2.5 с. Упирается в TTFB, блокирующие ресурсы и вес hero-картинки.
- INP (Interaction to Next Paint) — с марта 2024 заменил FID. Меряет худшую задержку отклика за визит: от нажатия до следующей отрисовки. Порог: ≤ 200 мс. Ловит тормоза в середине сессии, а не только при старте.
- CLS (Cumulative Layout Shift) — сумма неожиданных сдвигов вёрстки. Порог: ≤ 0.1. Источники: картинки без
width/height, поздно загруженный шрифт, баннер, вставленный в поток.
Ключевая тонкость: пороги считаются по 75-му перцентилю реальных пользователей, а не по вашему ноутбуку. Отсюда деление на лабораторные данные (Lighthouse, WebPageTest, Performance panel — воспроизводимы, годятся для отладки и CI) и полевые (CrUX и собственный RUM — отражают реальность, но приходят с задержкой и не показывают причину). Правильный процесс: поле говорит, что чинить; лаборатория говорит, почему.
// src/rum.ts — реальный пользовательский мониторинг на пакете web-vitals
import { onLCP, onINP, onCLS, onTTFB, type Metric } from 'web-vitals';
function report(m: Metric): void {
const payload = JSON.stringify({
name: m.name, value: Math.round(m.value), rating: m.rating, // good | needs-improvement | poor
id: m.id, path: location.pathname, // id визита нужен для дедупликации
// Контекст обязателен: без него нельзя резать данные по сегментам
conn: (navigator as any).connection?.effectiveType ?? 'unknown',
dm: (navigator as any).deviceMemory ?? 0,
});
// sendBeacon переживает закрытие вкладки, обычный fetch — нет
if (navigator.sendBeacon) navigator.sendBeacon('/api/rum', payload);
else fetch('/api/rum', { method: 'POST', body: payload, keepalive: true });
}
onLCP(report); onINP(report); onCLS(report); onTTFB(report);
Дальше на бэкенде считаете 75-й перцентиль по каждому маршруту. Почти всегда выясняется, что «в среднем всё хорошо», а конкретный маршрут с тяжёлой таблицей даёт INP под 600 мс. Про построение продуктовых дашбордов — Метрики продукта.
Рецепты DevTools
# Lighthouse из CLI, чтобы прогон жил в CI, а не в ручных кликах
npx lighthouse https://example.com --preset=desktop --output=json --output-path=./lh.json
# Coverage (Ctrl+Shift+P -> Show Coverage): красное = байты, за которые заплатили зря
# Performance -> дорожка Main: всё длиннее 50 мс блокирует ввод и бьёт по INP
# Performance -> "Forced reflow": браузер сам подписывает синхронные пересчёты layout
# Rendering -> Layout Shift Regions и Paint flashing: что двигает вёрстку и что
# перерисовывается зря. Всегда включайте CPU throttling 4x-6x и Network Slow 4G
Классика: layout thrashing
// Плохо: чтение геометрии чередуется с записью стилей. Каждый offsetHeight
// заставляет браузер СИНХРОННО пересчитать layout: N элементов -> N пересчётов.
for (const el of items) el.style.height = el.offsetHeight + 10 + 'px';
// Хорошо: разделяем фазы — один пересчёт на весь батч вместо N
const heights = items.map((el) => el.offsetHeight); // фаза чтения
items.forEach((el, i) => { el.style.height = heights[i] + 10 + 'px'; }); // фаза записи
// Ещё лучше: не измерять вручную там, где есть наблюдатели платформы — их
// колбэк вызывается браузером в правильный момент кадра и не ломает конвейер
const ro = new ResizeObserver((es) => es.forEach((e) =>
e.target.classList.toggle('is-narrow', e.contentRect.width < 480)));
items.forEach((el) => ro.observe(el));
CSS, который убирает целые классы проблем
/* CLS: резервируем место под медиа до загрузки */
img, video { max-width: 100%; height: auto; }
.hero { aspect-ratio: 16 / 9; }
/* CLS от шрифта: показываем системный, подменяем без скачка метрик */
@font-face {
font-family: 'Inter'; src: url('/fonts/inter-var.woff2') format('woff2-variations');
font-display: swap; size-adjust: 105%; /* подгоняем метрики под фолбэк */
}
/* Длинные списки: браузер не считает layout для того, что за экраном */
.card-list > .card { content-visibility: auto; contain-intrinsic-size: auto 320px; }
/* Анимации: только transform и opacity — они не вызывают layout и paint */
.drawer { transition: transform 200ms ease-out; will-change: transform; }
.drawer[hidden] { transform: translateX(-100%); }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
Бюджет в CI
Метрика без порога — украшение дашборда. Порог должен ломать сборку. Файл .size-limit.json описывает лимиты по чанкам, и npx size-limit возвращает ненулевой код, если бандл вырос:
[{ "name": "entry", "path": "dist/assets/index-*.js", "limit": "45 kB", "gzip": true },
{ "name": "vendor react", "path": "dist/assets/react-*.js", "limit": "48 kB", "gzip": true }]
# .github/workflows/perf.yml — шаги, которые роняют pull request при превышении
- run: npm ci && npm run build
- run: npx size-limit # порог по байтам
- run: npx @lhci/cli autorun # пороги по LCP, CLS и TBT из lighthouserc.json
Про пайплайны — DevOps: основы CI; полный разбор метрик и ленивой загрузки — Производительность фронтенда.
React и альтернативы: честно, без фанатизма
Все современные фреймворки решают одну задачу — связать состояние с DOM так, чтобы не писать императивные обновления руками. Различаются они ответом на вопрос «как узнать, что именно изменилось»:
- Реконсиляция виртуального дерева (React). При изменении состояния функция компонента выполняется заново, результат сравнивается с предыдущим деревом, в DOM применяется разница. Просто в модели, дорого в рантайме, требует ручных подсказок (
memo, ключи, стабильные колбэки). - Мелкозернистая реактивность (Solid, Vue 3, Svelte 5, Angular signals). Зависимости между состоянием и участками DOM фиксируются при первом выполнении, обновление точечное, компонент повторно не выполняется. Быстрее по умолчанию, но требует привыкания к тому, «когда именно читается сигнал».
- Компиляция (Svelte, отчасти Vue SFC). Компилятор превращает шаблон в прямые инструкции обновления DOM. Минимальный рантайм, но магия происходит на этапе сборки, и отладка идёт по сгенерированному коду.
| Фреймворк | Модель | Вес рантайма (порядок, gzip) | Где выигрывает | Где проигрывает |
|---|---|---|---|---|
| React | VDOM и реконсиляция | ~45 КБ | экосистема, найм, React Native, серверные компоненты | производительность по умолчанию, ручная мемоизация, много способов сделать одно и то же |
| Vue | реактивность и компиляция шаблона | ~35 КБ | плавная кривая обучения, отличная документация, SFC | меньше вакансий вне отдельных рынков, две эпохи API в статьях |
| Svelte | компиляция в прямые обновления | ~5–15 КБ | минимум кода и байтов, приятный DX, переходы из коробки | тоньше экосистема, сложнее найм, магия компилятора |
| Angular | DI, компилятор, сигналы | ~90 КБ и выше | крупные команды, всё из коробки, строгая структура | высокий порог входа, многословность, тяжёлый старт |
| Solid | сигналы без VDOM | ~7 КБ | эталонная скорость, JSX без ререндеров | маленькое комьюнити, ловушки деструктуризации пропсов |
| Astro | острова поверх любого UI | ~0 КБ на статике | контентные сайты, документация, лендинги | не для приложений с плотным интерактивом |
Как выбирать без религии: нужен найм и предсказуемая поставка — React (не потому, что лучший технически, а потому, что вокруг больше всего готовых решений и людей); маленькая команда с упором на скорость — Svelte или Solid, но проверьте, что нужные библиотеки существуют; крупный энтерпрайз с долгим горизонтом — Angular даёт единообразие, которое в больших организациях ценнее гибкости; контентный сайт — Astro или обычный SSG, тащить SPA под блог это инженерная ошибка, а не «современный стек»; легаси на jQuery — не переписывайте всё сразу, Vue и Preact подключаются точечно.
И главное: разница между фреймворками почти всегда меньше, чем разница между хорошей и плохой архитектурой на одном и том же фреймворке. Медленный сайт на Solid делается легко; быстрый на React — тоже. Развёрнутое сравнение с бенчмарками — Сравнение фреймворков.
Первый рабочий проект
Минимальный современный стек: Vite + React + TypeScript. Всё копируется и запускается.
npm create vite@latest my-app -- --template react-ts
cd my-app && npm install && npm run dev
npm run build && npm run preview # продакшн-сборка и проверка результата локально
npx vite-bundle-visualizer # что реально попало в бандл
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
target: 'es2022', // современные браузеры: меньше транспиляции, меньше байтов
sourcemap: true, // без карт исходников продакшн-ошибки бесполезны
// Отделяем редко меняющиеся зависимости: их кэш переживёт наши релизы
rollupOptions: { output: { manualChunks: { react: ['react', 'react-dom'] } } },
},
server: { port: 5173, open: true },
});
// src/App.tsx — маленький, но полноценный компонент: состояние, форма, список
import { useState, useId, type FormEvent } from 'react';
import './app.css';
type Task = { id: number; title: string; done: boolean };
export function App() {
const [tasks, setTasks] = useState<Task[]>([]);
const [draft, setDraft] = useState('');
const inputId = useId(); // стабильный id, безопасный при SSR
function addTask(e: FormEvent) {
e.preventDefault();
const title = draft.trim();
if (!title) return;
// Иммутабельное обновление: React сравнивает состояние по ссылке
setTasks((prev) => [...prev, { id: Date.now(), title, done: false }]);
setDraft('');
}
const toggle = (id: number) =>
setTasks((prev) => prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
return (
<main className="app">
<h1>Задачи</h1>
<form onSubmit={addTask} className="row">
<label htmlFor={inputId} className="visually-hidden">Новая задача</label>
<input id={inputId} value={draft} placeholder="Что нужно сделать?"
onChange={(e) => setDraft(e.target.value)} />
<button type="submit">Добавить</button>
</form>
<ul className="list">
{/* key — не украшение: по нему React сопоставляет элементы между рендерами.
Индекс массива как key ломает состояние при вставках и сортировке. */}
{tasks.map((task) => (
<li key={task.id}>
<label>
<input type="checkbox" checked={task.done} onChange={() => toggle(task.id)} />
<span className={task.done ? 'done' : undefined}>{task.title}</span>
</label>
</li>
))}
</ul>
<p aria-live="polite">Осталось: {tasks.filter((t) => !t.done).length}</p>
</main>
);
}
/* src/app.css — современный CSS без препроцессора */
:root {
color-scheme: light dark; /* тёмная тема силами браузера */
--bg: light-dark(#ffffff, #16181d); --fg: light-dark(#1b1f24, #e6e8eb);
--accent: light-dark(#2f6fb0, #6aa6e0); --space: 0.75rem;
}
body { margin: 0; background: var(--bg); color: var(--fg); font: 16px/1.5 system-ui, sans-serif; }
.app { max-width: 40rem; margin-inline: auto; padding: calc(var(--space) * 2); }
.row { display: flex; gap: var(--space); }
.row input { flex: 1; padding: var(--space); }
.list { display: grid; gap: calc(var(--space) / 2); padding: 0; list-style: none; }
.done { text-decoration: line-through; opacity: 0.6; }
button { padding: var(--space) calc(var(--space) * 1.5); background: var(--accent); color: #fff; border: 0; border-radius: 6px; }
button:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
.visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden;
clip-path: inset(50%); white-space: nowrap; } /* только для скринридеров */
В этой сотне строк уже сидит половина трека: семантика и метки формы, кастомные свойства и тема, иммутабельное состояние, key и реконсиляция, конфиг сборки с разделением чанков. Дальше разбираем каждый пункт вглубь.
Карта трека
Трек читается по порядку: каждая статья опирается на предыдущие. Блоки: фундамент платформы (1–6), инструменты (7), React (8–10), данные и ввод (11–12), продакшн (13–17).
| # | Статья | О чём |
|---|---|---|
| 1 | Как работает браузер | парсинг, DOM, CSSOM, layout, paint, композитинг, главный поток |
| 2 | Семантический HTML | структура, формы, метаданные и почему div вместо button дорого обходится |
| 3 | CSS с нуля | каскад, специфичность, наследование, единицы измерения |
| 4 | Вёрстка | flexbox, grid, позиционирование, адаптивность, контейнерные запросы |
| 5 | Архитектура стилей | БЭМ, CSS-modules, CSS-in-JS, Tailwind, дизайн-системы и их цена |
| 6 | DOM и события | дерево, делегирование, всплытие, IntersectionObserver и MutationObserver |
| 7 | Сборка фронтенда | Vite, бандлеры, tree-shaking, code splitting, dev-server и HMR |
| 8 | React: основы | компоненты, JSX, состояние, жизненный цикл, реконсиляция, ключи |
| 9 | Хуки и паттерны | useState, useEffect, мемоизация, кастомные хуки, композиция |
| 10 | Управление состоянием | локальное состояние, контекст, Redux, Zustand, сигналы |
| 11 | Работа с данными | fetch, React Query, кэширование, оптимистичные обновления, гонки запросов |
| 12 | Формы и валидация | контролируемые поля, схемы, UX ошибок |
| 13 | Роутинг и рендеринг | SPA, SSR, SSG, ISR, гидратация вглубь |
| 14 | Производительность | Core Web Vitals, бюджеты, ленивая загрузка, профилирование |
| 15 | Тестирование | юнит, компонентные, E2E, визуальная регрессия |
| 16 | Сравнение фреймворков | React, Vue, Svelte, Angular, Solid — что и когда выбирать |
| 17 | Архитектура фронтенда | слои, монорепо, микрофронтенды, деплой и мониторинг |
Смежное: тестирование как дисциплина, архитектурные паттерны и как работает веб на уровне протоколов.
Маршруты обучения
Две другие траектории. Бэкендер, которому нужен фронтенд: статьи 1–2 бегло, но обязательно 3–5 (CSS чаще всего недооценивают), затем 7–11 и 13; ваша сильная сторона — данные и архитектура, слабая — каскад и вёрстка. Верстальщик, растущий в разработчика: наоборот, внимательно 1, 6, 7, быстро 3–5, дальше плотно 8–12; слабая сторона — состояние и асинхронность.
Типичные ошибки, которые стоят дорого
- Начинать с React, не разобравшись в браузере: тогда любая проблема выглядит как «баг React», хотя это layout, кэш или порядок загрузки.
divвместо семантики: кнопка изdivтеряет фокус, клавиатуру, озвучку и:disabled— всё то, что платформа даёт бесплатно.- Оптимизация без замера:
useMemoна каждый чих добавляет работу и мешает читать код — сначала профайлер, потом изменения. И наоборот, один гигантский бандл: отсутствие code splitting по роутам — самая частая причина плохого LCP на реальных проектах. - Игнорирование состояний загрузки и ошибки: экран, где данные «просто появляются», в реальной сети выглядит сломанным.
- Клиентская валидация вместо серверной: всё, что в браузере, — это UX, а не безопасность; данные с клиента недоверенные всегда.
- Разработка только на быстром ноутбуке и Wi-Fi: включайте throttling и хотя бы иногда открывайте продукт на дешёвом телефоне.
- Копирование архитектуры больших компаний: микрофронтенды в команде из четырёх человек — оверинжиниринг, см. Связность и зацепление.
Чеклист: трек освоен, если вы можете
- Объяснить на схеме путь от ввода URL до первого пикселя, назвать, что блокирует отрисовку, и сказать, чем SSR отличается от SSG и что именно делает гидратация.
- Сверстать адаптивный интерфейс на grid и flexbox без фреймворка и найти причину плохого INP в Performance-панели.
- Написать компонент с формой, валидацией, состояниями загрузки и ошибки, а к нему компонентный тест и E2E-сценарий.
- Обосновать выбор React/Vue/Svelte для конкретного проекта в трёх пунктах, где хотя бы один — против вашего выбора.
- Настроить сборку с разделением чанков и бюджетом байтов в CI.
Источники
- MDN Web Docs — эталонная документация по HTML, CSS, JS и Web API.
- Core Web Vitals и Interaction to Next Paint — определения метрик и пороги.
- Rendering on the Web — каноническая статья о CSR, SSR и SSG.
- Chrome DevTools: Performance — как читать флеймграф и искать длинные задачи; Chrome UX Report — полевые данные по реальным пользователям.
- Web Almanac — ежегодный срез состояния веба по миллионам сайтов, лучший источник реальных цифр.
- React, Vue, Svelte, Solid, Angular, Vite — первоисточники вместо пересказов.
- patterns.dev — паттерны рендеринга и производительности с примерами; High Performance Browser Networking Ильи Григорика — бесплатная книга про сеть, без которой TTFB остаётся магией.
Мини-итог
Фронтенд — это про доставку и исполнение кода в среде, которой вы не управляете. Три опоры трека: платформа (браузер, DOM, CSS, главный поток), модель обновления UI (реконсиляция или реактивность) и измеримое качество (Core Web Vitals в поле, а не ощущения в лаборатории). Фреймворки над этими опорами меняются каждые несколько лет, сами опоры — нет. Начнём с нижней.
Что дальше
Как работает браузер: парсинг, DOM, CSSOM, рендер, композитинг — разбираем движок изнутри: как из байтов получается дерево, почему CSS блокирует отрисовку, что происходит на главном потоке и как браузер собирает финальный кадр.