Введение
Эта подборка охватывает 50 вопросов по Web-платформе, которые Senior Frontend Developer должен понимать на глубоком уровне. Это не про JavaScript или React - это про то, как работает браузер, сеть, безопасность и производительность. Каждый ответ содержит объяснение внутренней механики, векторы атак (где применимо), стратегии защиты и осознанные tradeoffs.
Вопросы идут в порядке убывания частоты появления на собеседованиях: от фундаментальной безопасности до продвинутых тем архитектуры.
1. Полный цикл загрузки и рендеринга страницы: от URL до пикселя
1. Ввод URL и DNS-резолвинг - пользователь вводит URL, браузер проверяет кеш (браузерный, OS, роутер, ISP). Рекурсивный DNS-запрос: Root → TLD → Authoritative. A/AAAA записи возвращают IP. Оптимизации: dns-prefetch (только DNS, ~50ms в тёплом кеше, до 300ms в холодном), preconnect (DNS + TCP + TLS).
2. TCP handshake - SYN → SYN-ACK → ACK, 1.5 RTT (~30-100ms). TCP Fast Open (TFO) для повторных соединений позволяет отправлять данные уже в SYN-пакете, экономя 1 RTT.
3. TLS handshake (HTTPS):
- TLS 1.2: 2-RTT (ClientHello → ServerHello → Certificate → Key Exchange → Finished)
- TLS 1.3: 1-RTT (объединены шаги, шифруется больше данных)
- Session resumption через Session ID/Tickets, 0-RTT для повторных визитов
- Итого: ~50-200ms для TLS 1.3
4. HTTP-запрос и ответ:
- Отправка запроса (метод, заголовки, тело), server-side обработка
- HTTP/2: мультиплексирование стримов в одном TCP-соединении, HPACK сжатие заголовков
- HTTP/3 (QUIC): UDP вместо TCP, устранение HOL blocking, 0-RTT resumption, connection migration
- Ответ: статус, заголовки, body
5. Парсинг HTML и построение DOM:
Браузер получает первые байты, начинает инкрементальный парсинг: байты → символы → токены → узлы DOM. Preload scanner (спекулятивный анализ) обнаруживает ресурсы пока парсер заблокирован скриптом. Parser-blocking скрипты (<script> без атрибутов) останавливают парсинг. async - загружается параллельно, выполняется сразу после загрузки. defer - загружается параллельно, выполняется после парсинга в порядке объявления.
6. Парсинг CSS и построение CSSOM: Загрузка всех CSS (link, style, inline), построение CSS Object Model. CSS render-blocking - браузер не может рендерить без CSSOM, так как не знает стили. Но CSS НЕ блокирует DOM-парсинг. Каскадирование и specificity определяют финальные значения свойств.
7. JavaScript: загрузка и выполнение:
Parser-blocking скрипты останавливают парсинг. Async-скрипты выполняются как готовы (порядок не гарантирован). Defer-скрипты выполняются после DOM-парсинга в порядке объявления, перед DOMContentLoaded. Module-скрипты (<script type="module">) defer по умолчанию. Влияние на DOMContentLoaded: defer-скрипты задерживают событие до своего выполнения.
8. Построение Render Tree:
Объединение DOM + CSSOM. Исключаются невидимые элементы (display: none, <head>, <script>, <link>). Псевдоэлементы (:before, :after) включаются в Render Tree. Render Tree содержит только видимые элементы с их стилями.
9. Layout (Reflow):
Вычисление геометрии каждого видимого элемента - размеры, позиции. Box model: content → padding → border → margin. Reflow вызывают: изменение размеров, позиции, содержимого, resize окна, смена шрифта. Forced synchronous layout возникает при чтении геометрических свойств (offsetHeight, clientWidth, getComputedStyle()) между записями DOM - браузер вынужден синхронно пересчитать layout.
10. Paint:
Преобразование Layout-дерева в список команд отрисовки (display lists). Разбивка на слои: will-change, transform, <video>, <canvas> создают отдельные composite-слои. Порядок отрисовки определяется z-index и stacking context. Paint заполняет пиксели - цвет, тени, фон, текст.
11. Composite:
Объединение слоёв в финальное изображение. GPU-ускорение через translateZ(0), will-change. Compositor thread работает независимо от main thread - анимации transform и opacity выполняются на compositor thread без блокировки main thread. Это единственный способ плавных 60fps анимаций.
Important
Полный цикл от клика до первого пикселя: DNS (~50ms в тёплом кеше, до 300ms в холодном) → TCP (~1.5 RTT, ~30-100ms) → TLS (~1-2 RTT, ~50-200ms для TLS 1.3) → HTTP-запрос (~1 RTT, ~30-100ms) → парсинг HTML (~5-20ms на 100KB) → CSSOM (~5-20ms) → JS-выполнение (зависит от объёма) → Layout (~10-30ms) → Paint (~10-20ms) → Composite (~5ms на GPU). Итого First Contentful Paint обычно 1-2.5 секунды на среднестатистическом соединении.
2. XSS: Reflected, Stored, DOM-based
XSS (Cross-Site Scripting) - инъекция злонамеренного скрипта в контекст другого пользователя. Три типа:
Reflected XSS - сервер отражает несанированный пользовательский ввод обратно в HTML (например, параметр ?q=<script>...</script> в поисковом запросе). Срабатывает при переходе жертвы по подготовленной ссылке.
Stored XSS - злонамеренный скрипт сохраняется в базе данных (комментарий, поле профиля) и выполняется у всех посетителей страницы. Наиболее опасный тип.
DOM-based XSS - атака происходит исключительно на клиенте: innerHTML, eval(), document.write(), location.hash используются для вставки данных из untrusted source в sink без валидации.
// DOM-based XSS через location.hash
const userInput = location.hash.slice(1);
document.getElementById('content').innerHTML = userInput; // XSS!Защита:
- Content-Security-Policy с
script-src 'self'и nonce-based подходом - Всегда использовать
textContentвместоinnerHTMLпри вставке пользовательского контента - Санировать контент через DOMPurify если HTML-вставка неизбежна
- Escaping контекстно-зависимый: HTML-entities в HTML-контексте, JavaScript-escape в JS-контексте
Important
CSP - это defence-in-depth, последняя линия защиты, а не замена санированию. Если у вас есть innerHTML с пользовательскими данными, никакой CSP не защитит от DOM XSS если скрипт уже на странице с разрешённым ‘self’.
Почему textContent безопаснее: textContent присваивает значение как текстовый узел в DOM, не проходя HTML-парсинг. <script> будет отображаться как текст, а не исполняться.
3. CSRF - механика атаки, SameSite cookies, CSRF tokens
CSRF (Cross-Site Request Forgery) - атакующий заставляет браузер жертвы выполнить нежелательный запрос к сайту, где жертва аутентифицирована. Браузер автоматически прикрепляет куки к кросс-доменному запросу.
Механика: Пользователь залогинен на bank.com. Атакующий размещает на evil.com форму <form action="https://bank.com/transfer" method="POST"> с скрытыми полями и автосабмитом. Браузер отправляет куки bank.com вместе с запросом.
<!-- На evil.com -->
<form action="https://bank.com/transfer" method="POST" id="csrf">
<input type="hidden" name="to" value="attacker">
<input type="hidden" name="amount" value="10000">
</form>
<script>document.getElementById('csrf').submit();</script>Защита:
- SameSite cookies:
SameSite=Strictблокирует отправку кук при кросс-сайтовых запросах.SameSite=Laxразрешает для top-level navigation (ссылкам), но блокирует для POST-форм с других сайтов - разумный default - CSRF tokens: сервер генерирует уникальный токен на сессию, клиент отправляет его в заголовке или теле запроса. Атакующий не может прочитать токен из-за SOP
- Double-submit cookie: токен дублируется в куке и в теле/заголовке запроса - сервер сравнивает их. Не требует хранения токена на сервере, но менее надёжен при subdomain takeover
- Custom header requirement: API требует кастомный заголовок (
X-Requested-With: XMLHttpRequest) - браузеры не позволяют кросс-доменным формам устанавливать кастомные заголовки без CORS
Important
С современными браузерами
SameSite=Lax(дефолт с Chrome 80+) покрывает большинство CSRF-векторов. Однако для защиты от уязвимостей на поддоменах и старых браузерах комбинируйте с CSRF-токенами.
4. CORS - preflight, simple requests, credentials
CORS (Cross-Origin Resource Sharing) - механизм, позволяющий серверу ослабить Same-Origin Policy для конкретных origin’ов. Браузер проверяет ответные заголовки и решает, разрешить ли доступ JS к ответу.
Simple requests - GET/POST/HEAD с определёнными заголовками (Accept, Accept-Language, Content-Language, Content-Type: text/plain | multipart/form-data | application/x-www-form-urlencoded). Не требуют preflight.
Preflight - OPTIONS запрос, отправляемый браузером перед основным запросом если тот не “simple” (например, Content-Type: application/json или кастомный заголовок). Сервер отвечает заголовками Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Max-Age.
OPTIONS /api/data HTTP/1.1
Origin: https://example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, PUT, DELETE
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Max-Age: 86400Credentials: По умолчанию кросс-доменные запросы не отправляют куки. Для включения: клиент fetch(url, { credentials: 'include' }) или xhr.withCredentials = true, сервер Access-Control-Allow-Credentials: true. При этом Access-Control-Allow-Origin не может быть *.
Почему * + credentials несовместимы: Если бы сервер мог разрешить * с credentials, любой сайт мог бы читать аутентифицированные ответы. Браузер принудительно требует конкретный origin.
Info
Preflight существует чтобы старые сервера, не знающие о CORS, не могли быть атакованы через cross-origin DELETE или PUT запросы, которые они не ожидают.
5. Content-Security-Policy (CSP)
CSP - HTTP-заголовок (или <meta>), определяющий какие ресурсы и откуда браузер может загружать. Белый список источников.
Основные директивы:
default-src- fallback для всех неуказанных директивscript-src- разрешённые источники скриптов (и inline-скриптов при'unsafe-inline')style-src- источники стилейimg-src,font-src,connect-src,frame-src,media-srcframe-ancestors- кто может встраивать страницу в iframe (заменяет X-Frame-Options)form-action- куда можно отправлять формы
Nonce-based подход: Сервер генерирует случайный nonce для каждого ответа, вставляет его в CSP-заголовок и в атрибут <script nonce="...">. Легитимные скрипты выполняются, инлайн-инъекции - нет.
Content-Security-Policy: script-src 'nonce-r4nd0m' 'strict-dynamic'; style-src 'self'; img-src *<script nonce="r4nd0m">console.log('allowed');</script>Hash-based подход: В CSP указывается SHA-256 хеш разрешённого инлайн-скрипта. Не требует генерировать nonce на сервере, но при любом изменении скрипта хеш надо обновить.
Content-Security-Policy: script-src 'sha256-abc123...' 'strict-dynamic''strict-dynamic' - разрешает скрипту, загруженному через nonce/hash, создавать другие скрипты (например, через document.createElement('script')). Без этого динамически созданные легитимные скрипты блокируются.
Report-only: Content-Security-Policy-Report-Only позволяет собирать отчёты о нарушениях не блокируя их:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-reportImportant
CSP - мощный defence-in-depth инструмент, но его сложно правильно сконфигурировать на существующем приложении. Начинайте с Report-Only, собирайте отчёты, итеративно ужесточайте политику.
6. Same-Origin Policy (SOP) + COOP/COEP
Same-Origin Policy - фундаментальный механизм безопасности браузера, предотвращающий доступ документа или скрипта из одного origin к ресурсам другого origin.
Что такое origin: scheme + host + port. https://example.com:443 и http://example.com:80 - разные origin из-за scheme. https://example.com и https://sub.example.com - разные из-за host.
Что ограничивает SOP:
- Доступ к DOM другого окна/iframe (cross-origin iframe нечитаем)
- AJAX-запросы - ответ читаем только с того же origin (CORS ослабляет это)
- Доступ к localStorage, cookies, IndexedDB - разделены по origin
- Шрифты через
@font-face(по умолчанию)
Что НЕ ограничивает SOP (по историческим причинам):
<script>,<img>,<video>,<link>,<iframe>могут загружать кросс-доменные ресурсы (но их содержимое недоступно через JS, только отображение)
Info
SOP существует не просто так - без него любой сайт мог бы через fetch читать ваш почтовый ящик на gmail.com, или через iframe + DOM-доступ читать страницу банка, где вы залогинены.
Современное расширение: Cross-Origin-Opener-Policy (COOP) и Cross-Origin-Embedder-Policy (COEP) позволяют процессной изоляции origin’ов и включают мощные фичи вроде SharedArrayBuffer.
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corpCOOP (Cross-Origin-Opener-Policy) контролирует, может ли страница иметь доступ к window.opener при открытии кросс-доменного окна. same-origin изолирует страницу в отдельном browsing context - кросс-доменные окна не получают ссылку на opener. Это предотвращает Spectre-подобные атаки через shared memory.
COEP (Cross-Origin-Embedder-Policy) требует, чтобы все кросс-доменные ресурсы были явно разрешены через CORS или CORP. require-corp означает: каждый кросс-доменный ресурс должен иметь заголовок Cross-Origin-Resource-Policy: same-site или cross-origin, либо быть загружен через CORS. Без COEP браузер не даст доступ к SharedArrayBuffer и performance.measureUserAgentSpecificMemory().
Вместе COOP + COEP создают полностью изолированный origin - это требование для мощных API, работающих с общей памятью между воркерами.
7. HTTPS/TLS handshake: 1.3 vs 1.2, certificate chain, HSTS
TLS handshake (1.3): Клиент отправляет ClientHello с поддерживаемыми cipher suites и случайным числом. Сервер отвечает ServerHello, выбирая cipher suite, плюс свой случайный number, и отправляет сертификат с публичным ключом. Обе стороны вычисляют общий секрет через (EC)DHE. В TLS 1.3 весь handshake занимает 1-RTT (против 2-RTT в TLS 1.2).
TLS 1.2 vs 1.3 ключевые отличия:
- TLS 1.2: 2-RTT handshake, поддерживает устаревшие cipher suites (RSA key exchange, CBC mode, SHA-1)
- TLS 1.3: 1-RTT handshake, удалены все небезопасные алгоритмы, обязательный (EC)DHE (forward secrecy), 0-RTT resumption через PSK (pre-shared key)
- TLS 1.3 шифрует больше данных handshake - в 1.2 ServerHello и сертификат были в открытом виде, в 1.3 они зашифрованы
Цепочка доверия (Chain of Trust): Сертификат сайта подписан Intermediate CA, Intermediate CA подписан Root CA. Root CA встроены в браузер/ОС (trust store). Проверка идёт снизу вверх: если корень доверенный, вся цепочка доверенная.
Сертификаты:
- DV (Domain Validation): проверка только владения доменом
- OV (Organization Validation): проверка организации
- EV (Extended Validation): расширенная проверка (зелёная плашка, сейчас браузеры убирают)
Почему HTTPS важен кроме шифрования: HTTPS обеспечивает три свойства - конфиденциальность (шифрование), целостность (MAC, защита от модификации), и аутентификацию (удостоверяет, что клиент общается с настоящим сервером).
Strict-Transport-Security: max-age=63072000; includeSubDomains; preloadImportant
HSTS заголовок заставляет браузер всегда использовать HTTPS для домена, даже если пользователь ввёл http:// - защита от SSL-stripping атак. Для включения в HSTS preload list (встроенную в браузеры) домен должен подать заявку на hstspreload.org.
8. JWT: HS256 vs RS256, refresh flow, полный цикл создания и проверки
JWT (JSON Web Token) - компактный URL-safe формат передачи claims между сторонами. Состоит из трёх частей через точку: header.payload.signature.
eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjMiLCJpYXQiOjE1MTYyMzkwMjJ9.signature
header: { "alg": "RS256", "typ": "JWT" }
payload: { "sub": "123", "iat": 1516239022, "exp": 1516242622 }
HS256 vs RS256 - ключевой выбор архитектуры:
| HS256 (HMAC + SHA-256) | RS256 (RSA + SHA-256) | |
|---|---|---|
| Тип ключа | Один симметричный секрет | Пара: приватный + публичный ключ |
| Кто подписывает | Auth-сервер (знает секрет) | Auth-сервер (приватным ключом) |
| Кто проверяет | Любой, у кого есть секрет | Любой с публичным ключом |
| Ротация ключей | Сложно - нужно синхронизировать секрет между всеми сервисами | Просто - новый приватный ключ, публичный через JWKS-эндпоинт |
| Где применять | Монолит, один сервис | Микросервисы, распределённые системы |
// HS256 - подпись и проверка ОДНИМ секретом
const secret = process.env.JWT_SECRET
const token = jwt.sign({ sub: '123' }, secret, { algorithm: 'HS256' })
const payload = jwt.verify(token, secret)
// RS256 - подпись ПРИВАТНЫМ, проверка ПУБЛИЧНЫМ
const privateKey = fs.readFileSync('private.pem')
const publicKey = fs.readFileSync('public.pem')
const token = jwt.sign({ sub: '123' }, privateKey, { algorithm: 'RS256' })
const payload = jwt.verify(token, publicKey)Important
В микросервисной архитектуре используйте RS256. Auth-сервис подписывает токен приватным ключом, а все остальные сервисы проверяют публичным (через JWKS endpoint:
GET /.well-known/jwks.json). Никто кроме auth-сервиса не знает приватный ключ - компрометация одного сервиса не позволяет подделывать токены.
Полный цикл создания и использования токенов:
┌─ ПОЛЬЗОВАТЕЛЬ ─┐ ┌─ AUTH СЕРВЕР ─┐ ┌─ API СЕРВЕР ─┐
│ │ │ │ │ │
│ 1. POST /login │────>│ 2. Проверка │ │ │
│ (email+pass) │ │ пароля │ │ │
│ │ │ │ │ │
│ │ │ 3. Генерация: │ │ │
│ │ │ access_token │ │ │
│ │ │ (RS256, 15m) │ │ │
│ │ │ refresh_token │ │ │
│ │ │ (opaque/256b) │ │ │
│ │ │ │ │ │
│ 4. access_token │<────│ (в теле) │ │ │
│ refresh_token│<────│ (HttpOnly │ │ │
│ (Set-Cookie) │ │ Secure │ │ │
│ │ │ SameSite) │ │ │
│ │ │ │ │ │
│ 5. GET /api/me │─────────────────────────>│ 6. Проверка: │
│ Authorization: │ │ - signature │
│ Bearer <access> │ │ - exp │
│ │ │ - issuer │
│ │ ┌────────────────────│ - audience │
│ │ │ JWKS endpoint │ │
│ │ │ GET /.well-known/ │ │
│ │ │ jwks.json │ │
│ │ └────────────────────│ │
│ │ │ │
│ 7. 200 + данные │<─────────────────────────│ │
│ │ │ │ │ │
│ │ │ │
│ [15 минут спустя - access истёк] │ │ │
│ │ │ │
│ 8. GET /api/me │─────────────────────────>│ 9. 401 │
│ │ │ │ │ │
│ 10. POST/refresh│────>│ 11. Проверка: │ │ │
│ (cookie авто) │ │ refresh_token │ │ │
│ │ │ (из HttpOnly │ │ │
│ │ │ cookie) │ │ │
│ │ │ │ │ │
│ │ │ 12. Новый │ │ │
│ │ │ access_token │ │ │
│ │ │ + при │ │ │
│ │ │ token │ │ │
│ │ │ rotation │ │ │
│ │ │ новый refresh │ │ │
│ │ │ │ │ │
│ 13. access_token │<────│ (в теле) │ │ │
│ refresh_token │<────│ (Set-Cookie) │ │ │
│ │ │ │ │ │
│ 14. GET /api/me │─────────────────────────>│ 15. 200 │
│ (с новым access)│ │ │
└─────────────────┘ └────────────────┘ └───────────────┘
По шагам:
- Пользователь логинится - отправляет email/password
- Auth-сервер проверяет учётные данные
- Генерирует access_token (RS256, 15 минут):
{ sub: userId, iat, exp, iss, aud }+ refresh_token (opaque строка 256 бит случайных данных, или JWT с долгим сроком) - Access token возвращается в теле ответа (frontend хранит в памяти/closure). Refresh token - в HttpOnly Secure SameSite cookie
- Frontend шлёт access token в заголовке
Authorization: Bearer <token>с каждым API-запросом - API-сервер проверяет token: загружает публичный ключ с JWKS-эндпоинта (
/.well-known/jwks.json), проверяет подпись, сверяетexp,iss,aud - Если всё OK - возвращает данные 8-9. Через 15 минут access token истекает - сервер возвращает 401 10-11. Frontend (или axios-интерсептор) молча шлёт POST /refresh - браузер автоматически прикрепляет HttpOnly cookie с refresh token
- Auth-сервер проверяет refresh token. При token rotation - старый refresh token инвалидируется, выдаётся НОВЫЙ refresh token (защита от replay: если украденный refresh использован повторно, инвалидируется вся сессия)
- Новые токены возвращаются: access в теле, refresh в Set-Cookie 14-15. Повтор оригинального запроса с новым access token
Token Rotation - защита от кражи refresh token:
// На сервере: при использовании refresh token
async function refreshTokens(oldRefreshToken) {
const session = await db.findSessionByRefreshToken(oldRefreshToken)
if (!session) {
// Refresh token не найден - возможно, уже был использован злоумышленником
// Инвалидируем ВСЕ сессии пользователя, заставляем перелогиниться
await db.invalidateAllUserSessions(session.userId)
throw new Error('Token reuse detected - all sessions revoked')
}
if (session.refreshTokenUsed) {
// Refresh token использован ПОВТОРНО - это атака!
// Владелец использовал оригинальный, злоумышленник пытается использовать копию
await db.invalidateAllUserSessions(session.userId)
throw new Error('Token replay attack detected')
}
// Помечаем старый как использованный и создаём новый
await db.markRefreshTokenUsed(oldRefreshToken)
const newTokens = await generateTokenPair(session.userId)
return newTokens
}Где хранить access token на фронтенде:
| Место | Защита от XSS | Защита от CSRF | Сохраняется после перезагрузки |
|---|---|---|---|
| HttpOnly cookie | ✅ (JS не читает) | ❌ (нужен SameSite + CSRF token) | ✅ |
| localStorage | ❌ (JS читает) | ✅ (не отправляется авто) | ✅ |
| sessionStorage | ❌ (JS читает) | ✅ | ❌ (только вкладка) |
| Closure variable | ✅ (вне DOM) | ✅ | ❌ |
| Service Worker | ✅ | ✅ | ✅ |
Рекомендация: access token - в памяти (closure), refresh token - в HttpOnly Secure SameSite=Strict cookie. Никогда не храните refresh token в localStorage.
Алгоритм none атака: если сервер доверяет alg из header, злоумышленник может отправить JWT с "alg": "none" и произвольным payload - подпись не проверяется. Всегда игнорируйте alg из header, используйте захардкоженный список допустимых алгоритмов.
9. OAuth 2.0 / PKCE flow
OAuth 2.0 - протокол делегирования авторизации (не аутентификации!). Позволяет приложению получить ограниченный доступ к ресурсам пользователя без передачи пароля.
Authorization Code Flow (рекомендуемый):
- Браузер редиректится на
/authorizeAuthorization Server сclient_id,redirect_uri,scope,state,code_challenge - Пользователь логинится и даёт согласие
- Authorization Server редиректит обратно с
code(authorization code, одноразовый) - Бэкенд обменивает
code + code_verifierнаaccess_token + refresh_tokenчерез POST/token
Почему этот flow безопаснее Implicit: Access token не появляется в URL/fragment браузера, не сохраняется в browser history. Код одноразовый, привязан к client_secret, требует участия бэкенда.
Implicit Flow (DEPRECATED): Токен возвращался прямо во fragment URL (#access_token=...). Уязвим к token leakage через Referer header, browser history, и JS на любой странице где приложение открыто в iframe.
PKCE (Proof Key for Code Exchange):
// Генерация code_verifier и code_challenge на клиенте
const codeVerifier = base64URLEncode(crypto.getRandomValues(new Uint8Array(32)));
const codeChallenge = base64URLEncode(await sha256(codeVerifier));
// code_challenge отправляется в /authorize, code_verifier - в /tokenPKCE предотвращает authorization code interception - даже если код украден, без code_verifier обменять на токен невозможно. Обязателен для мобильных/SPA приложений.
OpenID Connect (OIDC): надстройка над OAuth 2.0 для аутентификации. Добавляет id_token (JWT с информацией о пользователе) и эндпоинт /userinfo.
Important
Для SPA всегда используйте Authorization Code Flow + PKCE, никогда не Implicit Flow. Токены доступа храните в памяти (closure variable), не в localStorage, если есть угроза XSS.
10. Clickjacking + X-Frame-Options
Clickjacking - атака, при которой сайт жертвы встраивается в прозрачный iframe поверх безобидной страницы. Пользователь думает, что кликает по кнопке на видимой странице, а на самом деле кликает по скрытой кнопке в iframe.
Защита через заголовки:
X-Frame-Options: DENY- полностью запрещает встраивание в iframeX-Frame-Options: SAMEORIGIN- разрешает только своим страницамX-Frame-Options: ALLOW-FROM https://trusted.com- устарел, не поддерживается в Chrome
CSP frame-ancestors (современный подход):
Content-Security-Policy: frame-ancestors 'self' https://trusted.comframe-ancestors гибче, чем X-Frame-Options: поддерживает несколько источников, использует тот же синтаксис CSP. Используйте его, а X-Frame-Options оставляйте для старых браузеров как fallback.
Important
Всегда выставляйте оба заголовка.
X-Frame-Options: DENYкак fallback для IE/старых Safari, иframe-ancestors 'none'в CSP для современных браузеров.
11. SRI (Subresource Integrity)
SRI - атрибут integrity на <script> и <link>, содержащий base64-encoded криптографический хеш. Браузер сверяет хеш загруженного ресурса с указанным, и если они не совпадают - отказывается выполнять/применять.
<script src="https://cdn.example.com/lib.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"></script>Зачем: CDN может быть скомпрометирован. Если атакующий заменит jQuery на CDN злонамеренной версией, SRI-хеши не совпадут и скрипт не выполнится.
Генерация хеша:
openssl dgst -sha384 -binary lib.js | openssl base64 -A
# или
cat lib.js | openssl dgst -sha384 -binary | base64Важно: При использовании SRI с кросс-доменным CDN обязателен crossorigin="anonymous" - без него CORS не разрешит браузеру загрузить ресурс для проверки integrity на cross-origin ресурсах.
Info
SRI работает только для ресурсов, которые вы контролируете (знаете хеш версии). Для динамически генерируемого контента на сторонних CDN не применим. Комбинируйте SRI с CSP
require-sri-forдирективой.
12. HTTP Caching: Cache-Control, ETag, stale-while-revalidate
Уровни кеширования: Browser cache (private) → Proxy cache (shared) → CDN cache → Origin server.
Cache-Control директивы:
max-age- сколько секунд ресурс считается свежимs-maxage- то же для shared cache (CDN), перекрывает max-ageno-cache- требует валидацию перед использованием (с ETag/Last-Modified), но кеш можно хранитьno-store- полный запрет кеширования (чувствительные данные)must-revalidate- после истечения max-age обязательна валидация, нельзя отдавать stalepublic- можно кешировать даже аутентифицированные ответыprivate- только browser cache (персональные данные)immutable- ресурс никогда не изменится, браузер не делает conditional request даже при reload
Conditional requests (валидация):
ETag+If-None-Match- сервер генерирует хеш контента. Если не изменился -304 Not Modifiedбез телаLast-Modified+If-Modified-Since- сравнение по времени. Грубее чем ETag (время может совпасть при изменениях)
Клиент: GET /style.css
Сервер: 200 OK
ETag: "abc123"
Cache-Control: max-age=3600
// Через час (max-age истёк):
Клиент: GET /style.css
If-None-Match: "abc123"
Сервер: 304 Not Modified // ресурс не изменился
Cache-Control: max-age=3600 // продляем свежесть
stale-while-revalidate: Разрешает отдавать stale (устаревший) контент клиенту, пока асинхронно запрашивается свежий. Отличный UX компромисс: мгновенный ответ + обновление в фоне.
Cache-Control: max-age=3600, stale-while-revalidate=86400Important
Для статических ресурсов с хешем в имени файла (
app.abc123.js) используйтеCache-Control: max-age=31536000, immutable. Для HTML-страниц -no-cacheс ETag, чтобы всегда валидировать, но экономить трафик. Без immutable браузер делает conditional request при Navigate Reload даже для хешированных ресурсов.
13. HTTP/1.1 vs HTTP/2 vs HTTP/3 (QUIC)
HTTP/1.1 проблемы:
- Head-of-Line (HOL) blocking на уровне соединения - один медленный запрос блокирует все следующие
- 6-8 параллельных соединений на домен (browser connection limit) для обхода HOL - создаёт overhead
- Текстовый протокол, многословные заголовки (несжатые повторы Cookie, User-Agent)
HTTP/2 (SPDY-основа):
- Мультиплексирование (multiplexing): множество стримов в одном TCP-соединении. Запросы не ждут друг друга, ответы приходят в любом порядке
- Header compression (HPACK): заголовки сжимаются и кешируются между запросами, особенно эффективно для повторяющихся Cookie
- Server Push: сервер может отправить ресурсы до их запроса (CSS, JS), но на практике сложен и деприкейтится в Chrome
- Бинарный протокол: более эффективный парсинг, меньше байт
- Stream prioritization: клиент может задавать приоритеты стримов (weight, dependency), браузер управляет порядком отдачи
HTTP/2 проблема: TCP HOL blocking - потеря одного TCP-пакета блокирует все стримы (т.к. TCP гарантирует порядок доставки). HTTP/3 решает это полностью, переходя на UDP.
HTTP/3 (QUIC):
- Основан на UDP с QUIC вместо TCP+TLS
- Полностью устраняет HOL blocking - потеря пакета влияет только на один стрим
- 0-RTT возобновление соединения (для повторных визитов)
- Встроенное шифрование (TLS 1.3 встроен в QUIC)
- Connection migration - соединение не рвётся при смене IP (WiFi → 4G)
- Улучшенная congestion control: QUIC реализует собственную логику управления потоком в userspace, не зависит от ОС. Поддерживает BBRv2 и другие современные алгоритмы
- Faster handshake: QUIC объединяет транспортный и криптографический handshake в один этап. TLS 1.3 handshake идёт параллельно с установлением QUIC соединения
- No middlebox interference: TCP часто ломается из-за агрессивных NAT и фаерволов (особенно в мобильных сетях). UDP более стабилен, QUIC сам обеспечивает надёжность
# Альтернативный сервис - сервер сообщает о доступности HTTP/3
Alt-Svc: h3=":443"; ma=86400QUIC tradeoffs:
- UDP блокируется некоторыми корпоративными фаерволами (HTTP/3 fallback на TCP)
- Серверная инфраструктура должна поддерживать UDP (не все load balancers умеют)
- Отладка сложнее - стандартные TCP-тулзы (tcpdump, Wireshark) не работают без ключей шифрования
Info
HTTP/2 даёт основной выигрыш без изменения кода - достаточно включить HTTPS. HTTP/3 требует поддержки UDP на уровне инфраструктуры и даёт прирост на нестабильных сетях и мобильных устройствах. По данным Google, HTTP/3 улучшает p90 latency на 3-10% в мобильных сетях.
14. Cookies: Secure, HttpOnly, SameSite, Domain, Path
Cookie атрибуты:
Secure: Кука отправляется только по HTTPS. Критически важно для кук с sensitive данными. Без этого атакующий через MITM на публичной WiFi может перехватить куку.
HttpOnly: Кука недоступна через document.cookie в JavaScript. Защита от XSS - даже если атакующий инжектирует скрипт, он не может украсть такую куку.
SameSite:
Strict- кука никогда не отправляется при кросс-сайтовых запросах, даже при клике по ссылке. Максимальная защита от CSRF, но пользователь не будет аутентифицирован при переходе с внешнего сайтаLax- кука отправляется при top-level navigation (ссылках) GET-методом, но не в POST-формах и iframe. Оптимальный баланс UX и безопасности, дефолт в ChromeNone- кука отправляется во всех кросс-сайтовых запросах. Обязательно требуетSecure
Domain: Если не указан, кука привязана к точному хосту. Domain=example.com делает куку доступной на всех поддоменах (включая app.example.com, api.example.com). Чем уже scope, тем безопаснее.
Path: Ограничивает куку определённым путём на сервере. Path=/admin - кука отправляется только на /admin/*. Не является security mechanism - Same-Origin Policy работает на уровне origin, не path.
Set-Cookie: session_id=abc123; Secure; HttpOnly; SameSite=Lax; Path=/
Set-Cookie: refresh_token=xyz789; Secure; HttpOnly; SameSite=Strict; Path=/api/authCookie prefixes (дополнительный уровень защиты):
__Host-- кука должна быть Secure, без Domain атрибута, Path=/__Secure-- кука должна быть Secure
Set-Cookie: __Host-session=abc123; Secure; HttpOnly; SameSite=Lax; Path=/Important
Разделяйте access token и refresh token: access - SameSite=Lax для удобства (пользователь видит себя залогиненным при переходе по ссылке), refresh - SameSite=Strict на узкий Path=/api/auth (только для token refresh эндпоинта, атакующий не может подделать запрос на обновление).
15. Service Workers: lifecycle, caching strategies, offline
Service Worker (SW) - JS-файл, работающий в фоновом потоке браузера, выступающий как программируемый прокси между страницей и сетью. Не имеет доступа к DOM, работает только на HTTPS (кроме localhost).
Жизненный цикл:
- Registration:
navigator.serviceWorker.register('/sw.js') - Install: событие
install- идеальное время для предкеширования критических ресурсов.event.waitUntil()продлевает фазу install - Waiting: SW установлен, но старый SW активен.
self.skipWaiting()форсирует немедленную активацию - Activate: событие
activate- очистка старых кешей.clients.claim()заставляет SW контролировать все открытые страницы без перезагрузки - Fetch: SW перехватывает все сетевые запросы страницы - может вернуть из кеша, модифицировать запрос, или пропустить в сеть
// sw.js - Cache First стратегия для статики
const CACHE_NAME = 'v2';
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache =>
cache.addAll(['/', '/styles.css', '/app.js'])
)
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then(keys =>
Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)))
)
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then(cached =>
cached || fetch(event.request)
)
);
});Стратегии кеширования:
- Cache First: статика (шелы приложения)
- Network First: API-ответы, которые важна свежесть
- Stale While Revalidate: UX баланс - мгновенный кеш + фоновое обновление
- Network Only: некешируемые данные (платёжные)
- Cache Only: оффлайн-fallback страницы
Important
Service Worker - мощный инструмент, но сломанный SW может “закирпичить” сайт для пользователя (старый код кеширует невалидный ответ, и никакие перезагрузки не помогут). Всегда: (1) версионируйте кеши, (2) оставляйте no-cache для HTML, (3) тестируйте обновление SW между версиями, (4) реализуйте кнопку “force update” при детекте версионного несоответствия.
16. CDN: edge servers, cache invalidation, edge computing
CDN (Content Delivery Network) - географически распределённая сеть серверов (edge servers), кеширующих контент ближе к пользователю.
Как работает маршрутизация:
- DNS резолвит домен на IP ближайшего edge (anycast routing или geo-DNS)
- Edge-сервер проверяет кеш: HIT (возвращает из кеша) или MISS (запрашивает с origin, кеширует, возвращает)
- Контент распределяется по tiered cache: Edge → Regional → Origin - уменьшает нагрузку на origin
Cache invalidation стратегии:
- Purge: активное удаление из кеша (через API, обычно ~несколько секунд).
curl -X PURGE https://cdn.example.com/image.jpg - TTL-based: контент живёт в кеше заданное время, затем автоматически запрашивается свежий
- Cache key versioning:
style.v2.css- смена имени при изменении, старый URL истечёт сам - Surrogate keys (Fastly): тэговое инвалидирование - инвалидировать все ресурсы с тэгом
post-42
Типичная CDN-архитектура для SPA:
# HTML - не кешируем на CDN (динамический контент)
Cache-Control: no-cache
CDN-Cache-Control: no-cache
# Статика с хешем - кешируем навсегда
Cache-Control: public, max-age=31536000, immutable
# API ответы - короткий TTL
Cache-Control: public, max-age=60, s-maxage=300
Surrogate-Key: post-42 category-newsEdge computing: Современные CDN позволяют запускать код на edge (Cloudflare Workers, Fastly Compute, Lambda@Edge) - A/B тестирование, аутентификация, гео-редиректы выполняются максимально близко к пользователю.
Important
Ключевой принцип: HTML страница - no-cache (всегда валидировать свежесть ETag’ом), статические ассеты с content-hash в имени - immutable + долгий max-age. Смешивание этих подходов - основная причина проблем с деплоем (“старый HTML ссылается на старый JS, новый JS уже задеплоен”).
17. DNS: resolution, record types, dns-prefetch, DoH
Процесс разрешения DNS:
- Браузер проверяет свой DNS-кеш (chrome://net-internals/#dns)
- Проверка системного кеша ОС
- Проверка файла
/etc/hosts(или эквивалент) - Запрос к DNS-резолверу (обычно роутер или DNS-сервер провайдера)
- Резолвер рекурсивно опрашивает DNS-иерархию:
- Root DNS сервер → возвращает TLD-сервер (
.com) - TLD-сервер → возвращает authoritative сервер домена
- Authoritative сервер → возвращает IP-адрес
- Root DNS сервер → возвращает TLD-сервер (
Типы записей DNS:
- A - IPv4 адрес:
example.com. 300 IN A 93.184.216.34 - AAAA - IPv6 адрес
- CNAME - alias на другое имя:
www.example.com → example.com - MX - почтовый сервер
- TXT - произвольный текст (SPF, DKIM, domain verification)
- NS - authoritative name server
- SOA - Start of Authority, информация о зоне (serial, refresh, retry intervals)
- SRV - service location (используется для VoIP, XMPP, и discovery сервисов)
Время резолва и оптимизации:
dns-prefetchв<link rel="dns-prefetch" href="//api.example.com">- браузер начинает DNS-резолвинг заранее, до того как понадобитсяpreconnectвключает DNS + TCP + TLS- Каждый новый домен в критическом пути стоит ~50-100ms на резолвинг
DNS over HTTPS (DoH): DoH шифрует DNS-запросы через HTTPS, предотвращая:
- Просмотр DNS-трафика провайдером (privacy)
- DNS spoofing и manipulation (security)
- Блокировку сайтов на уровне DNS (censorship circumvention)
// Браузерная настройка DoH (Chrome/Firefox)
// Настройки → Privacy & Security → DNS over HTTPS
// Или через DNS-провайдера: Cloudflare (1.1.1.1), Google (8.8.8.8)DoH tradeoffs:
- Плюсы: приватность, защита от MITM, обход DNS-блокировок
- Минусы: корпоративные DNS-фильтры обходятся (проблема для IT-департаментов), дополнительная задержка HTTPS handshake поверх DNS, зависимость от конкретного DoH-провайдера
DNS over TLS (DoT): Альтернатива DoH - шифрует DNS через TLS на порту 853. Работает на уровне ОС (не браузера), но менее распространён.
Info
Минимизируйте количество уникальных доменов на странице, используйте
preconnectдля критических third-party доменов. DoH повышает приватность, но не заменяет HTTPS - это защита только DNS-запросов, не всего трафика.
18. REST vs GraphQL vs gRPC
REST (Representational State Transfer):
- Множество эндпоинтов, каждый возвращает фиксированную структуру данных
- Under-fetching: данных недостаточно → запросы к нескольким эндпоинтам
- Over-fetching: данных слишком много, клиент использует только часть полей
- HTTP-кеширование (CDN, браузер) работает из коробки
- Простая отладка через curl, browser devtools
GraphQL:
- Один эндпоинт
/graphql, клиент запрашивает ровно то что нужно - Решает over-fetching и under-fetching - один запрос → ровно нужные данные
- Сложный кеширование на клиенте (Apollo InMemoryCache, нормализация по
__typename+id) - N+1 проблема на сервере требует DataLoader на бэкенде
- Один большой query со множеством резолверов может быть медленнее REST-эндпоинта с SQL JOIN
- Сложнее CDN-кеширование (POST-запросы по умолчанию не кешируются, нужен persisted queries/Automatic Persisted Queries)
gRPC (gRPC Remote Procedure Calls):
- Protocol Buffers (бинарный формат) - компактнее JSON
- HTTP/2 транспорт с мультиплексированием стримов
- Стриминг из коробки (unary, server-streaming, client-streaming, bidi)
- Строгая контрактная типизация -
.protoфайлы генерируют клиентский и серверный код - В браузере ограничен: требует gRPC-Web прокси (Envoy), так как браузерный fetch не поддерживает полный HTTP/2
// Пример proto-файла
service UserService {
rpc GetUser (UserRequest) returns (UserResponse);
}
message UserRequest {
int32 id = 1;
}Info
Выбор для фронтенда: REST для публичных API и контента (кеширование, простота), GraphQL для сложных UI с вариативными данными (админ-панели, дашборды), gRPC для внутренних сервисов и высоконагруженных real-time приложений (через gRPC-Web для браузера).
19. WebSockets: handshake, frames, reconnection, vs SSE
WebSocket - полнодуплексный протокол поверх TCP для постоянного соединения через один сокет. Инициируется HTTP-апгрейдом.
Handshake (101 Switching Protocols):
Клиент:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Сервер:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=Sec-WebSocket-Accept вычисляется как base64(sha1(Sec-WebSocket-Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")). Эта константа гарантирует, что сервер понимает WebSocket протокол (а не случайный HTTP-сервер).
Фреймы: Сообщения разбиваются на фреймы. Каждый фрейм имеет opcode (text, binary, ping, pong, close), FIN-бит (последний фрейм сообщения), MASK-бит (клиент→сервер ВСЕГДА маскируются с XOR, защита от cache poisoning на прокси).
Reconnection strategies:
class RobustWebSocket {
constructor(url, maxRetries = 5) {
this.url = url;
this.retries = 0;
this.maxRetries = maxRetries;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onclose = (e) => {
if (e.code !== 1000) { // не нормальное закрытие
const delay = Math.min(1000 * 2 ** this.retries, 30000); // exponential backoff
setTimeout(() => this.connect(), delay);
this.retries++;
}
};
}
}WebSockets vs SSE: WebSockets - bidirectional, сложнее в масштабировании (sticky sessions), собственный протокол. SSE - только сервер→клиент, работает поверх HTTP (прокси/CDN понимают), авто-реконнект из коробки.
Масштабирование: WebSocket держит постоянное TCP-соединение. При горизонтальном масштабировании нужен sticky session (балансер направляет конкретного пользователя на тот же сервер) или Redis pub/sub для кросс-серверной коммуникации.
Important
WebSocket не дружит с HTTP/2 напрямую (для WS используют HTTP/1.1 апгрейд). HTTP/3 решает это лучше. Для надёжных real-time приложений всегда проектируйте reconnection с exponential backoff + jitter и message queue на время разрыва - не теряйте сообщения.
20. Server-Sent Events (SSE)
SSE - однонаправленный (сервер → клиент) протокол для стриминга событий через HTTP. Клиент подключается к эндпоинту, сервер держит соединение открытым и отправляет текстовые события.
Клиент:
GET /events HTTP/1.1
Accept: text/event-stream
Сервер:
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
data: {"price": 100.50}
event: alert
data: {"message": "Server restarting"}
id: 42
data: {"status": "ok"}
: это комментарий (игнорируется клиентом, используется для keepalive)
Когда SSE вместо WebSockets:
- Данные текут только в одном направлении (тикеры, ленты новостей, логи)
- Нужна простая интеграция с HTTP-инфраструктурой (прокси, CDN, аутентификация через куки)
- Требуется авто-реконнект - EventSource API сам переподключается при разрыве, сохраняя Last-Event-Id для возобновления стрима
Ограничения SSE:
- Максимум 6 подключений на домен в HTTP/1.1 (разделяется с обычными запросами). HTTP/2 снимает это ограничение через мультиплексирование
- Нет бинарных данных - только UTF-8 текст (можно кодировать в base64, но накладно)
- Нет кастомных заголовков при подключении (EventSource API не поддерживает)
const events = new EventSource('/api/events');
events.addEventListener('alert', (e) => {
const data = JSON.parse(e.data);
showAlert(data.message);
});
events.onerror = () => {
// EventSource автоматически попробует переподключиться
// с заголовком Last-Event-Id для возобновления
};Info
SSE отлично подходит для real-time фидов и прогресс-баров (загрузка файла, статус сборки). EventSource API встроен в браузер, не требует library. Комбинируйте SSE (чтение стрима) + обычный fetch POST (отправка действий) вместо WebSocket для многих use-case’ов.
21. TCP handshake: SYN, SYN-ACK, ACK, TFO
3-way handshake:
- Клиент → Сервер:
SYN(seq=x, флаг SYN) - Сервер → Клиент:
SYN-ACK(seq=y, ack=x+1, флаги SYN, ACK) - Клиент → Сервер:
ACK(seq=x+1, ack=y+1, флаг ACK)
Почему именно 3-way: TCP требует согласования начальных sequence numbers для обеих сторон. Каждая сторона подтверждает, что она может и отправлять и принимать:
- После шага 2: клиент знает, что сервер жив и принимает
- После шага 3: сервер знает, что клиент жив и принимает
Двух шагов недостаточно - если клиент отправляет SYN, сервер отвечает SYN-ACK и считает соединение установленным, но клиент мог уже отвалиться (полуоткрытое соединение). Третий ACK подтверждает, что клиент всё ещё на связи.
SYN flood атака: Атакующий отправляет множество SYN, игнорирует SYN-ACK. Сервер выделяет ресурсы под полуоткрытые соединения, исчерпывая connection queue. Защита: SYN cookies - сервер не хранит состояние, а кодирует информацию в sequence number.
TCP Fast Open (TFO): TFO позволяет отправлять данные уже в SYN-пакете, минуя необходимость ждать завершения handshake. Это сокращает задержку на 1 RTT для повторных соединений с тем же сервером.
# Обычный TCP: SYN → SYN-ACK → ACK → DATA (3 RTT до данных)
# С TFO: SYN + DATA → SYN-ACK + DATA → ACK (1 RTT до данных)
Как работает TFO:
- При первом соединении клиент получает TFO cookie от сервера (в SYN-ACK)
- При повторном соединении клиент включает cookie в SYN-пакет вместе с данными
- Сервер валидирует cookie и сразу начинает обрабатывать данные, не дожидаясь ACK
Ограничения TFO:
- Работает только для повторных соединений (нужен cookie с предыдущего визита)
- Поддержка зависит от ОС (Linux 3.6+, macOS 10.11+, Windows - ограниченная)
- Размер данных в SYN ограничен (~16KB, зависит от MSS)
- Не все серверы поддерживают TFO
TLS добавляет 1-2 RTT поверх TCP handshake. Общее время на установку соединения:
- HTTP/1.1 HTTPS: TCP (1 RTT) + TLS 1.2 (2 RTT) - итого 3 RTT до первого байта
- HTTP/1.1 HTTPS с TLS 1.3: TCP (1 RTT) + TLS 1.3 (1 RTT) - итого 2 RTT
- HTTP/3: QUIC (0-1 RTT, UDP без TCP handshake) - итого 0-1 RTT
- С TFO + TLS 1.3: TCP (0 RTT для данных) + TLS 1.3 (1 RTT) - итого 1 RTT
Important
Каждый RTT стоит ~20-200ms в зависимости от географической удалённости. TCP+TLS handshake - значительная часть TTFB.
preconnect, keep-alive и HTTP/2 мультиплексирование критичны для уменьшения этой задержки.
22. Keep-Alive + connection pooling
Keep-Alive (HTTP persistent connection): В HTTP/1.0 соединение закрывалось после каждого запроса-ответа. Keep-Alive позволяет переиспользовать одно TCP-соединение для нескольких запросов - не надо повторять TCP+TLS handshake для каждого ресурса.
Connection: keep-alive
Keep-Alive: timeout=5, max=1000Как работает connection pooling в браузере:
- Браузер открывает до 6 соединений на домен (HTTP/1.1)
- Каждое соединение переиспользуется (keep-alive) для нескольких запросов
- Если запросов больше 6 - они ставятся в очередь и ждут освобождения соединения
- В HTTP/2 одно соединение на домен, мультиплексирование убирает необходимость в пуле
Проблемы keep-alive без правильного таймаута:
- Сервер держит открытыми тысячи idle соединений → memory pressure
- Load balancer должен уметь отслеживать idle timeout и закрывать их чисто
- На мобильных устройствах каждое открытое TCP-соединение потребляет радио-батарею
Info
HTTP/2 решает проблему 6-соединений одним мультиплексируемым соединением. Но если доменов несколько - для каждого создаётся отдельное HTTP/2 соединение.
preconnectвсё ещё полезен для установки соединения с критическими third-party доменами заранее.
23. Long Polling vs Short Polling vs WebSockets vs SSE
Short Polling: Клиент регулярно опрашивает сервер с фиксированным интервалом.
setInterval(async () => {
const data = await fetch('/api/updates').then(r => r.json());
updateUI(data);
}, 5000);Минусы: создаёт нагрузку на сервер даже когда данных нет, задержка до 5 секунд.
Long Polling: Клиент делает запрос, сервер держит соединение открытым пока не появятся данные (или timeout), затем клиент сразу делает новый запрос.
async function longPoll() {
const data = await fetch('/api/updates?since=' + lastId).then(r => r.json());
updateUI(data);
longPoll(); // немедленно новый запрос
}Плюсы: почти real-time, не создаёт лишних запросов. Минусы: каждый запрос - новый TCP + HTTP overhead, сложно масштабировать (сервер держит много открытых соединений).
WebSockets: Постоянное полнодуплексное TCP-соединение. Минимальный overhead после установки, bidirectional, подходит для чатов и игр.
SSE: Однонаправленный (сервер→клиент) стрим поверх HTTP. Автореконнект, простота.
Сравнительная таблица выборов:
| Критерий | Short Polling | Long Polling | WebSockets | SSE |
|---|---|---|---|---|
| Направление | Client→Server | Client→Server | Bidirectional | Server→Client |
| Задержка | Высокая | Низкая | Минимальная | Низкая |
| Масштабирование | Простое | Сложное | Сложное | Среднее |
| HTTP/2 совместимость | Да | Частично | Нет (отдельный протокол) | Да |
| Авто-реконнект | Н/П | Ручной | Ручной | Встроен |
Info
Практическое правило: SSE для дашбордов, лент, уведомлений где данные идут сервер→клиент. WebSockets для чатов, коллаборативного редактирования, игр где bidirectional критичен. HTTP polling - для ситуаций где инфраструктура не поддерживает persistent connections.
24. Critical Rendering Path: DOM → CSSOM → Render Tree → Layout → Paint → Composite
Critical Rendering Path (CRP) - последовательность шагов, которые браузер выполняет для превращения HTML, CSS и JS в пиксели на экране.
Шаги:
- DOM (Document Object Model): Браузер парсит HTML байты → символы → токены → узлы → DOM-дерево. Парсинг инкрементальный и не блокируется ожиданием картинок, но блокируется синхронными скриптами
- CSSOM (CSS Object Model): Парсинг CSS в дерево стилей. CSS Parser строже чем HTML - ошибка не ломает парсинг, просто пропускает правило. CSS render-blocking: браузер не начнёт рендеринг без CSSOM
- Render Tree: Объединение DOM + CSSOM, только видимые элементы (
display: noneисключены,visibility: hiddenвключены) - Layout (Reflow): Расчёт геометрии - координаты и размеры каждого элемента в зависимости от viewport
- Paint: Отображение (растеризация) пикселей - цвет, тени, фон, текст
- Composite: Объединение слоёв (изолированных bitmap) в финальное изображение - происходит на GPU
<!-- CSS - render-blocking -->
<link rel="stylesheet" href="styles.css">
<!-- JS - parser-blocking (и render-blocking через ожидание CSSOM) -->
<script src="app.js"></script>
<!-- Атрибуты для неблокирующей загрузки -->
<script async src="analytics.js"></script> <!-- выполняется сразу после загрузки -->
<script defer src="app.js"></script> <!-- выполняется после DOM-парсинга, до DOMContentLoaded -->Оптимизация CRP:
- Critical CSS inline в
<head>, остальные стили асинхронно черезmedia="print" onload="this.media='all'" deferдля не-критических скриптов- Минимизировать глубину DOM (парсинг быстрее, селекторы быстрее)
- Preload критических ресурсов:
<link rel="preload" href="font.woff2" as="font" crossorigin>
Important
Critical CSS inline делает First Paint быстрее (не нужно ждать внешние CSS файлы), но увеличивает HTML размер. Обычно применяют для above-the-fold стилей (первые ~14KB - размер одного congestion window в TCP). Для современных SPA подход может отличаться - SSR даёт HTML с контентом, стили стримятся.
25. Repaint vs Reflow: causes, avoidance
Reflow (Layout): Браузер пересчитывает позиции и размеры элементов в документе. Это самая дорогая операция, так как перерасчёт одного элемента может каскадно затронуть всё дерево.
Что вызывает Reflow:
- Изменение размеров/позиции (width, height, margin, padding, top, left)
- Изменение содержимого (текст, картинки)
- Добавление/удаление DOM-элементов
- Изменение размера окна
- Изменение шрифта
- Чтение геометрических свойств после записи без батчинга (см. Layout Thrashing)
Repaint: Перерисовка пикселей элемента без изменения геометрии. Дешевле, но всё ещё затратно.
Что вызывает Repaint (но не Reflow):
color,background-color,box-shadow,outlinevisibilityborder-color
Свойства, вообще не затрагивающие Render Tree (только Composite):
transform(translate, rotate, scale)opacity(только этот слой composite)filter(при определённых условиях GPU)
/* ❌ Плохо - вызывает layout + paint */
.box { top: 10px; background: red; }
/* ✓ Хорошо - только composite, GPU-ускоренно */
.box { transform: translateY(10px); }/* contain: строгий layout/reflow изолируется внутри элемента */
.widget {
contain: layout style; /* reflow внутри .widget не влияет на остальную страницу */
}Important
Для анимаций используйте ТОЛЬКО
transformиopacity. Они работают на compositor thread (GPU), не блокируют main thread, никогда не вызывают reflow/repaint. Любые другие свойства (width,top,margin) при анимации вызывают layout на каждом кадре = джанк.
26. Browser Event Loop + rendering timing: macrotasks, microtasks, rAF positioning
Event Loop - бесконечный цикл, координирующий выполнение JS, рендеринг и другие задачи в браузере. Архитектура: один main thread для JS и большей части рендеринга.
Очереди и приоритет:
- Macrotasks (Task queue):
setTimeout,setInterval,I/O, UI rendering,postMessage,MessageChannel. Одна macrotask за итерацию event loop - Microtasks:
Promise.then/catch/finally,MutationObserver,queueMicrotask(). Выполняются ВСЕ до перехода к следующей macrotask или рендеру - requestAnimationFrame (rAF): Выполняется ПЕРЕД рендерингом, синхронизирован с частотой обновления экрана (60fps → каждые ~16.7ms)
- Render: Браузер решает, нужен ли рендер (есть ли изменения DOM/CSS), и если да - выполняет style → layout → paint → composite
Порядок в одной итерации:
macrotask → ВСЕ microtasks → rAF callbacks → render → requestIdleCallback → macrotask → ...
requestAnimationFrame(() => console.log('2. rAF - before paint'));
setTimeout(() => console.log('4. setTimeout'), 0);
Promise.resolve().then(() => console.log('1. microtask'));
console.log('0. sync');
// Вывод: 0, 1, 2, (render), 4Важное следствие: Если macrotask плодит microtasks (рекурсивные Promise.then), рендеринг НЕ ПРОИЗОЙДЁТ пока очередь microtasks не опустеет. Можно случайно заблокировать UI, бесконечно добавляя microtasks.
Important
requestAnimationFrame- единственный гарантированный способ выполнить код перед рендерингом. Идеально для: чтения layout-свойств (все DOM изменения уже применены), обновления анимаций, батчинга DOM-изменений в одном кадре. Не используйтеsetTimeout(0)для этого - он может выполниться до или после рендера непредсказуемо.
27. requestAnimationFrame vs requestIdleCallback vs setTimeout
requestAnimationFrame (rAF):
- Выполняется перед каждым repaint, синхронизирован с vsync
- Гарантированно вызывается ~60 раз в секунду (при активной вкладке)
- На неактивных вкладках приостанавливается (экономия батареи)
- Используется для: анимаций, обновления canvas, батчинга DOM-операций перед рендером
requestIdleCallback (rIC):
- Выполняется когда браузер НЕ занят (между кадрами, при простое)
- Получает
IdleDeadlineсtimeRemaining()- сколько миллисекунд осталось до следующего кадра - Низкий приоритет, НЕ подходит для изменения DOM (может вызвать непредсказуемую задержку)
- Используется для: аналитики, отправки логов, префетчинга, не-критических обновлений
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0 && tasks.length > 0) {
processTask(tasks.shift());
}
}, { timeout: 2000 }); // если idle нет 2 секунды - выполнить принудительноsetTimeout(cb, 0):
- Добавляет macrotask, выполнится после текущей macrotask и ВСЕХ microtasks
- Не гарантирует время выполнения - реальная задержка ≥4ms для вложенных вызовов (clamping), но не синхронизирован с рендером
- Используется для: разбиения длинных задач (yield to event loop), defer-а выполнения
Сравнение:
| rAF | rIC | setTimeout | |
|---|---|---|---|
| Синхронизация с рендером | Перед рендером | В idle | Не синхронизирован |
| Приоритет | Высокий | Низкий | Средний |
| Анимации | ✓ Идеально | ✗ | ✗ (джиттер) |
| DOM изменения | ✓ | ✗ Не рекомендуется | ✓ |
| Фоновая работа | ✗ | ✓ Идеально | Средне |
Info
Для анимаций используйте rAF. Для разбиения длинных задач на чанки по 50ms (INP-friendly) используйте
scheduler.postTask()илиscheduler.yield()- новые API, позволяющие браузеру приоритизировать. Для логов/аналитики идеально подходит rIC.
28. Layout Thrashing + FastDOM pattern
Layout Thrashing - паттерн когда JS чередует чтение layout-свойств и запись стилей, заставляя браузер делать reflow на каждой итерации.
// ❌ Layout Thrashing - reflow на каждой итерации
for (let i = 0; i < elements.length; i++) {
const height = elements[i].offsetHeight; // READ → layout
elements[i].style.height = height + 10 + 'px'; // WRITE → invalidates layout
}Браузер вынужден синхронно пересчитывать layout после каждой записи, так как следующий read требует актуальных значений.
FastDOM паттерн: Разделяем чтения и записи - сначала все read, потом все write.
// ✓ FastDOM - один reflow
const heights = [];
for (const el of elements) {
heights.push(el.offsetHeight); // ТОЛЬКО READ
}
for (let i = 0; i < elements.length; i++) {
elements[i].style.height = heights[i] + 10 + 'px'; // ТОЛЬКО WRITE
}Как детектить:
- Chrome DevTools → Performance → запишите профиль → ищите фиолетовые полосы “Layout” с маркером “Forced reflow”
- В коде: между каждым
style.xxx = ...и следующимoffsetHeight/clientWidth/getComputedStyle()вставлен ли write? Если да - вероятен thrashing
Почему это важно: Каждый forced reflow может занимать десятки-сотни миллисекунд на сложных страницах. При 60fps бюджет одного кадра - 16.7ms, из которых JS должен занять ~10ms. Thrashing легко сжирает весь бюджет и вызывает джанк.
Important
Используйте
requestAnimationFrameдля батчинга write-операций. Все read делайте синхронно в начале кадра, все write - в колбеке rAF. Либо используйте библиотеки (fastdom, React с Virtual DOM) которые делают это автоматически.
29. Composite Layers: GPU acceleration, will-change
Composite-слой - изолированное растровое изображение (битмап), хранящееся в GPU-памяти. Браузер может композитить слои, не затрагивая main thread.
Как создаются слои:
- 3D CSS свойства:
transform: translateZ(0),translate3d(0,0,0) <video>,<canvas>,<iframe>will-changeсвойство - явное указание браузеру, что элемент будет анимироватьсяopacityанимация + отдельный stacking contextposition: fixedилиoverflow: scrollчасто создают слои
translateZ(0) hack: Форсирует создание composite-слоя. Работает, но это хак, не семантичный, и создаёт лишние слои без причины, что расходует GPU-память.
/* ✓ Правильно - явно указываем что изменится */
.slide-in {
will-change: transform;
}
/* Лучше применять will-change непосредственно перед анимацией и убирать после */Как исследовать слои: Chrome DevTools → Rendering → Layer borders (показывает оранжевые границы слоёв). Layers panel в DevTools показывает все слои, их размер и причину создания.
Проблемы избыточных слоёв:
- Каждый слой потребляет GPU-память (растровое изображение размером с элемент)
- Много слоёв на мобильных = краш вкладки из-за исчерпания GPU-памяти
- Слишком большие слои (>3000×3000px) дороги для композитинга
Info
Эмпирическое правило: явно создавайте слой только для анимируемых элементов через
will-change. Не используйтеtranslateZ(0)глобально на все элементы - это увеличивает memory footprint без пользы. Для скролл-контейнеров с частыми обновлениями -will-change: scroll-position.
30. Core Web Vitals: LCP, INP, CLS overview
Core Web Vitals - метрики Google, измеряющие реальный пользовательский опыт. Влияют на SEO (page experience ranking signal).
LCP (Largest Contentful Paint): Измеряет время загрузки самого большого видимого элемента (hero image, video poster, текстовый блок). Хорошо: ≤2.5s. Плохо: >4.0s. Основная метрика perceived loading speed.
INP (Interaction to Next Paint): ЗАМЕНИЛ FID в марте 2024. Измеряет задержку между взаимодействием пользователя (клик, тап, нажатие клавиши) и следующим отрисованным кадром для ВСЕХ взаимодействий на странице, а не только первого. Берётся 75-й перцентиль худших взаимодействий. Хорошо: ≤200ms. Плохо: >500ms.
CLS (Cumulative Layout Shift): Измеряет визуальную стабильность - сумму всех неожиданных сдвигов контента за время жизни страницы. Максимальный счёт одной сессии сдвигов - 5 секунд (session window). Хорошо: ≤0.1. Плохо: >0.25.
Почему заменили FID на INP: FID измерял только задержку ПЕРВОГО взаимодействия и только input delay (время до начала обработки), игнорируя processing time и presentation delay. INP охватывает всю цепочку: input delay + processing time + rendering delay, для всех взаимодействий.
// Измерение INP через PerformanceObserver
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.interactionId > 0) {
const duration = entry.processingEnd - entry.startTime;
console.log(`${entry.name}: ${duration}ms`);
}
}
}).observe({ type: 'event', buffered: true, durationThreshold: 16 });Important
Core Web Vitals - это не просто чек-лист, а индикаторы UX. LCP < 2.5s требует быстрого TTFB + раннюю загрузку главного контента. INP < 200ms требует, чтобы обработчики событий и рендер-апдейты укладывались в кадр. CLS < 0.1 требует резервирования места под весь динамический контент.
31. LCP optimization: 4 phases, preload, fetchpriority, fonts
LCP измеряет когда largest above-the-fold элемент становится видимым. Типичные LCP-элементы: <img>, <image> (SVG), <video> poster, фоновое изображение через url(), текстовый блок (p, h1).
Что влияет на LCP (4 фазы):
- TTFB (Time to First Byte): серверная задержка - до ~40% времени LCP
- Load delay: время от TTFB до начала загрузки LCP-ресурса - улучшается preload, inline critical CSS
- Load time: время загрузки ресурса - размер файла, сжатие, CDN
- Render delay: время от загрузки до рендера - если LCP-элемент - текст, нужен font block period; если картинка - decoding
Оптимизация LCP:
<!-- Preload LCP-изображения -->
<link rel="preload" as="image" href="hero.webp" imagesrcset="hero-2x.webp 2x">
<!-- Используем fetchpriority для LCP-изображения -->
<img src="hero.webp" fetchpriority="high" loading="eager" decoding="sync">Стратегии:
- LCP-изображение должно загружаться с HTML, не через JS (сразу в
<img src>, не через React state) - Используйте современные форматы (WebP, AVIF) - меньше байт на то же качество
- Responsive images с
srcset- не грузить десктопную 2x картинку на маленький экран - Inline небольшие изображения в data-URI если они критичны
- Не лейзилоадьте LCP-изображение (
loading="lazy"на нём = плохо) - Серверный рендеринг (SSR) - пользователь видит контент быстрее
LCP и веб-шрифты: Текстовый LCP-блок может задерживаться загрузкой шрифтов. Используйте font-display: swap + preload для шрифтов:
@font-face {
font-family: 'Inter';
src: url('inter.woff2') format('woff2');
font-display: swap; /* Показать fallback шрифт сразу, заменить когда загрузится */
}Important
LCP - это НЕ просто “картинка быстро загрузилась”. Это метрика отражающая всю цепочку от соединения с сервером до отрисовки главного элемента. Для сложных SPA без SSR LCP будет плохим почти всегда, так как JS должен выполниться прежде чем хоть что-то появится.
32. INP: what it replaced FID, how to measure/improve
INP - метрика отзывчивости, заменившая FID в марте 2024. Измеряет latency ВСЕХ взаимодействий (не только первого) на протяжении всего визита.
Измеряемые взаимодействия: click, tap, key press (не scroll и не hover).
Что включает задержка INP:
- Input delay: время от события до начала обработки (main thread занят другими задачами)
- Processing time: время выполнения обработчиков событий (JS execution)
- Presentation delay: время от окончания обработки до paint (браузер ждёт следующего кадра)
Почему FID недостаточен: FID измерял только input delay первого взаимодействия. Реальная UX проблема часто в том, что после загрузки страницы обработка событий занимает 300ms - FID уже не считает, а пользователь страдает.
Как улучшить INP:
- Разбивайте длинные задачи: задача >50ms должна быть разбита на чанки
function processLargeArray(items, callback) { let index = 0; function chunk() { const end = Math.min(index + 10, items.length); for (; index < end; index++) callback(items[index]); if (index < items.length) { scheduler.postTask(chunk, { priority: 'user-blocking' }); } } chunk(); } - Избегайте больших re-render в обработчиках событий - используйте колбек-батчинг через rAF
- Yield to main thread:
scheduler.yield()- новый API для явной отдачи контроля event loop - Не делайте дорогих операций в
scroll,mousemove- throttle/debounce
Методология INP: Берётся 75-й перцентиль всех задержек взаимодействия. Если у пользователя было 100 кликов, 75 из них быстрее 200ms - INP хороший. Один медленный клик из 100 не сильно повлияет.
Important
Переход от FID к INP радикально меняет фокус оптимизации: нужно следить не только за загрузкой страницы, но и за отзывчивостью на ВСЁМ протяжении сессии. Long Tasks API и Total Blocking Time (TBT) становятся критически важными метриками для дебага.
33. CLS: causes, session window, solutions
CLS измеряет неожиданные сдвиги макета, когда видимый элемент меняет позицию между кадрами. Оценка = impact fraction × distance fraction.
Impact fraction: доля viewport, затронутая сдвигом (0-1). Distance fraction: насколько далеко элемент сдвинулся относительно viewport (0-1).
Session window: сдвиги группируются в окна по 5 секунд (1 секунда между сдвигами = разрыв окна). CLS = максимальная сумма сдвигов в одном окне.
Основные причины CLS и решения:
-
Изображения без размеров:
<!-- ❌ Плохо - браузер не знает размер до загрузки --> <img src="photo.jpg"> <!-- ✓ Хорошо - резервируем место заранее --> <img src="photo.jpg" width="800" height="600"> <!-- или --> <style>.img-container { aspect-ratio: 4/3; }</style> -
Динамический контент (реклама, эмбеды):
.ad-slot { min-height: 250px; /* резервируем минимальное место */ } -
Веб-шрифты (FOIT/FOUT):
@font-face { font-display: optional; /* или swap, избегать block */ } /* Подберите fallback-шрифт с похожими метриками */ @font-face { font-family: 'Custom'; src: url('custom.woff2'); font-display: swap; ascent-override: 90%; /* метрики fallback-шрифта */ descent-override: 20%; } -
Поздняя подгрузка данных в элементы:
.content { min-height: calc(100vh - 200px); } -
Анимации, меняющие layout:
/* ❌ Сдвигает контент вниз */ @keyframes slideIn { from { top: -100px; } to { top: 0; } } /* ✓ Использует transform - не влияет на layout */ @keyframes slideIn { from { transform: translateY(-100px); } to { transform: translateY(0); } }
Important
CLS - самая коварная метрика. Её сложно заметить при разработке (всегда тёплый кеш, быстрая загрузка), но на медленных соединениях сдвиги становятся критичными. Обязательно тестируйте CLS в throttled-условиях и на реальных устройствах.
34. TTFB: components, optimization
TTFB - время от начала запроса до получения первого байта ответа. Включает: redirect time, DNS lookup, TCP+TLS handshake, server processing time.
TTFB для разных протоколов в идеальных условиях (0ms latency):
- HTTP/1.1 HTTPS: TCP (1 RTT) + TLS 1.2 (2 RTT) = 3 RTT
- HTTP/2 HTTPS: TCP (1 RTT) + TLS 1.3 (1 RTT) = 2 RTT
- HTTP/3: QUIC (0 RTT для повторных соединений) = 0-1 RTT
На практике каждый RTT стоит 20-200ms в зависимости от географии.
Что влияет на TTFB:
- Server processing time: генерация ответа (database query, SSR, template rendering) - основная доля для динамических страниц
- Redirects: каждый redirect добавляет RTT - избегайте цепочек, используйте
rel="canonical" - Network latency: физическое расстояние до сервера
- Connection setup: DNS, TCP, TLS - минимизируется через
preconnectи HTTP/2/3
Оптимизация TTFB для фронтендера:
<!-- Preconnect к критическому API/ориджину -->
<link rel="preconnect" href="https://api.example.com" crossorigin>
<!-- Указываем режим 103 Early Hints для серверной логики -->
<!-- Сервер отправляет 103 с Link-заголовками для preload пока готовится основной ответ -->Серверные оптимизации (которые должен понимать фронтендер):
- Кеширование на edge (CDN) - HTML с
CDN-Cache-Controlдля не-персонализированных страниц - Edge workers (Cloudflare Workers, Lambda@Edge) - обработка запроса на ближайшем edge, минуя origin
- Streaming SSR - отправка HTML чанками по мере генерации (TTFB раньше, контент видим быстрее)
Info
TTFB > 800ms считается проблемным (Lighthouse). Для статического контента TTFB > 200ms - повод проверить CDN и географическое распределение. Для SSR - нормально до 500ms, выше - нужен edge-кешинг или streaming.
35. Resource Hints: preload, prefetch, preconnect, dns-prefetch, prerender
Resource Hints - <link> атрибуты, подсказывающие браузеру что и когда загружать, оптимизируя критический путь.
dns-prefetch:
<link rel="dns-prefetch" href="//api.example.com">Выполняет DNS-резолвинг заранее (~50-100ms экономии). Используйте для сторонних доменов которые скоро понадобятся (API, CDN, analytics).
preconnect:
<link rel="preconnect" href="https://api.example.com">DNS + TCP + TLS handshake. Экономит 100-500ms. Используйте для КРИТИЧЕСКИХ third-party origins. Не злоупотребляйте - каждое preconnect открывает соединение, которое потребляет ресурсы. Максимум 4-6 на страницу.
preload:
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="hero.jpg" as="image" imagesrcset="hero-2x.jpg 2x">Загружает ресурс с ВЫСОКИМ приоритетом немедленно. Обязательно указывайте as (font, image, script, style) чтобы браузер знал Content-Type и мог приоритизировать. Без as - запрос будет с низким приоритетом, неправильно.
prefetch:
<link rel="prefetch" href="page-2.js">Загружает ресурс с НИЗКИМ приоритетом, для будущих навигаций (следующая страница). Выполняется в idle time. Не гарантирует что ресурс будет загружен - браузер может отменить если не хватает ресурсов.
prerender:
<link rel="prerender" href="https://example.com/next-page">Загружает И РЕНДЕРИТ целую страницу в фоновом iframe. Мгновенная навигация, но потребляет много ресурсов. Chrome ограничивает количество одновременных prerender.
<!-- Типичная комбинация для SPA -->
<link rel="preconnect" href="https://api.example.com" crossorigin>
<link rel="preload" href="/fonts/inter.woff2" as="font" crossorigin>
<link rel="preload" href="/critical.css" as="style">
<link rel="prefetch" href="/page-about.js">Important
preload - мощный но опасный инструмент. Если вы preload’ите ресурс и не используете в течение ~3 секунд - Chrome покажет консольное предупреждение, ресурс считается потраченным впустую, ухудшая общую загрузку. Preload’ьте только ресурсы, которые ТОЧНО понадобятся в critical path.
36. Priority Hints (fetchpriority)
Priority Hints - атрибут fetchpriority, позволяющий явно указать браузеру приоритет загрузки ресурса поверх дефолтных эвристик браузера.
Приоритеты загрузки браузера (по умолчанию):
- Highest: CSS (блокирует рендеринг), шрифты
- High: скрипты в
<head>(блокируют парсинг), preload, картинки в viewport - Medium: скрипты с
defer/async - Low: скрипты с
asyncвнизу страницы, prefetch, картинки вне viewport - Lowest: картинки с
loading="lazy"
Браузер определяет приоритет автоматически на основе позиции элемента в DOM, типа ресурса, и видимости. Но эвристики не идеальны - именно здесь fetchpriority вступает в игру.
<!-- LCP-изображение: самый высокий приоритет -->
<img src="hero.webp" fetchpriority="high">
<!-- Не-критическая картинка в футере: низкий приоритет -->
<img src="footer-logo.png" fetchpriority="low" loading="lazy">
<!-- Критический скрипт -->
<script src="main.js" fetchpriority="high"></script>
<!-- Preload + fetchpriority -->
<link rel="preload" href="hero.webp" as="image" fetchpriority="high">Какие элементы поддерживают fetchpriority:
<img>- наиболее распространённый use-case<link rel="preload">- для любого типа ресурса<script>- для JS-бандловfetch()API - через опцию{ priority: 'high' | 'low' | 'auto' }
fetchpriority vs preload: Это разные инструменты с разными целями:
preload- говорит браузеру “начни загружать этот ресурс НЕМЕДЛЕННО, не жди пока дойдёшь до него в HTML”fetchpriority- говорит браузеру “когда ты загружаешь этот ресурс, поставь его ВЫШЕ/НИЖЕ в очереди сетевых запросов”
Они работают вместе: preload определяет КОГДА начать загрузку, fetchpriority определяет КАКОЙ приоритет у этого запроса в сетевой очереди.
Когда использовать fetchpriority="high":
- LCP-изображение, которое браузер мог бы определить как low-priority (например, если оно загружается через CSS background-image или находится ниже в DOM)
- Критический JS-бандл, от которого зависит интерактивность
- Шрифты для above-the-fold текста
Когда использовать fetchpriority="low":
- Изображения ниже сгиба (below-the-fold), которые браузер мог бы загрузить с высоким приоритетом
- Не-критические скрипты, которые всё равно нужны, но не срочно
- Ресурсы для контента, который появится только после пользовательского взаимодействия
Как fetchpriority влияет на HTTP/2+:
В HTTP/2 мультиплексирование позволяет отправлять множество запросов параллельно, но сервер всё ещё решает в каком порядке отдавать данные (stream prioritization). fetchpriority передаёт приоритет в HTTP/2 stream weight, влияя на порядок отдачи сервером. В HTTP/3/QUIC приоритизация работает аналогично, но с улучшенной обработкой из-за отсутствия TCP HOL blocking.
Измерение эффекта:
Chrome DevTools → Network → колонка “Priority” показывает приоритет каждого запроса. До и после добавления fetchpriority можно увидеть изменение порядка загрузки.
Important
fetchpriorityне меняет ПОРЯДОК загрузки (HTML всё ещё парсится сверху вниз), он меняет КОНКУРЕНЦИЮ в очереди сетевых запросов. Ресурс сfetchpriority="high"получит приоритет над другими в тот момент, когда запрос отправлен. Используйте Lighthouse - он подсвечивает изображения с неправильным приоритетом.
37. Bundle size optimization: code splitting, tree shaking, compression
Оптимизация размера бандла - постоянный процесс, а не разовая акция. Каждый kilobyte JS стоит: download time (сеть), parse time (CPU), execution time (CPU).
Code Splitting - разделение кода на бандлы:
// Статический import - входит в основной бандл
import { heavyFunction } from './heavy.js';
// Динамический import - отдельный чанк, загружается при необходимости
const { heavyFunction } = await import('./heavy.js');Уровни code splitting:
- Route-level: каждая страница - отдельный бандл. Простейший и самый эффективный
- Component-level: тяжёлые компоненты лениво загружаются (чарты, rich text editors)
- Vendor chunk: framework + общие зависимости выносятся в отдельный бандл для долгосрочного кеширования
Tree Shaking - удаление мёртвого кода:
Работает только с ES modules (статический import/export). CommonJS (require) не shakable.
// ❌ Не tree-shakable - весь модуль включается
import _ from 'lodash';
_.debounce(fn, 300);
// ✓ Tree-shakable - только debounce
import { debounce } from 'lodash-es';
debounce(fn, 300);Условия для tree shaking: ES modules + production mode + sideEffects: false в package.json (для библиотек).
Другие техники:
- Differential serving:
<script type="module">для современных браузеров (меньше полифилов). Legacy - в<script nomodule> - Compression: Brotli (лучше чем Gzip, но медленнее сжатие), Zopfli (максимальное Gzip, медленно, для статики на CDN). Brotli даёт ~15-20% выигрыш над Gzip для JS/CSS
- Image CDN: автоматическая конвертация в WebP/AVIF, ресайз до нужного размера
- Shared dependencies: если микрофронтенды - выносить общие зависимости (react, react-dom) через Module Federation
sharedконфигурацию
Мониторинг:
// Webpack: webpack-bundle-analyzer
// Vite: rollup-plugin-visualizer
// Общий: bundlesize в CI (не дать бандлу вырасти без ревью)Important
Code splitting - это НЕ про уменьшение общего размера кода. Это про загрузку ТОЛЬКО нужного кода для текущей страницы. При плохом сплиттинге пользователь на /home качает код для /settings. Правильный сплиттинг даёт больший прирост TTI, чем любые микрооптимизации размера.
38. Lazy Loading: loading=“lazy”, IntersectionObserver, dynamic import()
Нативный lazy loading (HTML атрибут):
<img src="photo.jpg" loading="lazy" decoding="async">
<iframe src="video.html" loading="lazy"></iframe>Браузер сам решает когда загружать на основе расстояния до viewport (обычно ~1250px для Chrome). Не требует JS, работает мгновенно. Но нет контроля над thresholds и нельзя lazy-load background-image.
Intersection Observer для кастомной логики:
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src; // подстановка реального src
img.classList.add('loaded');
observer.unobserve(img);
}
});
}, { rootMargin: '200px' }); // загружать за 200px до появления
document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img));Плюсы Intersection Observer: точный контроль threshold, rootMargin для предзагрузки, можно лейзилоадить что угодно (iframe, компоненты, фоны). Не влияет на layout - колбек вызывается асинхронно.
Динамический import (code splitting):
// Ленивая загрузка модуля при навигации
button.addEventListener('click', async () => {
const { renderChart } = await import('./heavy-chart.js');
renderChart(data);
});
// Preload модуля по ховеру
button.addEventListener('mouseenter', () => {
import('./heavy-chart.js'); // начинает загрузку, но не выполняет
});Ленивая загрузка компонентов в React/Vue:
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));
// Suspense с fallback UI
<Suspense fallback={<Skeleton />}>
<HeavyComponent />
</Suspense>Info
Комбинируйте подходы:
loading="lazy"для ниже-сгиба картинок (просто и без JS), Intersection Observer для компонентов/фонов с кастомной логикой, dynamicimport()для тяжелых JS-бандлов. Не лейзилоадьте above-the-fold изображения - это увеличит LCP.
39. content-visibility + contain: CSS containment for performance
content-visibility - CSS свойство, позволяющее браузеру пропускать рендеринг невидимых (вне viewport) элементов. Применяет автоматический contain к элементу.
/* Браузер НЕ рендерит элемент, пока он находится вне viewport */
.long-page-section {
content-visibility: auto;
contain-intrinsic-size: 500px; /* placeholder высота для скролбара */
}Как работает: content-visibility: auto говорит браузеру: “не лейауть, не пейнти содержимое этого элемента пока он не приблизится к viewport”. Это даёт огромный прирост на длинных страницах (списки, infinite scroll history).
contain-intrinsic-size - placeholder размер, чтобы скролбар был корректным и не дёргался при подгрузке. Без него элемент схлопнется до 0px.
Свойство contain:
contain: layout- внутренний reflow не влияет на внешний (и наоборот)contain: paint- содержимое не рисуется за границами элемента (какoverflow: hiddenно без clipping скролбаров)contain: size- размер элемента не зависит от потомков (нужен явный размер)contain: style- counter и quotes изолированыcontain: strict=layout paint size style
/* Компонент-виджет, изолированный от остальной страницы */
.dashboard-widget {
contain: layout style;
content-visibility: auto;
contain-intrinsic-size: 300px;
}Когда применять: Длинные страницы с однотипными секциями (новостные ленты, e-commerce каталоги, infinite scroll). Не применяйте для above-the-fold контента - это замедлит First Paint.
Important
content-visibility: auto+contain-intrinsic-size- пожалуй, самый простой способ радикально улучшить LCP и общую производительность длинных страниц. Браузер просто не рендерит оффскрин контент, экономя layout/paint на всём что вне viewport. Прирост может быть 2-10x на страницах с тысячами элементов.
40. BFCache (Back-Forward Cache) + Page Lifecycle API
BFCache (Back-Forward Cache) - механизм браузера, который сохраняет полную snapshot страницы (DOM, JS state, CSSOM, render tree) в памяти при навигации away. При возврате через кнопку “Назад” или “Вперёд” страница восстанавливается мгновенно - без повторной загрузки, парсинга, или выполнения JS.
Как работает BFCache:
- Пользователь уходит со страницы (клик по ссылке, forward navigation)
- Браузер проверяет, eligible ли страница для BFCache
- Если да - замораживает страницу (freeze event), сохраняет snapshot
- При возврате - размораживает (resume/pageshow event), страница появляется мгновенно
Когда страница НЕ eligible для BFCache:
- Есть открытые WebSocket соединения
- Есть
unloadevent listener (это блокирует BFCache в Chrome!) - Используется
window.opener(страница была открыта черезwindow.open) - Есть active
beforeunloadhandler - Страница содержит
<form>с unsaved changes - Используется
navigator.sendBeaconв unload - Страница использует
SharedArrayBufferбез правильной COOP/COEP настройки
Page Lifecycle API - состояния страницы:
Active → Passive → Hidden → Frozen → Terminated
↑ ↓
└── Resumed ←──
- Active: страница на переднем плане, пользователь взаимодействует
- Passive: страница видна но не в фокусе (другое окно поверх)
- Hidden: страница скрыта (пользователь переключил вкладку)
- Frozen: страница в BFCache, JS execution приостановлен, таймеры заморожены
- Terminated: страница удалена из памяти (evicted из BFCache)
Freeze/Resume события:
// Обработка заморозки (страница уходит в BFCache)
document.addEventListener('freeze', () => {
// Остановить polling, закрыть WebSocket, сохранить state
clearInterval(pollingInterval);
});
// Обработка разморозки (страница возвращается из BFCache)
document.addEventListener('resume', () => {
// Перезапустить polling, проверить свежесть данных
startPolling();
});
// pageshow с persisted = true означает возврат из BFCache
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
// Страница восстановлена из BFCache
// Обновить данные, проверить авторизацию
refreshUserData();
}
});Почему страницы evict’ятся из BFCache:
- Memory pressure (браузеру нужна память для новой страницы)
- Страница была в BFCache слишком долго (TTL зависит от браузера)
- Страница использовала ресурсы, которые не могут быть заморожены
- Пользователь очистил browsing data
Как тестировать BFCache:
- Chrome DevTools → Application → Back-forward cache → Run test
- Или через URL:
chrome://discards/показывает BFCache eligibility - В тестах: навигация away и back с проверкой
event.persistedвpageshow
Влияние на Core Web Vitals: BFCache радикально улучшает LCP и INP для возвратных навигаций - страница появляется мгновенно, без network round-trips. Google учитывает BFCache в CrUX данных, но Lighthouse пока не тестирует его напрямую.
Important
Никогда не используйте
unloadevent listener - он блокирует BFCache. Замените наpagehideилиvisibilitychange. Если ваша страница не попадает в BFCache, каждый возврат пользователя означает полную перезагрузку - это сотни миллисекунд задержки, которых можно избежать.
41. DOM parsing mechanics
DOM parsing - процесс превращения HTML-байтов в DOM-дерево. Браузер делает это инкрементально, по мере поступления байтов от сети.
Инкрементальный HTML parsing: Браузер не ждёт загрузки всего HTML. Как только первые байты приходят, tokenizer начинает работу:
- Байты → символы (через encoding detection)
- Символы → токены (
<tag>,</tag>, текст, комментарий) - Токены → узлы DOM-дерева
Speculative parsing (preload scanner):
Пока основной HTML-парсер заблокирован синхронным скриптом, браузер запускает preload scanner - отдельный поток, который сканирует оставшийся HTML в поисках ресурсов для загрузки (<img>, <link>, <script src>). Это позволяет начать загрузку ресурсов заранее, не дожидаясь разблокировки парсера.
<script src="blocking.js"></script>
<!-- Пока blocking.js загружается и выполняется, preload scanner уже нашёл: -->
<link rel="stylesheet" href="styles.css"> <!-- начнёт загрузку заранее -->
<img src="hero.jpg"> <!-- начнёт загрузку заранее -->Parser-blocking scripts:
Синхронный <script src="..."> без async или defer блокирует HTML-парсинг:
- Парсер доходит до
<script> - Останавливает парсинг HTML
- Загружает скрипт (если внешний)
- Выполняет скрипт
- Только потом продолжает парсинг
Именно поэтому скрипты традиционно размещали в конце <body> - чтобы DOM был построен до выполнения JS.
CSS блокирует рендеринг, но НЕ парсинг: Это важное различие. CSSOM строится параллельно с DOM-парсингом. Но:
- CSS блокирует рендеринг (браузер не покажет страницу без CSSOM)
- CSS НЕ блокирует DOM-парсинг (браузер продолжает строить DOM)
- CSS блокирует выполнение JS (скрипт ждёт CSSOM, так как может читать computed styles через
getComputedStyle)
async/defer impact on parsing:
<!-- Блокирует парсинг: загружается и выполняется немедленно -->
<script src="app.js"></script>
<!-- НЕ блокирует парсинг: загружается параллельно, выполняется сразу после загрузки -->
<script async src="analytics.js"></script>
<!-- НЕ блокирует парсинг: загружается параллельно, выполняется после DOM-парсинга, перед DOMContentLoaded -->
<script defer src="app.js"></script><template> element (inert DOM):
<template> создаёт DocumentFragment - DOM-дерево, которое не рендерится и не участвует в layout. Содержимое template полностью inert: скрипты не выполняются, картинки не загружаются, стили не применяются. Активация происходит при document.importNode(template.content, true) или template.content.cloneNode(true).
<template id="card-template">
<div class="card">
<img src="placeholder.jpg"> <!-- НЕ загружается пока template не активирован -->
</div>
</template>innerHTML vs createElement performance:
innerHTML- браузер парсит HTML-строку в DOM. Быстрее для массовой вставки, но требует full re-parse и XSS-опасен при пользовательском контентеcreateElement- программное создание узлов. Медленнее для массовой вставки, но безопаснее и даёт больше контроля- Для больших вставок
innerHTMLможет быть в 2-5x быстрее, ноDocumentFragment+createElementдаёт лучшую производительность при множественных вставках
Info
Современный браузер оптимизирует парсинг aggressively. Speculative preload scanner - одна из самых важных оптимизаций: она позволяет загружать ресурсы параллельно с выполнением blocking-скриптов. Используйте
deferдля всех скриптов которые не нужны до DOMContentLoaded - это даёт парсеру работать без блокировок.
42. SSR vs SSG vs CSR vs ISR
CSR (Client-Side Rendering): Браузер загружает минимальный HTML + JS бандл, рендерит всё на клиенте.
- Плюсы: простой деплой (статический хостинг), богатая интерактивность
- Минусы: плохой SEO (пустой HTML для краулеров, хотя Googlebot теперь рендерит JS), плохой LCP/FCP, большой JS в основном потоке
SSR (Server-Side Rendering): Сервер рендерит HTML на каждый запрос, отправляет готовую страницу с данными.
- Плюсы: отличный LCP/FCP, хороший SEO, пользователь видит контент сразу
- Минусы: выше TTFB (сервер должен отработать), дороже инфраструктура, атака на сервер влияет на доступность
SSG (Static Site Generation): HTML генерируется на билде, раздаётся как статика.
- Плюсы: мгновенный TTFB (CDN), минимальный LCP, не нужен сервер, высокая надёжность
- Минусы: длинный билд для больших сайтов, контент не обновляется между билдами, персонализация ограничена
ISR (Incremental Static Regeneration): Комбинация SSG + SSR. Страницы генерируются на билде, но при запросе к устаревшей (stale) странице она регенерируется в фоне.
- Плюсы: быстро как SSG, но контент обновляется в реальном времени
- Минусы: сложнее инфраструктура, stale-while-revalidate паттерн (пользователь может получить устаревшую версию)
Когда что выбирать:
// CSR - internal tools, dashboards, SPA за логином
// (SEO не важен, пользователи авторизованы)
// SSR - новостные сайты, e-commerce, блоги с комментариями
// (важен свежий контент + SEO)
// SSG - документация, маркетинговые страницы, блоги редких авторов
// (контент меняется редко, нужна максимальная скорость)
// ISR - e-commerce с частыми обновлениями цен, крупные блоги
// (нужна скорость CDN + свежесть контента)Hybrid подходы: Next.js позволяет на одной странице смешивать SSG (общая структура) + CSR (персонализированные данные через useEffect). Или streaming SSR с Suspense boundaries - контент стримится чанками, быстрые части рендерятся раньше.
Important
Нет серебряной пули. Для контентного сайта SSG на CDN - идеал. Для SPA-приложения за логином CSR приемлем. Для продукта с SEO - SSR + edge caching. Ключевое: не выбирайте архитектуру до того как определите аудиторию (SEO-зависимая?) и частоту обновления контента.
43. Microfrontends: Module Federation, iframes, Web Components
Микрофронтенды (MFE) - архитектурный подход, при котором фронтенд-приложение собирается из независимо разрабатываемых и деплоимых микро-приложений.
Подходы:
-
Module Federation (Webpack 5 / Rspack):
// host/webpack.config.js new ModuleFederationPlugin({ name: 'host', remotes: { checkout: 'checkout@http://localhost:3001/remoteEntry.js', products: 'products@http://localhost:3002/remoteEntry.js', }, shared: { react: { singleton: true, eager: true } }, });Позволяет загружать JS-модули из других приложений в рантайме. Shared-зависимости (react, react-dom) загружаются один раз. Самый популярный подход для Webpack-экосистемы.
-
iframe: Простейшая изоляция: полная изоляция DOM, CSS, JS. Коммуникация через
postMessage. Минусы: плохая производительность (каждый iframe - отдельный browsing context), сложная маршрутизация, плохой UX (скролл, accessibility). -
Web Components: Каждая команда поставляет кастомный элемент. Независимы от фреймворка.
class CheckoutWidget extends HTMLElement { connectedCallback() { this.innerHTML = '<checkout-app></checkout-app>'; import('./checkout-bootstrap.js'); } } customElements.define('checkout-widget', CheckoutWidget); -
Server-side composition (Tailor, Podium): Сервер собирает HTML из нескольких микро-приложений на уровне шаблонов. Каждый фрагмент рендерится независимо, с собственным TTL и fallback.
Tradeoffs:
- Плюсы: независимые деплои, изолированные команды, можно использовать разные фреймворки
- Минусы: сложность оркестрации, дублирование зависимостей (раздувание бандла), сложное end-to-end тестирование, проблемы с shared state и роутингом
- Не начинайте с микрофронтендов если у вас не десятки команд и независимые релизные циклы. Это решение организационной проблемы, а не технической
Important
Module Federation даёт наилучший UX (общий SPA, нет перезагрузок страницы между MFE), но сложнее в настройке. iframe дают наилучшую изоляцию ценой UX. Web Components дают фреймворк-независимость. Выбор зависит от приоритета: UX побеждает изоляцию или наоборот.
44. BFF (Backend for Frontend) pattern
BFF - серверный слой, создаваемый специально под нужды конкретного фронтенд-клиента. Вместо того чтобы фронтенд общался с десятками микросервисов, он общается с одним BFF, который агрегирует и трансформирует данные.
Проблема без BFF:
Mobile App → /api/users (user service)
→ /api/orders (order service)
→ /api/products (product service)
← 3 запроса с разными форматами данных ← сложная логика на клиенте
С BFF:
Mobile App → /mobile-api/dashboard → BFF → /users + /orders + /products
← агрегирует и форматирует ← один запрос
Зачем нужен BFF:
- Агрегация данных: собрать данные из 5 микросервисов в один ответ, оптимизированный для экрана. Меньше round-trips, меньше логики на клиенте
- Трансформация форматов: бэкенд отдаёт snake_case, фронтенд хочет camelCase. Или бэкенд отдаёт полные модели, фронтенду нужна проекция (view model)
- Секьюрность: API ключи и токены живут в BFF, не в браузере. BFF выполняет OAuth code exchange (confidential client, не public)
- Специфичная для канала логика: мобильное приложение хочет сжатые данные (меньше трафика), веб - полные для SEO
GraphQL как BFF: GraphQL-сервер часто выполняет роль BFF - он агрегирует данные из множества источников (REST API, базы данных, gRPC сервисы) и предоставляет единый endpoint. Резолверы GraphQL могут вызывать разные backend-сервисы и комбинировать результаты.
# Один GraphQL query заменяет 3 REST запроса
query Dashboard {
user { name, avatar }
recentOrders { id, total, status }
recommendations { id, name, price }
}BFF vs API Gateway:
- API Gateway - единая точка входа для ВСЕХ клиентов, маршрутизирует запросы к backend-сервисам. Generic, не специфичен для клиента
- BFF - специфичен для ОДНОГО клиента (web, iOS, Android). Знает UI-потребности, трансформирует данные под конкретный экран
- Часто BFF стоит ЗА API Gateway: Client → API Gateway → BFF → Backend Services
Пер-client BFF: В крупных организациях каждый клиент имеет свой BFF:
web-bff- полный UI, SEO, rich interactionsmobile-bff- компактные ответы, offline-first, push notificationspartner-bff- ограниченный набор данных для внешних интеграций
Кеширование в BFF: BFF - идеальное место для кеширования:
- Кеширует ответы backend-сервисов (уменьшает нагрузку)
- Применяет стратегии stale-while-revalidate
- Может использовать Redis для shared cache между инстансами
Когда BFF overkill:
- Монолит на бэкенде (BFF дублирует логику)
- Один клиент (если только Web - overhead неоправдан)
- Маленькое приложение (few endpoints, simple data shapes)
- GraphQL уже покрывает потребность в гибких запросах
Info
BFF - это про адаптацию бэкенд-API к нуждам конкретного UI. Это не GraphQL-сервер (хотя может содержать его) и не API Gateway (хотя выполняет похожую роль). Это тонкий translation/aggregation слой, специфичный для одного клиента. Обычно разрабатывается фронтенд-командой.
45. State management patterns: URL vs localStorage vs server cache vs client state
Принцип: каждое состояние должно храниться там, где его жизненный цикл наиболее соответствует смыслу данных.
URL (Query params, path params, hash): Состояние, которое должно пережить перезагрузку и быть shareable.
// Храним фильтры и пагинацию в URL
/search?q=react&page=2&sort=date
// Пользователь может скопировать ссылку и отправить коллегеЧто хранить в URL: поисковые запросы, фильтры, пагинацию, выбранную вкладку, состояние модального окна (если оно должно открываться по ссылке).
Server cache (React Query, SWR, Apollo Cache): Серверные данные - всё что пришло от API. Не надо копировать их в Redux.
const { data, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000, // 5 минут считаем свежим
});Серверный кеш решает: дедупликацию запросов, кеширование, оптимистичные обновления, фоновую ревалидацию.
Client state (useState, useReducer, Redux, Zustand, Jotai): Состояние UI - открыто ли меню, какая тема, состояние формы до сабмита.
const [isOpen, setIsOpen] = useState(false);
const [theme, setTheme] = useState('light');localStorage / sessionStorage: Данные которые должны пережить перезагрузку, но не являются URL-shareable.
- localStorage: тема, язык, onboarding completion
- sessionStorage: временный черновик формы, scroll position
- Никогда: access/refresh токены (XSS вектор)
IndexedDB: Большие объёмы структурированных данных для offline-first. Сообщения чата, закешированные ответы API.
Important
Правило трёх вопросов: (1) Нужно ли это состояние при перезагрузке? → URL или storage. (2) Это данные с сервера? → server cache (React Query/SWR). (3) Это преходящее UI-состояние? → client state (useState/Redux). Если данные попали в две категории - вы что-то делаете неправильно.
46. Offline-first architecture: Service Worker, IndexedDB, Background Sync
Offline-first - архитектура, где приложение работает без сети, а сетевая синхронизация происходит когда возможно. Не “best effort”, а гарантированная работа.
Ключевые технологии:
- Service Worker - перехватывает запросы, возвращает из кеша
- IndexedDB - структурированное хранилище на клиенте
- Background Sync - отложенная отправка данных когда появится сеть
- Cache API - программный кеш HTTP-ответов
Стратегия кеширования - Stale While Revalidate:
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open('v1').then(cache =>
cache.match(event.request).then(cached => {
const fetchPromise = fetch(event.request).then(networkResponse => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
return cached || fetchPromise; // cached - немедленно, network - для обновления
})
)
);
});Проблема конфликтов синхронизации: Два пользователя редактируют один и тот же документ офлайн. Оба синхронизируют изменения - чьи данные сохранить?
Стратегии разрешения конфликтов:
- Last Write Wins (LWW): простое, но данные могут потеряться
- CRDT (Conflict-free Replicated Data Types): математически гарантируют конвергенцию без конфликтов. Подходит для collaborative editing (текст, счётчики)
- Operational Transformation (OT): трансформация операций для разрешения конфликтов. Google Docs
- Ручное разрешение: сохранить обе версии, показать пользователю diff, он решает
// Простейшая LWW синхронизация (PouchDB/CouchDB модель)
const doc = {
_id: 'note-1',
_rev: '2-abc',
content: 'Hello world',
updatedAt: Date.now() // временная метка для LWW
};Background Sync API:
// Регистрируем sync когда пользователь офлайн
navigator.serviceWorker.ready.then(reg => {
return reg.sync.register('sync-messages');
});
// В Service Worker обрабатываем sync
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-messages') {
event.waitUntil(syncPendingMessages());
}
});Important
Offline-first требует дисциплины: каждая операция записи должна работать без сети и предоставлять feedback пользователю что данные ещё не синхронизированы. Визуальные индикаторы синхронизации, пессимистичное/оптимистичное обновление UI, обработка конфликтов - это не опционально, а обязательно.
47. Authentication in SPA: access + refresh tokens, HttpOnly cookie vs localStorage
Выбор хранилища для токенов - самый контроверсиальный вопрос SPA-безопасности.
Подход A: HttpOnly cookies (рекомендуемый):
Browser: POST /login { email, password } (credentials: 'include')
Server: Set-Cookie: access_token=...; Secure; HttpOnly; SameSite=Lax; Path=/
Set-Cookie: refresh_token=...; Secure; HttpOnly; SameSite=Strict; Path=/api/auth/refresh
- Access token в куке (Secure + HttpOnly) - JS не читает, автоматически с каждым запросом
- XSS не может украсть токен (HttpOnly), но может делать запросы от имени пользователя
- CSRF защита через SameSite + дополнительный CSRF-токен в заголовке
- Refresh token на строгом SameSite=Strict и Path=/api/auth/refresh
Подход B: localStorage + Authorization header:
Browser: localStorage.setItem('access_token', token)
fetch(url, { headers: { Authorization: `Bearer ${token}` } })
- XSS читает токен напрямую - полная компрометация
- Нет автоматической отправки - защита от CSRF из коробки
- Подвержен XSS-векторам: любая npm-зависимость, XSS в rich text,
<a href="javascript:...">оставленный в комментариях
Подход C: BFF (Backend for Frontend) - максимальная безопасность:
Browser → BFF (/api/*) → Backend
← HTTP-only cookie ←
- Токены живут в BFF (конфиденциальный клиент, не public)
- Браузер общается с BFF через сессионную HttpOnly куку
- BFF проксирует запросы к основному API, прикрепляя access token
- Самый безопасный подход, но требует дополнительной инфраструктуры
Token refresh flow с обработкой race conditions:
let refreshPromise = null;
async function apiFetch(url, options) {
let res = await fetch(url, withAuth(options));
if (res.status === 401) {
if (!refreshPromise) {
refreshPromise = refreshToken().finally(() => { refreshPromise = null; });
}
await refreshPromise; // все параллельные 401 ждут один refresh
res = await fetch(url, withAuth(options));
}
return res;
}Important
HttpOnly cookie (с Secure, SameSite=Lax) - рекомендуемый подход для большинства SPA. localStorage - только если вы абсолютно уверены в отсутствии XSS (что невозможно в реальном мире). Комбинируйте с CSP и регулярным аудитом npm-зависимостей.
48. PWA: manifest, Service Worker, install prompt
PWA (Progressive Web Application) - веб-приложение, использующее современные API для поведения, близкого к нативному.
Ключевые компоненты:
1. Web App Manifest:
{
"name": "My App",
"short_name": "App",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#1976d2",
"icons": [
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" },
{ "src": "/icon-512-maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
]
}display: standalone - открывается без браузерного chrome. maskable иконка - адаптируется под форму иконок платформы (Android adaptive icons).
2. Service Worker: Обязателен для offline-работы и installability. Без SW страница не проходит PWA criteria в Chrome.
3. Install Prompt:
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault(); // не показывать автоматический мини-бар
deferredPrompt = e;
// Показать кастомную кнопку "Установить"
});
installButton.addEventListener('click', async () => {
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log(`User response: ${outcome}`); // 'accepted' или 'dismissed'
deferredPrompt = null;
});
window.addEventListener('appinstalled', () => {
console.log('PWA installed');
});Критерии installability (Chrome):
- HTTPS
- Valid manifest (с иконками минимум 192x192 и 512x512)
- Зарегистрированный Service Worker с fetch handler
- Пользователь провёл на сайте достаточно времени (engagement heuristic)
Продвинутые PWA-фичи:
- Web Share API:
navigator.share({ title, url })- нативный share sheet - Shortcuts: быстрые действия из контекстного меню иконки
- Periodic Background Sync: периодическое обновление в фоне
- Web OTP API: автозаполнение SMS OTP кодов
Info
PWA - это не просто “добавить манифест + SW”. Это progressive enhancement: базовый сайт работает везде, PWA-фичи добавляются там где поддерживаются. Лучшая стратегия: начните с HTTPS и responsive design, добавьте manifest, затем SW с офлайн-страницей, затем кеширование.
49. Accessibility (a11y): ARIA, focus management, screen readers
Accessibility (a11y) - практика создания интерфейсов, доступных для людей с ограниченными возможностями. Это не про “поддержку screen readers”, а про то, что ВСЕ пользователи взаимодействуют с интерфейсом по-разному: клавиатура, мышь, тач, голос, switch-устройства.
ARIA (Accessible Rich Internet Applications): ARIA атрибуты добавляют семантику там, где HTML недостаточно.
<!-- Tab компонент -->
<div role="tablist" aria-label="Product tabs">
<button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1">
Description
</button>
<button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2" tabindex="-1">
Reviews
</button>
</div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
Product description...
</div>Ключевые ARIA правила:
aria-label- человекочитаемый label для элементов без видимого текстаaria-labelledby- ссылается на ID элемента-заголовкаaria-describedby- дополнительное описаниеaria-live="polite/assertive"- динамическое объявление изменений (ошибки, загрузка)aria-expanded,aria-selected,aria-current- состояние интерактивных элементов
Focus management:
// После закрытия модального окна - вернуть фокус
const previousFocus = document.activeElement;
modal.showModal();
// ...работа с модалкой...
modal.addEventListener('close', () => {
previousFocus.focus(); // возвращаем фокус куда было
});
// Focus trap в модальном окне
modal.addEventListener('keydown', (e) => {
if (e.key === 'Tab' && e.shiftKey && e.target === firstFocusable) {
e.preventDefault();
lastFocusable.focus();
}
});Screen reader только контент (sr-only):
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}Для текста который должен быть прочитан screen reader но не виден: label кнопки-иконки, skip-link для клавиатурной навигации.
Keyboard navigation:
- Все интерактивные элементы должны быть доступны через Tab
- Tab order должен соответствовать визуальному порядку (избегайте
tabindex > 0) - Enter/Space - основные клавиши активации
- Escape - закрытие, отмена
Important
Accessibility - не фича, это базовая функциональность. Тестируйте: отключите мышь и пройдите по сайту только клавиатурой. Включите VoiceOver (Cmd+F5 на Mac) и попробуйте выполнить основные сценарии. Плагин axe DevTools для быстрого аудита. Внедрите a11y линтер (eslint-plugin-jsx-a11y) в CI - ловите ошибки до прода.
50. SEO for SPA: SSR, prerendering, dynamic rendering, meta tags
Фундаментальная проблема SPA + SEO: поисковые роботы приходят за HTML. SPA по умолчанию отдаёт пустой <div id="root"></div> и JS-бандл. Googlebot умеет рендерить JS (с 2019, на базе Chrome 74), но это медленно, ресурсозатратно и непредсказуемо.
Подходы к решению:
1. SSR (Server-Side Rendering): Сервер рендерит страницу с данными, отдаёт полный HTML. Идеально для SEO.
// Next.js App Router - RSC (React Server Components)
export default async function ProductPage({ params }) {
const product = await db.product.findUnique({ where: { id: params.id } });
// Рендерится на сервере, HTML содержит данные продукта
return <ProductDetails product={product} />;
}Минусы: стоимость инфраструктуры, задержка сервера (TTFB).
2. Prerendering / SSG (Static Site Generation): HTML генерируется на билде, раздаётся как статика.
// Next.js: getStaticPaths + getStaticProps
export async function getStaticPaths() {
const products = await getProducts();
return { paths: products.map(p => ({ params: { id: p.id } })), fallback: 'blocking' };
}Подходит для контента который редко меняется. Не работает для миллионов страниц (слишком долгий билд).
3. Dynamic Rendering: Сервер определяет, робот или пользователь (по User-Agent), и для роботов отдаёт пререндеренный HTML (Puppeteer/Playwright), а для пользователей - обычный SPA. Используется как временное решение, не рекомендуется как постоянное.
4. Meta tags для всех подходов:
<title>Product Name - My Store</title>
<meta name="description" content="Buy Product Name for $29.99...">
<meta property="og:title" content="Product Name">
<meta property="og:image" content="https://example.com/product-image.jpg">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://example.com/product/123">Структурированные данные (JSON-LD):
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Product Name",
"offers": {
"@type": "Offer",
"price": "29.99",
"priceCurrency": "USD"
}
}
</script>Обеспечивает rich results в поиске (цена, рейтинг, наличие). JSON-LD не зависит от JS-рендеринга - парсится из HTML напрямую.
Important
Googlebot рендерит JS, но с ЗАДЕРЖКОЙ (часы-дни для render queue). Не полагайтесь на это. SSR или SSG - единственные надёжные подходы для production SEO. Dynamic rendering - костыль, а не стратегия.
51. Internationalization (i18n) advanced
i18n - подготовка приложения к поддержке множества языков и культурных особенностей. Не просто перевод строк, а форматирование дат, чисел, валют, plural rules, RTL-поддержка.
ICU MessageFormat (стандарт для сложных переводов):
// ICU MessageFormat строка в JSON переводов
{
"items_count": "{count, plural, =0 {No items} one {# item} other {# items}}",
"gender_msg": "{gender, select, male {He} female {She} other {They}} liked this",
"date_range": "{start, date, medium} - {end, date, medium}"
}
Обрабатывает plural rules (не count === 1, а Unicode CLDR plural rules - в русском one/few/many, в арабском другие), gender, select.
RTL (Right-to-Left) поддержка:
/* Используйте логические свойства вместо физических */
.element {
margin-inline-start: 16px; /* вместо margin-left */
padding-inline-end: 8px; /* вместо padding-right */
border-inline-start: 1px solid; /* вместо border-left */
inset-inline-start: 0; /* вместо left: 0 */
}
/* При dir="rtl" на <html> эти свойства автоматически зеркалируются */<html dir="rtl" lang="ar">CSS logical properties для RTL:
margin-inline-start/endвместоmargin-left/rightpadding-inline-start/endвместоpadding-left/rightborder-inline-start/endвместоborder-left/rightinset-inline-start/endвместоleft/righttext-align: startвместоtext-align: leftfloat: inline-startвместоfloat: left
dir="auto" на элементах с пользовательским контентом автоматически определяет направление текста на основе первого “сильного” символа (буквы).
Динамическая загрузка переводов:
// Загружаем только переводы для текущего языка
const locale = navigator.language; // 'ru-RU', 'en-US'
const messages = await import(`./locales/${locale}.js`);
// С fallback цепочкой: 'pt-BR' → 'pt' → 'en'
function resolveLocale(preferred) {
const available = ['en', 'pt', 'pt-BR', 'ru'];
if (available.includes(preferred)) return preferred;
const base = preferred.split('-')[0];
if (available.includes(base)) return base;
return 'en'; // fallback
}Локализация дат, чисел, валют через Intl API:
const date = new Date();
new Intl.DateTimeFormat('ru-RU', { dateStyle: 'long' }).format(date);
// '6 мая 2026 г.'
new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(99.99);
// '99,99 €' (европейский формат: запятая для десятичных, пробел для тысяч)
new Intl.RelativeTimeFormat('ru', { style: 'long' }).format(-3, 'day');
// '3 дня назад'
// List formatting
new Intl.ListFormat('en', { style: 'long', type: 'conjunction' }).format(['A', 'B', 'C']);
// 'A, B, and C'React i18n библиотеки сравнение:
react-intl (FormatJS):
- Самый зрелый, основан на ICU MessageFormat
- Компоненты
<FormattedMessage>,<FormattedDate>,<FormattedNumber> - Плюсы: полная ICU поддержка, хорошая производительность с babel-plugin для extraction
- Минусы: большой bundle size, сложный setup
next-intl:
- Нативная интеграция с Next.js App Router
- Поддержка i18n routing, middleware для locale detection
- Плюсы: tree-shakable, type-safe, server components support
- Минусы: привязан к Next.js, не подходит для других фреймворков
i18next:
- Самая популярная библиотека, экосистема плагинов
- Поддержка backend (загрузка переводов с сервера), caching, interpolation
- Плюсы: гибкий, много плагинов, работает с любым фреймворком
- Минусы: свой формат (не ICU), требует адаптеров для React (react-i18next)
Translation extraction workflow:
- Разметка строк в коде через i18n функции/компоненты
- Extraction tool (i18next-parser, formatjs cli) сканирует код, генерирует JSON keys
- Переводчики работают с JSON файлами (через Lokalise, Crowdin, Transifex)
- CI проверяет что все keys переведены, нет orphaned keys
- Переводы деплоятся как часть build pipeline
Common pitfalls:
- Concatenation:
"Hello " + name + ", you have " + count + " messages"- не переводится. Используйте ICU:"{greeting} {name}, you have {count, plural, one {# message} other {# messages}}" - Context loss: одно слово “Save” может быть глаголом (сохранить) или существительным (сейф). Используйте keys с контекстом:
"button.save","noun.save" - Hardcoded dates/numbers: всегда используйте Intl API, не форматируйте вручную
- Font support: не все шрифты поддерживают все языки. Используйте font fallback chains с Noto Sans как универсальный fallback
- Layout expansion: немецкий текст на 30% длиннее английского, арабский может быть короче. Дизайн должен выдерживать ±50% изменения длины текста
Info
i18n - это не про “добавить переводы в конце”. Это архитектурное решение. Форматирование дат/чисел/валют через Intl (браузерный API, не нужны библиотеки), логические CSS-свойства для RTL-ready вёрстки, ICU MessageFormat для plural/select правил, динамическая загрузка переводов (не раздувать основной бандл).
Summary
Эти 50 вопросов покрывают ключевые знания Senior Frontend Developer о Web-платформе. Глубокое понимание безопасности (XSS, CSRF, CORS, CSP) критично, потому что фронтендер - первая линия защиты пользовательских данных. Знание HTTP и сетевых протоколов позволяет принимать обоснованные архитектурные решения. Понимание внутренностей браузера (Event Loop, Critical Rendering Path, composite layers) необходимо для решения сложных проблем производительности.
Senior-уровень - это не про заучивание ответов, а про понимание tradeoffs: почему выбрали WebSocket вместо SSE, почему JWT в HttpOnly cookie а не localStorage, почему ISR а не SSG для конкретного проекта. Именно этого ждут на собеседовании.