Observability в проде: LoAF, INP и полная поверхность атаки
LoAF и INP в продовой телеметрии, скролл вне главного потока, display locking, reduced-motion, Web Workers, Service Workers, CI-тестирование и полная поверхность атаки пайплайна.
Ваша страница набирает 95 в Lighthouse в офисе. На Pixel 4a пользователя в Джакарте с 3G-троттлингом INP — 340 мс. Это два разных измерения двух разных вещей. Только одно из них важно.
LoAF и INP: два взаимодополняющих сигнала
PerformanceLongAnimationFrameTiming (LoAF, Long Animation Frame — длинный кадр анимации; поставлено в Chromium в 2023–2024) сообщает о каждом кадре длиннее 50 мс с разбивкой того, что доминировало: время рендеринга, блокирующий JS, форсированная компоновка. Это диагностический инструмент — говорит что работало долго, а не что почувствовал пользователь.
INP (Interaction to Next Paint — взаимодействие до следующей отрисовки) измеряет время от пользовательского input-события (клик, клавиша, тап) до следующего paint, видимо реагирующего. INP стал Core Web Vital в марте 2024, заменив FID. Плохой INP (>200 мс p75) почти всегда восходит к одной из двух проблем пайплайна:
- Длинная JS-задача в input-обработчике, откладывающая rAF
- Форсированная синхронная компоновка от чтения геометрии внутри обработчика
LoAF даёт данные для атрибуции в продакшене; INP даёт метрику, которую чувствует пользователь. Вместе они замыкают петлю от продовой телеметрии к конкретным диагностикам пайплайна.
Путь диагностики INP
INP > 200 мс — пользователь воспринимает взаимодействие вялым
LoAF — какой кадр, что доминировало (JS / компоновка / рендеринг)
Скролл вне главного потока
Казалось бы, скролл всегда «бесплатный» — пользователь воспринимает его как мгновенный. Когда он вдруг становится вязким, понимание того, почему он откатился на главный поток, сразу подсказывает, как это починить.
Браузеры поставляют скролл на потоке compositor последнее десятилетие: когда пользователь скроллит обычную страницу, compositor транслирует viewport на GPU без касания главного потока. Страница может скроллить плавно даже пока идёт долгая JS-задача.
Подводный камень: любой элемент с JS scroll-обработчиком, подключённым не-passively (без {passive: true}), заставляет браузер откатиться на скролл главного потока, потому что обработчик может вызвать preventDefault(). Всегда передавайте passive: true слушателям scroll, wheel и touchmove, если только вам действительно не нужен preventDefault. Современные браузеры предупреждают в DevTools, когда non-passive слушатель задерживает скролл.
Display locking и content-visibility
Механизм за content-visibility: auto — «display locking»: браузер приостанавливает рендеринг заблокированного поддерева (без пересчёта стилей, без компоновки, без отрисовки) и заменяет его placeholder intrinsic-size. Когда поддерево пересекает viewport через внутренний IntersectionObserver браузера, оно разблокируется и рендерится.
Числа: таблица из 10 000 строк, ранее стоившая 1 200 мс style + layout, рендерится за ~10 мс с content-visibility: auto.
Компромисс: скролл в ранее заблокированную область кратко останавливается для рендеринга. Если строки дорогостоящие, виден однокадровый рывок на границе разблокировки. Пары с contain-intrinsic-size даёте браузеру реалистичный placeholder-размер, чтобы позиция скролла не прыгала.
Reduced-motion как клапан бюджета рендеринга
@media (prefers-reduced-motion: reduce) устанавливается пользователями, испытывающими укачивание или желающими снизить энергопотребление. Помимо доступности, это клапан бюджета рендеринга: любую compositor-управляемую анимацию можно заменить мгновенным изменением состояния при reduced-motion, освобождая compositor от покадровой работы. Пользователи с ограниченным зарядом на бюджетном Android-телефоне получают значимую экономию батареи.
Web Workers и разгрузка главного потока
Когда главный поток достигает потолка, единственный способ снизить его — перенести работу. Web Workers выполняют JS в отдельном потоке без доступа к DOM; сериализация через postMessage стоит ~1 мс/МБ, что оправдано для CPU-тяжёлых задач (парсинг большого JSON, сжатие, криптография, обработка разметки).
OffscreenCanvas даёт прямой доступ к canvas API из воркера, полностью обходя главный поток. SharedArrayBuffer + Atomics предоставляют примитивы синхронизации между потоками для высокочастотных данных (аудио, потоки сенсоров реального времени). Стоимость входа высока (structured cloning, архитектура передачи сообщений), но потолок производительности пропорционально выше: четырёхъядерный телефон с занятым главным потоком всё ещё имеет 3 незадействованных ядра, которые большинство приложений никогда не используют.
Service Worker и пайплайн первой отрисовки
Service Workers не часть пайплайна рендеринга, но критически влияют на его входные данные. Service Worker, отвечающий на запросы из кэша (cache-first для статических ресурсов, network-first для API), доставляет первую отрисовку за 50 мс на повторных посещениях вместо 500 мс — 4 кадра против 30 при 60 fps.
Ключевое ограничение: скрипт Service Worker работает в отдельном потоке, но его запуск (когда он перехватывает первый запрос) имеет небольшую латентность. Держите Service Worker тонким и быстрым; 200-мс запуск при перехвате fetch задерживает первый HTML-байт и весь пайплайн парсинга вместе с ним.
Performance CI: перехват регрессий до мерджа
Бюджеты удерживаются, только когда регрессии перехватываются до мерджа. Реалистичный CI-пайплайн имеет три уровня:
- Lighthouse CI в headless Chrome на каждом PR — ограничивает по абсолютным метрикам (LCP < 2.5 с, INP < 200 мс, CLS < 0.1), блокирует мердж при регрессии.
- Синтетические бенчмарки на критических сценариях (открыть страницу, проскроллить список 1000×, нажать 5 кнопок) — измеряет p95 длительности кадра и p95 LoAF, сравнивает с baseline-веткой.
- RUM (Real User Monitoring — мониторинг реальных пользователей) в продакшене — отправляет перцентили INP, LCP, CLS в Datadog или подобное, алертит когда p75 INP пересекает 200 мс на любом сегменте пользователей (страна, устройство, версия приложения).
Без всех трёх уровней регрессия рендеринга попадает в продакшен, живёт там тихо месяцами и обнаруживается, когда жалоба пользователя заставляет кого-то смотреть.
Профилирование на реальном железе vs DevTools-троттлинг. «4-кратное замедление CPU» в DevTools Performance — приближение, не замена реального железа. M2 MacBook при 4-кратном замедлении всё равно имеет другую модель памяти, другой GPU и другой профиль теплового троттлинга, чем Pixel 6a. Профилируйте на физическом среднеценовом Android хотя бы раз в неделю с помощью Chrome для Android + удалённая отладка, и сравнивайте длительности кадров. Разница >30% означает мобильную регрессию, невидимую на десктопе.
▸Граничные случаи
Layer squashing — ответ compositor на взрыв слоёв из-за перекрытия. Когда много соседних неанимирующихся элементов продвигаются (из-за перекрытия с единственным анимированным слоем), compositor сжимает их в единый общий «squashed layer» bitmap. Это уменьшает GPU-память ценой большего одиночного bitmap — если один элемент в squash меняется, весь squashed layer должен перерисоваться. Эвристика squashing не управляется пользователем; единственное лечение — изолировать анимированные слои от неанимирующихся соседей, чтобы правило перекрытия не срабатывало.
Спроектируйте поведение скролла для виртуализированного списка чата, содержащего 50 000 сообщений, с достижением 60 fps на среднеценовом Android-телефоне.
- Бюджет кадра: 16.67 мс. Реалистичный бюджет главного потока после оверхеда браузера: ~10 мс.
- Компоновка не должна зависеть от off-screen строк.
- Composite-only путь во время скролла. Компоновка и отрисовка допустимы только когда новые строки входят в viewport.
- GPU-память: предположим 200 МБ доступно. Количество слоёв должно оставаться ниже 30 в любой момент.
- Resize-обработчик не должен зацикливать чтения и записи (без форсированного reflow).
- Виртуализация ограничивает размер DOM независимо от размера датасета.
- Transform-позиционирование направляет скролл через compositor.
- Scroll-обработчик без измерений — чистая математика от scrollTop.
- Will-change ограничен анимируемым элементом и активным окном.
- Resize батчится: все чтения, затем все записи.
Click-обработчик выполняется 80 мс. Пользователь воспринимает задержку перед обновлением UI. Какой Core Web Vital это измеряет и что с этим связано в пайплайне?
Touchmove-слушатель подключён без `{passive: true}`. Какая регрессия производительности это вызывает?
- 01Что сообщает LoAF, и чем отличается от INP?
- 02Почему non-passive scroll/touchmove-слушатели вызывают jank?
- 03Назовите три уровня CI для перехвата регрессий рендеринга до продакшена.
LoAF атрибутирует долгие кадры; INP измеряет воспринятую латентность взаимодействия — плохой INP (>200 мс p75) восходит к длинной JS-задаче или форсированной синхронной компоновке в input-обработчике. Non-passive scroll-слушатели откатываются на скролл главного потока; passive: true восстанавливает compositor-скролл. content-visibility: auto display-блокирует off-screen поддеревья, снижая 1200-мс компоновку до 10 мс. @media (prefers-reduced-motion) — одновременно требование доступности и оптимизация бюджета рендеринга. Web Workers разгружают CPU-тяжёлый JS с главного потока; Service Workers обслуживают первую отрисовку из кэша. CI нужны три слоя: Lighthouse на PR, синтетический p95 LoAF на критических путях и RUM-алерты на продовые INP-перцентили. Полная поверхность атаки — блокирующие парсер скрипты, раздутый CSS, сложные селекторы, глубокие flex-компоновки, paint-тяжёлые фильтры, переполнение слоёв, layout thrash, non-passive слушатели, задачи >50 мс и долгие input-обработчики — отображается один к одному на конкретные стадии пайплайна. Теперь, когда пользователь на бюджетном Android в Джакарте сообщает о вялом тапе, у вас есть петля телеметрии: INP даёт взаимодействие, LoAF — кадр, карта стоимости стадий — лечение.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
встречается в188
- Почему 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
- Что такое движок JavaScriptzero
- Ignition и байт-кодmiddle
- Внутри цикла интерпретатораmiddle
- Как V8 представляет значениеmiddle
- Что такое Map на самом деле: вскрываем hidden classmiddle
- Деревья переходов, deprecation и миграцияmiddle
- Быстрые свойства, медленные свойства и slack trackingmiddle
- Inline cache в глубину: feedback-слоты и handlersmiddle
- Monomorphic, polymorphic, megamorphic — и stub cachemiddle
- Четыре яруса и как работает разогревmiddle
- Обратная связь о типах: топливо для оптимизатораsenior
- TurboFan: оптимизатор на море узловsenior
- Спекуляция и guardssenior
- Деоптимизация: падение с обрываsenior
- On-Stack Replacement: подмена фрейма посреди циклаsenior
- Области видимости, контексты и scope chainmiddle
- Что на самом деле удерживает замыканиеmiddle
- Когда V8 аллоцирует Context (и чего это стоит)middle
- Замыкания, feedback-векторы и полиморфизм точки вызоваmiddle
- Зачем нужен GC и достижимостьmiddle
- Утечки в языке со сборкой мусораsenior
- Движок против циклаmiddle
- Микрозадачи против макрозадач: порядокmiddle
- Внутри Promisemiddle
- async/await, десахаризированныйmiddle
- Капстоун: оптимизируем горячий путьsenior
- Конверт IPjunior
- Читаем IP-заголовокmiddle
- Трёхстороннее рукопожатие TCPjunior
- Номера последовательности и состояние соединенияmiddle
- DNS: что делает и зачем существуетjunior
- Обход резолвера: перенаправления, типы записей и gluemiddle
- TTL, кеширование и распространение DNSmiddle
- Что делает TLS и зачем он нуженjunior
- Рукопожатие за 1 RTT: key share и ECDHEmiddle
- Возобновление сессии и 0-RTTmiddle
- Расписание ключей, SNI, ALPN и расширенияsenior
- Защита 0-RTT, ECH, гибридный PQ и продакшн TLSsenior
- 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
- Двенадцать слоёв: один URL, семь действующих лицjunior
- 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
- Что такое OpenTelemetry: API, SDK, Collector, OTLPjunior
- Сигналы OTel, Semantic Conventions и проводной формат OTLPmiddle
- Collector OTel: receivers, processors, exporters и паттерны развёртыванияmiddle
- Vendor-нейтральность, eBPF-инструментирование, Operator и OTel в браузере и serverlesssenior
- Эксплуатация OTel Collector: надёжность, version skew, режимы отказа и управлениеsenior
- SLI, SLO и error budget: надёжность в числахjunior
- Error budget policy, latency SLO и составные journeysmiddle
- Продакшн-отказы SLO, самонаблюдаемость, безопасность и общая картинаsenior
- Что такое trace propagation и почему сломанная propagation хуже отсутствия трейсовjunior
- traceparent и tracestate: полный формат W3C-заголовкаmiddle
- Baggage и async-границы: перенос контекста через очереди и callback''''иmiddle
- Async context на разных языках, service mesh, миграция B3 и безопасностьsenior
- Production-сбои propagation, span links и платформенный дизайнsenior
- Debugging-воронка: SLO → RED → trace → profilejunior
- Архитектура OTel: один SDK, четыре сигнала, один wire-форматmiddle
- Петля инцидента: от пейджера до постмортема до предотвращенияmiddle
- Масштаб, безопасность и ROI наблюдаемых системsenior
- 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
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.