Браузер и фронтенд-рантайм
Граничные случаи service worker: version skew, долговременность и ловушка навигации
Вы деплоите service worker, который раздаёт кеш app shell по стратегии cache-first. Через неделю деплоите исправление бага. Часть пользователей никогда его не получит — они продолжают получать сломанный закешированный shell при каждой перезагрузке, и никакой кнопки восстановления для обычного пользователя не существует.
Проблема обновления и version skew
Кеш ресурсов service worker привязан к версии его кода. При деплое версии N+1 открытая страница может работать с воркером версии N, пока HTML версии N+1 уже задеплоен — или наоборот. Если воркер раздаёт cache-first app.js версии N, а HTML ожидает app.js версии N+1, получается runtime-ошибка из-за несовпадающих модулей.
Надёжный паттерн:
- Content-hash каждое имя файла ресурса (
app.4f3a1c.js). Старые и новые ресурсы сосуществуют в кеше без коллизий. - Version-tag имена кешей (
cache-v3,cache-v4). Pre-cache весь набор ресурсов каждого деплоя под version tag. - В
activateудалять только устаревшие кеши — кеши, чей version tag не является текущим. Делайте это послеclients.claim(), чтобы ни одна контролируемая страница не потеряла ресурсы в середине сессии. - Раздавать навигационные запросы network-first (или по выделенному маршруту app-shell), чтобы пользователи всегда получали HTML, консистентный с активным воркером.
Тот же класс бага применим к sw.js самому по себе: браузеры кешируют файл воркера по умолчанию до 24 часов. Современная практика — отдавать sw.js с заголовком Cache-Control: no-cache, чтобы браузер всегда перепроверял его на навигации.
Service workers не являются долговременными
Браузер агрессивно убивает idle service worker — часто в течение секунд после завершения fetch-события — и перезапускает при следующем событии. Любое состояние в module-level переменной исчезает при перезапуске. Это частый источник багов:
- Счётчик запросов в полёте.
- Кеш ожидающих промисов.
- WebSocket-соединение в глобальной переменной.
Всё испаряется. Постоянное состояние должно жить в IndexedDB или Cache API.
Долгоживущая работа внутри обработчика события должна быть обёрнута в event.waitUntil(promise) — это говорит браузеру: «не убивай меня, пока этот промис не завершится». Забыть waitUntil — значит браузер может завершить воркер на полпути, и background sync, push-обработка, заполнение кеша молча не завершатся.
- Время убийства idle-воркера
- Секунды после последнего события
- HTTP-кеш браузера для sw.js
- До 24 часов по умолчанию
- Рекомендуемый заголовок для sw.js
- Cache-Control: no-cache
- Варианты постоянного состояния
- Только Cache API или IndexedDB
- Забытый waitUntil → тихий сбой
- push, sync, заполнение кеша
Перехват навигации и опасность app-shell
Самый мощный — и самый опасный — паттерн service worker: перехват навигационных запросов. fetch-обработчик ловит запрос на HTML-документ и возвращает закешированный app shell. Это даёт мгновенную загрузку, но создаёт класс багов, иначе невозможных.
Ловушка: если вы задеплоите баг в app shell и закешируете его с cache-first, каждый повторный визит будет раздавать сломанный shell из кеша, минуя сеть, где лежит исправление. Пользователь не может выбраться обычной перезагрузкой.
Защита многоуровневая:
- Навигационные запросы — network-first с коротким таймаутом (~3 с, с откатом на кеш). Это гарантирует, что исправление дойдёт до пользователей при первой успешной загрузке.
- Держите kill switch — версионированный эндпоинт, который воркер проверяет при activate или периодически. По сигналу вызывает
self.registration.unregister()и удаляет кеши. Это позволяет удалённо отсоединить сломанный service worker от всех клиентов. - Никогда не кешировать навигацию только из кеша. Всегда должен быть сетевой путь.
Сломанный service worker, задеплоенный широко — это инцидент stop-the-deploy, потому что обычные пользователи не имеют кнопки восстановления: они не могут открыть DevTools, не могут очистить данные сайта. Единственный выход — kill switch или свежий деплой, который старый воркер подхватит при следующей активации.
Service worker хранит карту запросов в полёте в module-level переменной `const cache = new Map()`. Через несколько секунд простоя записи исчезают. Почему?
Вы деплоите обновление service worker, и часть пользователей сообщает о сломанной странице: скрипты не загружаются с ошибками module-mismatch. Какова наиболее вероятная причина и надёжное исправление?
Пользователь застрял на сломанном закешированном app shell, и обычная перезагрузка не помогает. Какой механизм восстановления нужно было заложить заранее?
Почему это работает
Почему сломанный service worker так сложно исправить? Когда service worker перехватывает навигацию, он сидит между браузером и сервером для самого HTML-документа — страница не может загрузиться без ответа воркера первым. В отличие от сломанного CDN (где браузер откатывается на origin), сломанный service worker успешно отвечает сломанным закешированным ответом. Браузер не может отличить правильный кешированный ответ от ошибочного. Вот почему kill switch должен быть проактивным: URL, который воркер проверяет на каждой активации, чей ответ говорит воркеру, нужно ли ему отменить регистрацию. Если вы ждёте сообщений от пользователей — вы уже задеплоили.
- 01Почему module-level состояние service worker исчезает между запросами?
- 02Каков failure mode version skew в service workers и как его предотвратить?
- 03Почему сломанный service worker, перехватывающий навигацию — инцидент stop-the-deploy, и какова архитектурная защита?
У service workers три основных failure mode. Version skew: раздача кешированных ресурсов неверной версии — предотвращается content-hashed именами файлов и version-tagged кешами. Ловушка долговременности: module-level состояние испаряется между событиями, потому что браузер убивает idle воркеры; используйте event.waitUntil для долгих операций и IndexedDB/Cache API для состояния. Перехват навигации: кеширование самого HTML-документа означает, что сломанный shell навсегда удерживает пользователей — всегда используйте network-first для навигации и стройте kill-switch эндпоинт. Все три сбоя становятся трудноотменяемыми продакшн-инцидентами без превентивных мер. Файл sw.js должен отдаваться с Cache-Control: no-cache — иначе обновление воркера может задержаться до 24 часов.
встречается в143
- Почему GraphQL получает N+1junior
- Механика DataLoader: батчинг на границе тикаmiddle
- Контракты batch-функции: порядок, формы, ошибкиmiddle
- Federation и lookahead: батчинг за пределами DataLoadermiddle
- Защита сложности запросов: depth, cost, persisted queriesmiddle
- Senior GraphQL API: scheduling-контракт, изоляция арендаторов, наблюдаемостьsenior
- Зачем идемпотентность: безопасные retryjunior
- Серверный state machine: четыре состояния idempotency keymiddle
- Outbox и inbox: effectively-once через dual-write границуmiddle
- Конкурентность и архитектура кеша для идемпотентности на масштабеsenior
- Наблюдаемость, production-инциденты и дизайн для глобального масштабаsenior
- Что такое cache stampede и почему он делает всё хужеjunior
- Лок и single-flight: ограничение параллельных rebuildmiddle
- XFetch: вероятностное раннее истечение без координацииmiddle
- Stale-while-revalidate и CDN request coalescingmiddle
- Детектирование stampede и дизайн TTL для продакшенаmiddle
- Метастабильный сбой, fencing-токены и production-постмортемыsenior
- Что такое отношение: таблицы, строки, ключи и ограниченияjunior
- Ограничения, ключи и типы данных Postgresmiddle
- Нормальные формы, денормализация и почему схемы «прилипают»middle
- JSONB, массивы и когда side table побеждаетmiddle
- Heap-хранилище, TOAST и выравнивание колонокsenior
- Целостность схемы: deferral, версионирование и сбои в продакшнеsenior
- Реляционная модель vs документные, wide-column, граф и key-valuesenior
- Index-only scan, Visibility Map и INCLUDEsenior
- Типичные сбои в продакшне и аудит индексовsenior
- pg_statistic, ANALYZE и производственная наблюдаемостьmiddle
- Производственные режимы отказа и стабильность плановsenior
- MVCC: как Postgres раздаёт согласованные снимкиjunior
- Заголовок tuple и механика снимковmiddle
- HOT-обновления и уровни изоляцииmiddle
- VACUUM, bloat и autovacuummiddle
- CLOG, XID wraparound и MultiXactsenior
- SSI и production-тюнинг autovacuumsenior
- Реальные провалы MVCC, deployment-паттерны и распределённые снимкиsenior
- Connection pool: зачем амортизировать стоимость backend Postgresjunior
- Режимы PgBouncer: session, transaction и statementmiddle
- Размер пула: формула (ядра × 2) + шпинделей и двухуровневый стекmiddle
- Исчерпание пула и idle-in-transaction: сценарий отказа в 3 ночиmiddle
- Миграция на transaction mode: план развёртывания и prepared statements в PgBouncer 1.21middle
- Процессная модель Postgres и почему увеличение max_connections снижает производительностьsenior
- Ландшафт пулеров 2026, serverless connection storms и полная таксономия отказовsenior
- Что такое миграция схемы и почему она заменяет ad-hoc DDLjunior
- ADD COLUMN: мгновенно в PG 11+ против перезаписи в старом Postgresjunior
- Режим отказа очереди блокировок: почему мгновенный DDL может заморозить базуmiddle
- Безопасные DDL-паттерны: NOT VALID, CONCURRENTLY и исправления небезопасных операцийmiddle
- Expand-contract: нулевой простой для ломающих изменений схемыmiddle
- Advisory-блокировки, инструменты миграций и координация деплояsenior
- Таксономия сбоев миграций и дисциплина продакшнаsenior
- Зачем нужно шардирование: потолок одного Postgresjunior
- Выбор ключа шарда: стратегии hash, range, list и directorymiddle
- Партиционирование против шардирования: одно слово, два разных понятияmiddle
- Ко-локация и Citus: инвариант, делающий шардирование пригодным к использованиюmiddle
- Режим отказа hot shard: обнаружение, изоляция и долгосрочная политикаmiddle
- Schema-based шардирование и альтернативы мультиарендностиsenior
- Онлайн-решардинг, 2PC и операционная стоимость шардированияsenior
- Семь актов: от CREATE TABLE до Citusjunior
- Акты 1–3 в глубину: схема, индексы и статистика планировщикаmiddle
- Акты 4–6 в глубину: MVCC bloat, connection pooling и безопасные миграцииmiddle
- Акт 7 в глубину: шардинг, co-location и семиуровневый каскад трейдоффовmiddle
- Наблюдаемость, антипаттерны и производственный триажsenior
- Роли Raft, term и почему majority-кворум предотвращает split brainjunior
- Как Raft реплицирует log entry и решает, что его безопасно коммититьmiddle
- Выборы лидера в Raft: таймауты, правила голосования и четыре свойства безопасностиmiddle
- Raft в реальном мире: partition, медленный диск и клиентская маршрутизацияmiddle
- Расширения Raft: pre-vote, learner, snapshot и линеаризуемые чтенияsenior
- Raft в production: membership change, Multi-Raft и observabilitysenior
- Где происходит data fetching — и почему это решает LCPjunior
- Fetch waterfall''''ы — диагностика и лечение через Promise.allmiddle
- React Server Components и Suspense streamingmiddle
- Клиентский кэш: TanStack Query, SWR и stale-while-revalidatemiddle
- LCP, prefetch и race conditions в интерактивном fetchingmiddle
- Senior internals: RSC payload, слои кэша и production паденияsenior
- Трёхстороннее рукопожатие TCPjunior
- Номера последовательности и состояние соединенияmiddle
- DNS: что делает и зачем существуетjunior
- Обход резолвера: перенаправления, типы записей и gluemiddle
- TTL, кеширование и распространение DNSmiddle
- Рукопожатие за 1 RTT: key share и ECDHEmiddle
- Возобновление сессии и 0-RTTmiddle
- WebSocket: HTTP-апгрейд до постоянного соединенияjunior
- Формат WebSocket-фрейма: opcodes, маскирование, фрагментацияmiddle
- Backpressure в WebSocket: когда клиенты не успеваютmiddle
- Реконнект: jittered backoff, thundering herd, восстановление сообщенийsenior
- WebSocket в масштабе: HTTP/2 мультиплексирование, permessage-deflate, C10Msenior
- WebSocket в production: прокси, безопасность и распределённая архитектураsenior
- Что делают обратные проксиjunior
- Health checks, connection draining и slow startmiddle
- Session affinity, consistent hashing и правильное решениеmiddle
- Retry-бури, circuit breakers и load sheddingsenior
- Устойчивая архитектура LB: anycast, zone-aware маршрутизация и observabilitysenior
- Почему QUIC, а не TCP+TLSjunior
- Connection ID и миграция сетиmiddle
- Возобновление 0-RTT и шифрование пакетовsenior
- DDoS: что это и почему работаетjunior
- Атаки усиления и истощение состоянияmiddle
- Ограничение скорости: алгоритмы и архитектураmiddle
- WAF, межсетевые экраны, mTLS и HSTSmiddle
- Отравление DNS-кэша и BGP-перехватsenior
- Эшелонированная защита и экономика атакsenior
- DNS, TCP, TLS по очереди: куда уходят миллисекундыmiddle
- Перехват прокси и шлюзы безопасности: rate limiter, WAF, mTLSmiddle
- Альтернативные пути: QUIC 0-RTT, WebSocket upgrade, миграция соединенияmiddle
- Наблюдаемость: распределённые трейсы, USE/RED и семплированиеsenior
- Устойчивость: каскадные повторы, circuit breakers и error budgetsenior
- Что такое три сигнала: метрики, логи, трейсыjunior
- Зачем нужны структурные логи: дневник против таблицыjunior
- Схема продакшн-лога: поля, которые несёт каждая строкаmiddle
- PII-редакция и log injectionsenior
- OTel Logs Data Model и audit-логи как подсистемаsenior
- SLI, SLO и error budget: надёжность в числахjunior
- Error budget policy, latency SLO и составные journeysmiddle
- Продакшн-отказы SLO, самонаблюдаемость, безопасность и общая картинаsenior
- Петля инцидента: от пейджера до постмортема до предотвращенияmiddle
- Cache lines и false sharing: когда параллелизм замедляет кодmiddle
- SIMD и data layout: AoS vs SoA и разница в 4–8xmiddle
- Cache-oblivious алгоритмы, PGO и production failuressenior
- GC в production: наблюдаемость, безопасность, edge cases и управление флотомsenior
- Batching: амортизируй фиксированную цену каждой операцииjunior
- Окно батчинга: размер и время ожиданияmiddle
- Batching в Kafka и Postgresmiddle
- io_uring и наблюдаемость пакетированияmiddle
- От Nagle до io_uring: эволюция пакетированияmiddle
- Backpressure, изоляция сбоев и безопасность батчей в продакшенеsenior
- CI enforcement и RUM: делаем бюджеты рабочимиmiddle
- V8 JIT-пайплайн, HTTP-приоритеты и безопасность bundlesenior
- Цикл performance: дисциплина, а не проектjunior
- Классификация и исправление: сопоставление family bottleneck с методамиmiddle
- Observability-стек и CI gates: ловить регрессии до выпускаmiddle
- От инцидента к enforcement: SLO burn до верифицированного исправления за 35 минутmiddle
- Культура, экономика и масштаб performancesenior
- At-most-once, at-least-once, exactly-once: три контракта доставкиjunior
- Три ножки сбоя — где реально происходят дубликаты и потериmiddle
- Consumer-side dedup: самый дешёвый путь к exactly-once processingmiddle
- Kafka exactly-once semantics: idempotent producer и транзакцииmiddle
- SQS visibility timeout, DLQ и outbox patternmiddle
- Exactly-once в production: impossibility-доказательство, гибридные паттерны и реальные инцидентыsenior
- Что такое OAuth и почему пароли — не ответjunior
- Authorization code flow с PKCEmiddle
- Валидация ID-токена и управление JWKS-кешемmiddle
- Ротация refresh-токенов и scope-based least privilegemiddle
- Sender-constrained токены: DPoP и mTLSsenior
- OAuth в production: audience атаки, observability и реальные провалыsenior