Клиентская сторона: что происходит на стороне пользователя
Клиент — это единственная часть вашей системы, которая выполняется на технике, которой вы не владеете, в сети, которую вы не контролируете, у человека, чьи намерения вам неизвестны. Всё остальное — сервер, база, очередь, кластер — стоит в вашем периметре: вы выбираете железо, версию рантайма, момент рестарта. На клиенте у вас нет ничего из этого.
Из одного этого факта вытекает почти всё содержание главы. Мы не будем учить HTML, CSS и JavaScript — для этого есть трек frontend, где восемнадцать глав разбирают браузер, DOM, React, сборку и производительность до дна. Здесь другая задача: понять, где проходит граница клиента, какая ответственность на ней лежит и какие решения инженер принимает каждый день, стоя на этой границе.
Почему клиент — это отдельная инженерная задача
Серверный код живёт в тепличных условиях: известный процессор, известная версия ОС, гигабитная сеть до базы, единственная копия кода в проде. Клиентский код живёт в поле. Четыре свойства этого поля определяют всё.
Среда недоверенная. Любой байт, пришедший от клиента, написан противником — это рабочая гипотеза, а не паранойя. Пользователь открывает DevTools и правит переменные. Расширение подменяет fetch. Кто-то воспроизводит ваш запрос через curl вообще без браузера. Мобильное приложение распаковывают и читают строки. Клиентская проверка — это подсказка пользователю, а не защита системы. Подробности атак — в главах OWASP Top 10 и XSS и CSRF.
Железо чужое и разное. Разрыв в производительности устройств не сокращается, а растёт: флагман считает JavaScript в 5–10 раз быстрее бюджетного телефона, на котором сидит медианный пользователь развивающегося рынка. Алекс Расселл ежегодно измеряет этот разрыв в серии Performance Inequality Gap; вывод стабилен — «у меня локально быстро» не значит ничего. Бюджет на исполнение скрипта нужно задавать в миллисекундах на медленном устройстве, а не в килобайтах на вашем ноутбуке.
Сеть чужая. Метро, лифт, перегруженный Wi-Fi в кафе, корпоративный TLS-инспектирующий прокси, оператор, режущий заголовки. Задержка не константа, а распределение с длинным хвостом. Клиент обязан вести себя осмысленно, когда запрос идёт три секунды, когда он не дошёл, и когда он дошёл дважды. Что происходит на этом уровне — в треке networking.
Настройки чужие. Блокировщики рекламы вырезают ваши скрипты аналитики (а иногда и не только их). Пользователь увеличил шрифт до 200 %, включил тёмную тему, ограничил анимации, отключил сторонние куки. Экран — от 320 CSS-пикселей до телевизора.
Из этих четырёх фактов следуют три инженерных правила, к которым мы будем возвращаться всю главу: не доверяй (проверяй всё на сервере повторно), не предполагай (измеряй на реальных устройствах), деградируй (сценарий должен как-то работать, когда часть условий не выполнена).
«Клиент» — это гораздо шире, чем браузер
Слово «клиент» в вебе почти сроднилось со словом «браузер», и это мешает думать. Клиент — это роль в протоколе: тот, кто инициирует запрос. Ровно в этой роли выступают:
| Тип клиента | Как обновляется | Оффлайн | Чем ограничен |
|---|---|---|---|
| Браузер (веб) | мгновенно, при следующей загрузке | опционально, через Service Worker | размер бандла, время старта |
| Мобильное приложение | через магазин, дни–недели, старые версии живут годами | ожидается по умолчанию | батарея, память, тепловой троттлинг |
| Десктоп (Electron/Tauri/нативный) | собственный автоапдейтер | обычно полный | вес дистрибутива, права в ОС |
| CLI и SDK | через пакетный менеджер, обновляет пользователь | нет сети — нет работы | ожидание «скриптуемости», stdin/stdout |
| Встроенный дисплей, киоск, ТВ | прошивка, редко и болезненно | обязателен | слабый CPU, отсутствие ввода |
| Другой сервис | вместе с деплоем этого сервиса | нет | таймауты, ретраи, квоты |
Общего у всех ровно два элемента: контракт с сервером (что я имею право попросить и что мне обещали вернуть) и локальное состояние (что я помню между запросами). Всё остальное — детали платформы. Поэтому «серверный сервис, который ходит в чужой API» — такой же клиент, как браузер, и обязан жить по тем же правилам: таймауты, ретраи, версионирование контракта. Об этом — глава Интеграция.
Важное следствие: если у вас больше одного типа клиента, контракт становится главным артефактом системы. Мобильная версия 3.1, которую до сих пор не обновили 8 % пользователей, — это живой потребитель вашего API, который вы не можете починить деплоем. Отсюда — обратная совместимость как жёсткое требование, а не пожелание: см. эволюция контрактов.
Frontend, backend, fullstack: откуда взялось деление
Термины пришли не из моды, а из принципа разделения ответственности. Внутренняя реализация (как хранить, как считать, как гарантировать целостность) и внешнее представление (как показать, как принять ввод, как объяснить ошибку) меняются по разным причинам и с разной скоростью. Прайс-лист и правила скидок переписывают вместе с бизнесом; кнопку «Купить» перекрашивают вместе с редизайном. Складывать эти два потока изменений в один модуль — значит гарантировать, что каждое изменение будет задевать чужое. Это ровно та же логика, что в главе связность и связанность.
Практическое следствие деления — специализация: фронтендеру не обязательно знать план запроса в PostgreSQL, бэкендеру — тонкости каскада CSS. Но у деления есть цена: между двумя половинами появляется шов, и большинство дорогих багов живёт именно в шве. Кто нормализует деньги? Кто владеет форматом дат и часовым поясом? Что означает пустой массив — «нет данных» или «данные не загрузились»? Как выглядит ошибка, чтобы её можно было показать человеку?
«Fullstack» — это не «два специалиста по цене одного», а инженер, который держит шов целиком и может провести фичу от формы до таблицы, не устраивая двухнедельную переписку о формате поля. В маленькой команде это единственный работающий режим; в большой — ценная роль на стыках. Карьерные траектории разбирает трек sdlc-and-career.
Маятник: клиент то толстеет, то худеет
История клиент-серверных систем — это не прогресс по прямой, а колебание. Одни и те же аргументы («логика ближе к пользователю» против «логика ближе к данным») побеждают по очереди, потому что меняются внешние условия.
Маятник двигают пять сил, и полезно уметь называть ту, которая действует в вашем проекте.
- Задержка. Круговой путь до сервера — это 50–300 мс. Если отклик на ввод должен укладываться в 100 мс, логика обязана быть локальной. Отсюда толстые клиенты у редакторов, карт, таблиц.
- Стоимость и мощность устройства. Когда у пользователя мощная машина — выгодно грузить её. Когда медианное устройство слабое, а трафик дорогой — выгодно считать на сервере.
- Оффлайн. Требование «работает в самолёте и в поле» немедленно делает клиент толстым: нужны локальное хранилище, очередь операций и разрешение конфликтов.
- SEO и первый экран. Поисковику и пользователю, пришедшему по ссылке, нужен готовый HTML быстро. Это тянет рендеринг обратно на сервер.
- Размер и структура команды. SPA плюс REST — способ разрезать команду на две независимые. Для команды из трёх человек это чистые накладные расходы, и монолит с серверным рендерингом окажется быстрее в разработке. Подробнее — Архитектурные шаблоны на практике.
Практический вывод: у решения «толстый или тонкий» всегда есть доминирующая сила. Если вы не можете её назвать, вы выбираете по моде.
Что можно и что нельзя класть на клиент
Правило, которое стоит держать в голове постоянно:
Клиент — это UX. Сервер — это истина.
Всё, что влияет на деньги, доступ, целостность и историю, решается на сервере. Всё, что влияет на скорость отклика и удобство, может и должно жить на клиенте.
Нельзя на клиенте:
- Прямой доступ к базе данных. Клиент не может ходить в СУБД: у него нет способа хранить пароль, нет способа ограничить запрос, нет способа гарантировать, что он выполнит именно тот SQL, который вы задумали. Даже когда платформа это предлагает (Firebase, Supabase и подобные), доступ на самом деле идёт через сервисный слой с правилами доступа — вопрос лишь в том, кто написал этот слой.
- Авторитетная бизнес-логика. Скидка, лимит, право на действие, расчёт стоимости — на сервере. Клиент может продублировать расчёт, чтобы нарисовать сумму мгновенно, но итог берётся из ответа сервера.
- Авторитетное состояние. «Заказ оплачен», «остаток на складе», «пользователь — админ» — это состояние сервера. Клиент держит копию, которая всегда может устареть.
- Секреты. Всё, что попало в бандл или в бинарник приложения, — публично. Ключ API, подписывающий секрет, пароль к внешнему сервису — только на сервере, см. управление секретами.
Можно и нужно на клиенте:
- Валидация формата и обязательности — чтобы человек узнал об ошибке до отправки, а не через 400 мс.
- Операции над уже загруженными данными: сортировка, группировка, фильтрация, подсчёт итогов по видимой таблице. Ключевое слово — уже загруженными: сортировать сто строк на клиенте правильно, «загрузить сто тысяч и отсортировать» — нет.
- Состояние интерфейса: открытые панели, выбранные вкладки, черновики, позиция скролла, тема оформления.
- Транспортное шифрование и криптография там, где она уместна: TLS обязателен всегда; сквозное шифрование (мессенджеры, менеджеры паролей) действительно требует, чтобы ключ никогда не покидал устройство. Но «зашифруем пароль на клиенте перед отправкой» вместо HTTPS — это не безопасность, а её имитация: см. прикладная криптография.
- Оптимистичные обновления — показать результат до подтверждения сервера, с честным откатом при отказе.
Двойная валидация — не дублирование, а разделение целей
Классическое возражение: «мы же уже проверили на клиенте». Проверили — но для другой цели. Клиент валидирует ради скорости отклика. Сервер валидирует ради корректности. Это два разных требования, и удовлетворять их надо в двух местах.
Обратите внимание на узел «Сеть»: между клиентской и серверной проверкой лежит участок, который вы не контролируете. Именно поэтому стрелка от B к F не может быть заменена доверием.
Практический приём — одна схема, два потребителя. Формат описывается один раз в общем модуле, из него выводятся и типы, и валидатор; клиент и сервер импортируют одно и то же. Сервер добавляет сверху то, чего клиент знать не может.
// shared/order.ts — общий модуль, его импортируют И клиент, И сервер.
import { z } from "zod";
export const OrderDraft = z.object({
itemId: z.string().uuid(),
email: z.string().email("Некорректный адрес почты"),
// Количество — целое, от 1 до 100. Это правило формата: клиент его знает.
quantity: z.number().int().min(1).max(100),
promoCode: z.string().regex(/^[A-Z0-9-]{4,16}$/, "Неверный формат промокода").optional(),
// Цены здесь нет намеренно: цену клиент не присылает, её считает сервер.
});
export type OrderDraft = z.infer<typeof OrderDraft>;
// client/checkout.ts — валидация ради скорости отклика.
import { OrderDraft } from "../shared/order";
export function validateDraft(input: unknown) {
const parsed = OrderDraft.safeParse(input);
if (parsed.success) return { ok: true as const, value: parsed.data };
// Раскладываем ошибки по именам полей, чтобы подсветить конкретный input.
const byField: Record<string, string> = {};
for (const issue of parsed.error.issues) {
byField[issue.path.join(".")] = issue.message;
}
return { ok: false as const, byField };
}
// server/orders.ts — валидация ради корректности плюс всё, чего клиент не знает.
import { OrderDraft } from "../shared/order";
export async function createOrder(req: Request, ctx: Ctx): Promise<Response> {
// 1. Формат — та же схема. Никакого доверия к тому, что клиент уже проверил.
const parsed = OrderDraft.safeParse(await req.json());
if (!parsed.success) {
return json(422, { errors: parsed.error.flatten().fieldErrors });
}
const draft = parsed.data;
// 2. Кто это. Личность берём из сессии или токена, а НЕ из тела запроса.
const user = await ctx.auth.requireUser(req);
// 3. Правила, которые клиент физически не может проверить честно.
const item = await ctx.db.items.findForUpdate(draft.itemId);
if (!item || item.stock < draft.quantity) {
return json(409, { error: "OUT_OF_STOCK", available: item?.stock ?? 0 });
}
const discount = await ctx.pricing.resolvePromo(draft.promoCode, user.id);
// 4. Цена считается ЗДЕСЬ, из БД и правил, и никогда не берётся из запроса.
const total = ctx.pricing.total(item.price, draft.quantity, discount);
// 5. Идемпотентность: повтор с тем же ключом не создаёт второй заказ.
const key = req.headers.get("Idempotency-Key");
const order = await ctx.db.orders.createIdempotent(key, {
userId: user.id, itemId: item.id, quantity: draft.quantity, total,
});
return json(201, { id: order.id, total: order.total });
}
Три детали этого кода стоят целой главы. Личность берётся из сессии, а не из поля userId в теле — иначе любой клиент оформит заказ от чужого имени. Цена считается на сервере — иначе покупка за 1 копейку станет вопросом одного вызова в DevTools. Ключ идемпотентности превращает «пользователь дважды нажал кнопку» и «сеть повторила запрос» в безопасные события: см. идемпотентность и доставка.
Как всё это выглядит со стороны формы — подробно в главе формы и валидация.
Четыре сорта состояния — и почему их нельзя смешивать
Больше половины «загадочных» багов интерфейса — это одна ошибка: разные по природе данные положили в одно хранилище. Разделите их, и половина проблем исчезнет сама.
| Сорт | Источник истины | Срок жизни | Что с ним делают |
|---|---|---|---|
| Серверный кэш — список заказов, профиль, справочники | сервер | пока не устареет | загружают, инвалидируют, перезапрашивают |
| UI-состояние — открытая модалка, свёрнутая панель, выбранная вкладка | клиент | пока экран смонтирован | переключают |
| Состояние формы — введённые значения, «поле трогали», ошибки | пользователь | пока форма не отправлена | редактируют, валидируют, сбрасывают |
| Состояние маршрута — id открытой сущности, фильтры, страница, поиск | URL | пока адрес не изменился | читают из адреса, кладут в адрес |
Смешение даёт узнаваемые симптомы. Серверные данные, положенные в глобальный стор как обычное поле, никогда не инвалидируются — пользователь видит вчерашние цифры. Фильтры, спрятанные в локальный стейт вместо URL, ломают кнопку «назад» и делают ссылку на отфильтрованный список бесполезной. Состояние формы, поднятое в глобальный стор, живёт после ухода с экрана и «воскресает» с чужими данными. Модалка, привязанная к серверному кэшу, схлопывается при фоновом обновлении.
Практическое правило: у каждого куска состояния должен быть один владелец, и вы должны уметь назвать его вслух. Серверные данные — библиотека кэширования запросов (TanStack Query и аналоги). Маршрут — роутер и адресная строка. Форма — библиотека форм или локальный стейт компонента. UI — локальный стейт, поднятый ровно настолько, насколько нужно. Глубже — управление состоянием и загрузка данных.
Серверный кэш — единственный сорт с нетривиальным жизненным циклом, и его стоит держать в голове целиком:
Ключевой момент этой схемы — состояние Stale: данные устарели, но они есть, и показывать пустой скелетон вместо них — регресс, а не корректность. «Показываем старое, тихо обновляем, помечаем момент обновления» — почти всегда лучший UX, и почти всегда его забывают заложить.
Стратегии рендеринга: карта без углубления
Пять стратегий покрывают практически всё. Ниже — карта «когда что», детали и код — в главе маршрутизация и рендеринг и в обзорной статье Google Rendering on the Web.
| Стратегия | Где собирается HTML | Первый экран | Кому подходит | Главная цена |
|---|---|---|---|---|
| CSR | в браузере | медленный, зависит от JS | админки, редакторы, всё за логином | пустой HTML для поисковика, долгий старт |
| SSR | на сервере, на каждый запрос | быстрый | персонализированные страницы | нагрузка на сервер, сложнее кэшировать |
| SSG | при сборке | самый быстрый | документация, блоги, лендинги | контент обновляется только пересборкой |
| ISR | при сборке плюс фоновое обновление | быстрый | каталоги, витрины | сложность инвалидации, «немного устаревшие» страницы |
| Стриминг / RSC | на сервере, по частям | быстрый и прогрессивный | смешанные страницы «шапка мгновенно, лента потом» | требует зрелого фреймворка и дисциплины |
Две ловушки. Первая: стратегия выбирается на страницу, а не на приложение — лендинг может быть статикой, каталог ISR, кабинет CSR, и это нормально. Вторая: выбор рендеринга бессмысленно обсуждать в отрыве от доставки — CDN, кэш-заголовки и edge меняют картину сильнее, чем сам механизм: см. CDN и edge и serverless и edge.
Из чего сделан веб-клиент: три языка, три роли
Фронтенд стоит на трёх языках, и их разделение — не историческая случайность, а всё тот же принцип разделения ответственности, только внутри страницы.
HTML отвечает за структуру и смысл. Это не «то, из чего рисуется вёрстка», а описание документа: здесь заголовок, здесь навигация, здесь кнопка, здесь поле ввода с подписью. От этого смысла напрямую зависит доступность (скринридер читает роли, а не цвета), SEO и поведение по умолчанию: <button> реагирует на Enter и Space, попадает в порядок табуляции и объявляется как кнопка — бесплатно, тогда как <div onclick> не делает ничего из этого и требует ручной починки. Правильная разметка — это ещё и деградация: страница остаётся читаемой, когда скрипт не загрузился. Подробно — семантика HTML.
CSS отвечает за представление. Декларативный язык, где вы описываете желаемый результат, а браузер решает, как его достичь; каскад, наследование и специфичность определяют, какое правило победит. Главная инженерная сложность CSS — не синтаксис свойств, а масштабируемость: в проекте на сотню компонентов стили начинают конфликтовать, и вопрос «как изолировать» (методология вроде BEM, CSS-модули, CSS-in-JS, утилитарные классы) важнее вопроса «как отцентрировать». Раскладка сегодня — это Flexbox и Grid, а адаптивность — контейнерные запросы и логические свойства. См. основы CSS, раскладка и архитектура стилей.
JavaScript отвечает за поведение. Он реагирует на действия пользователя, меняет DOM, ходит по сети, хранит данные локально, управляет историей навигации. Это единственный язык с прямым доступом к DOM: WebAssembly даёт скорость для вычислений (кодеки, 3D, CAD), но в DOM всё равно ходит через JS. Практически весь продуктовый фронтенд сегодня пишут на TypeScript — не из любви к типам, а потому что контракт с сервером и структура состояния должны проверяться компилятором, а не в проде. См. DOM и события.
Ландшафт фреймворков: короткая честная история
Легаси-материалы любят фразу «фреймворк номер один». В 2026 году она бессмысленна: категории разные, а лидерство меряется в разных единицах.
Bootstrap (2011, вырос из внутреннего Twitter Blueprint) — это CSS-фреймворк, а не конкурент React. Он даёт сетку, набор готовых компонентов и предсказуемый вид без дизайнера. Он по-прежнему массово используется — прежде всего в админках, внутренних инструментах и прототипах, — но перестал быть выбором по умолчанию: утилитарные системы вроде Tailwind и собственные дизайн-системы на CSS-переменных заняли большую часть новых проектов. Признак «бутстраповского» интерфейса узнаётся с первого взгляда, и это его главный минус для продукта с брендом.
AngularJS (2010) — историческая величина. Google прекратил поддержку 31 декабря 2021 года; сегодня это чистое легаси, и встреча с ним означает проект на миграцию. Angular 2+ — другой фреймворк с тем же брендом: TypeScript-first, встроенный DI, свой CLI, реактивность на RxJS, а с версии 16+ — сигналы. Это «батарейки в комплекте» для больших корпоративных приложений и команд, которым нужна единообразная структура кода без споров.
React (2013, Facebook) — библиотека представления, а не фреймворк: маршрутизация, загрузка данных и сборка добираются экосистемой или метафреймворком (Next.js, Remix). Крупнейшая экосистема и рынок труда; современный вектор — серверные компоненты и перенос части работы обратно на сервер. Здесь мы намеренно не разбираем ни хуки, ни реконсиляцию — это 08-react-fundamentals и 09-react-hooks-and-patterns.
Vue (2014) — прогрессивный фреймворк: подключается тегом на одну страницу и масштабируется до полноценного SPA с Nuxt. Vue 3 принёс Composition API и точную реактивность; сильные стороны — низкий порог входа и цельная официальная экосистема (роутер, стор, devtools от той же команды).
Svelte и Solid — компиляторный и сигнальный подходы: работа по поиску изменений переносится на этап сборки, виртуальный DOM не нужен, рантайм минимален. Сообщества меньше, зато код проще и легче.
Главный тезис, который стоит унести: разница между фреймворками почти всегда меньше, чем разница между хорошей и плохой архитектурой на одном и том же фреймворке. Честное сравнение по осям производительности, экосистемы и стоимости владения — в главе сравнение фреймворков.
Карта компетенций клиентского инженера
Легаси-список технологий («HTML, CSS, jQuery, CMS, SVG, DOM, базы данных…») — это свалка, из которой невозможно построить план обучения. Осмысленная карта выглядит иначе: слои, у каждого своя роль, и внутри слоя ясно, что учить раньше.
Что из старого списка ушло и почему: CMS (WordPress, Drupal, Joomla) — это отдельная профессия, а не компетенция фронтендера; jQuery — историческая справка; «базы данных и SQL» — полезно понимать модель, но это databases, а не рабочий инструмент клиента; препроцессоры CSS во многом вытеснены нативными переменными и вложенностью. Что добавилось и стало обязательным: типы, доступность, производительность как измеряемая величина, безопасность клиента и эксплуатация — фронтенд перестал заканчиваться на «сдал вёрстку». Инструментальная часть — в главах сборка и зависимости и качество в потоке.
Мобильный и десктопный клиент: чем отличаются инженерно
Веб приучает к мысли, что клиент обновляется мгновенно. Мобильный клиент отучает от неё жёстко.
Релизный цикл идёт через магазин. Между «нашли баг» и «фикс у пользователя» лежат ревью в App Store, раскатка и — главное — готовность пользователя обновиться. Практическое следствие: критичные переключатели должны быть на сервере (фиче-флаги, remote config), а API — терпимым к старым версиям клиента годами. См. релизы и магазины.
Оффлайн — не фича, а базовое ожидание. Мобильное приложение обязано открываться в метро и показывать последние данные. Значит: локальная база, очередь исходящих операций, разрешение конфликтов, синхронизация. Это резко толстит клиент — см. данные и оффлайн.
Ресурсы конечны и наказуемы. Батарея, тепловой троттлинг, лимиты фоновой работы, платный трафик. Лишний polling здесь стоит не миллисекунд, а процентов заряда и оценок в сторе: производительность и фоновая работа и push.
Платформенные API и разрешения. Камера, геолокация, биометрия, уведомления — всё через диалоги согласия, всё с разной семантикой на iOS и Android. Выбор «нативно или кросс-платформенно» (Kotlin Multiplatform, Flutter, React Native) — не про синтаксис, а про то, сколько платформенной специфики вы готовы обслуживать: кросс-платформенная разработка.
Десктоп (Electron, Tauri, нативные тулкиты) добавляет свои особенности: доступ к файловой системе и локальным устройствам, собственный механизм автообновления, подпись и нотаризация дистрибутива, работа в корпоративной сети за прокси. Электрон-подобные приложения — это фактически ваш веб-клиент с расширенными правами, поэтому все правила клиентской безопасности к ним применимы вдвойне.
Типичные ошибки
Секреты в бандле. Ключ платёжного провайдера в .env, который собирается в клиентский код; токен админского API в мобильном приложении. Всё, что уехало на устройство, — публично. Лечится ревью и автоматическим сканированием артефактов сборки, см. supply chain.
Доверие клиентской валидации. «Кнопка задизейблена, значит нельзя» — самый дешёвый способ потерять деньги. Каждая проверка, влияющая на результат, повторяется на сервере.
N+1 запросов с клиента. Загрузили список, потом для каждого элемента дёрнули отдельный запрос за деталями. На локалхосте незаметно, на 4G — пять секунд ожидания. Лечится агрегирующей ручкой, батчингом или сменой стиля API — см. стили API. Отдельный подвид — «водопад»: запрос, который стартует только после ответа предыдущего, хотя мог бы идти параллельно.
Бесконтрольный рост бандла. Никто не добавляет мегабайт разом — добавляют по 30 килобайт в неделю, и через год приложение стартует четыре секунды. Единственное лекарство — бюджет производительности, проверяемый в CI: размер чанка и метрики Core Web Vitals как failing-проверка, а не как отчёт, который никто не читает. См. производительность веба.
«Сделаем SPA, потому что модно». Контент-сайт на SPA получает худший первый экран, проблемы с индексацией, ручную реализацию того, что браузер умеет сам (история, скролл, предзагрузка), и вдвое больший объём кода. Если у страницы нет длинной интерактивной сессии — SPA не нужен.
Смешение сортов состояния. Разобрано выше; симптом — «данные не обновились», «кнопка назад не работает», «форма помнит чужой ввод».
Отсутствие состояний ошибки и пустоты. Дизайн-макет обычно нарисован для счастливого пути. В проде экран чаще бывает в состоянии «загружается», «пусто», «нет сети», «нет прав» — и все четыре надо спроектировать, иначе пользователь увидит белый экран или вечный спиннер.
Мини-итог
- Клиент — это код в недоверенной среде на чужом железе и чужой сети. Три правила: не доверяй, не предполагай, деградируй.
- «Клиент» — роль в протоколе, а не браузер. Общее у всех клиентов — контракт с сервером и локальное состояние.
- Деление на frontend/backend выросло из разделения ответственности; главная работа инженера происходит в шве между ними.
- Толщина клиента колеблется исторически; выбирая её, назовите доминирующую силу — задержку, оффлайн, SEO, устройство или размер команды.
- Клиент — это UX, сервер — это истина. Валидация делается дважды, с разными целями; цена, права и итог считаются только на сервере.
- Клиентское состояние бывает четырёх сортов, у каждого свой владелец и свой жизненный цикл. Смешение — источник большинства багов интерфейса.
- Стратегия рендеринга выбирается на страницу, а не на приложение.
- Фреймворк — наименее важное из ваших решений; архитектура и дисциплина важнее.
Источники
- Alex Russell. The Performance Inequality Gap — почему «у меня быстро» ничего не значит.
- Google. Rendering on the Web — каноническая карта CSR/SSR/SSG и гибридов.
- Core Web Vitals — метрики, по которым измеряют клиент.
- Jakob Nielsen. Response Times: The 3 Important Limits — откуда берутся 100 мс и 1 с.
- MDN: Progressive Web Apps — оффлайн и установка веб-клиента.
- Zod — схема как единственный источник истины о формате.
- TanStack Query — эталонная модель работы с серверным кэшем.
- OWASP Top 10 — что именно ломают на клиенте и через клиент.
- Web Almanac — ежегодная статистика по реальному вебу вместо ощущений.
Что дальше
Мы разобрали половину контракта — ту, что живёт у пользователя. Теперь перейдём на другую сторону провода и посмотрим, что происходит с запросом после того, как он покинул устройство: Серверная сторона.