Frontend-разработка Frontend: карта трека и как устроена современная веб-разработка
0%

Frontend: карта трека и как устроена современная веб-разработка

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=""><!-- ниже экрана -->

Стратегии рендеринга и гидратация

Главный источник путаницы во фронтенде. Есть два независимых вопроса, которые постоянно смешивают:

  1. Где формируется HTML? В браузере (CSR), на сервере по запросу (SSR), на этапе сборки (SSG) или заранее с фоновым обновлением (ISR).
  2. Что делает JS после того, как HTML показан? Ничего (статика), оживляет всё дерево (полная гидратация), оживляет отдельные куски (острова) или подгружает обработчики по требованию (resumability).

Таймлайны CSR, SSR, SSG и стриминга

Что такое гидратация и почему она дорогая

Сервер прислал готовый 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>;
}

Как выбрать стратегию

Стратегия Сильная сторона Чем платите
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 блокирует отрисовку, что происходит на главном потоке и как браузер собирает финальный кадр.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов