Инженерная практика Клиентская сторона: что происходит на стороне пользователя
0%

Клиентская сторона: что происходит на стороне пользователя

Клиентская сторона: что происходит на стороне пользователя

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

Из одного этого факта вытекает почти всё содержание главы. Мы не будем учить 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.

Маятник: клиент то толстеет, то худеет

История клиент-серверных систем — это не прогресс по прямой, а колебание. Одни и те же аргументы («логика ближе к пользователю» против «логика ближе к данным») побеждают по очереди, потому что меняются внешние условия.

Маятник двигают пять сил, и полезно уметь называть ту, которая действует в вашем проекте.

  1. Задержка. Круговой путь до сервера — это 50–300 мс. Если отклик на ввод должен укладываться в 100 мс, логика обязана быть локальной. Отсюда толстые клиенты у редакторов, карт, таблиц.
  2. Стоимость и мощность устройства. Когда у пользователя мощная машина — выгодно грузить её. Когда медианное устройство слабое, а трафик дорогой — выгодно считать на сервере.
  3. Оффлайн. Требование «работает в самолёте и в поле» немедленно делает клиент толстым: нужны локальное хранилище, очередь операций и разрешение конфликтов.
  4. SEO и первый экран. Поисковику и пользователю, пришедшему по ссылке, нужен готовый HTML быстро. Это тянет рендеринг обратно на сервер.
  5. Размер и структура команды. 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 — ежегодная статистика по реальному вебу вместо ощущений.

Что дальше

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

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

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

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

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