LCP, prefetch и race conditions в интерактивном fetching
Как диагностировать LCP регрессии по network waterfall, что даёт prefetch и чего стоит, и как устранить race conditions в поиске по мере ввода.
Пользователь вводит “react” в поле поиска. Запускаются два fetch’а — для “reac” и “react”. Ответ на “reac” приходит на 200ms позже “react” из-за медленного сервера. UI показывает результаты “reac” пока в поле введено “react”. Классический race condition.
LCP и критический fetch
К концу урока ты сможешь прочитать Network waterfall, объяснить LCP-регрессию одной фразой и назвать конкретный фикс — ещё до того, как откроешь конфиг.
Largest Contentful Paint измеряет когда основной контент-элемент страницы становится виден. Для страницы товара LCP-элемент обычно — hero image или заголовок товара. Пороги Web Vitals:
- Хорошо: LCP менее 2.5s
- Отлично: LCP менее 1.5s
Самое большое улучшение LCP, которое могут сделать большинство приложений: перенести data fetching LCP-элемента с клиента на сервер. Client-fetched заголовок добавляет загрузку JS + монтирование + fetch к LCP critical path — легко в 2–3 раза дольше, чем эквивалентная SSR страница.
Дополнительные инструменты:
fetchpriority="high"на LCP изображении сигнализирует браузеру приоритизировать его обнаружение при парсинге<link rel="preload">для LCP изображений, URL которых известен при сборке, раньше достигают браузера
Диагностика waterfall’а из DevTools
Реальный паттерн регрессии в production: четыре последовательных API вызова, каждый начинается только после предыдущего ответа:
GET /products/42 50ms → 250ms
GET /app-bundle.js 260ms → 850ms
GET /api/product/42 860ms → 1300ms (useEffect, после bundle)
GET /api/reviews 1310ms → 1700ms (после product)
GET /api/user 1710ms → 2000ms (после reviews)
LCP на 2050msТри антипаттерна в одном waterfall’е:
- Запросы 3–5 последовательны, хотя независимы — reviews и user не обязаны ждать ответ на product
- Все три запускаются useEffect’ом — ждут загрузки JS bundle, парсинга, монтирования
- bundle.js слишком большой (~590ms) — code-split критический LCP-путь до 50KB
Все три усиливают друг друга: большой bundle задерживает срабатывание useEffect, который задерживает последовательные fetch’и. Исправь любой один — срежешь 300–600ms; исправь все три — LCP падает с 2s ниже 400ms.
После фикса — RSC с параллельными серверными fetch’ами, небольшой интерактивный bundle:
GET /index.html (streaming) 50ms → 80ms TTFB
streaming чанки 80ms → 300ms LCP ~320msRace conditions в интерактивном fetching
Race condition поиска по вводу: пользователь вводит “rea” → fetch A запускается. Вводит “react” → fetch B. Сервер медленнее для fetch A. Fetch B разрешается первым (результаты для “react”), UI обновляется. Потом приходит fetch A (результаты для “rea”), UI перезаписывается неверными результатами.
Фикс 1: AbortController — отменить in-flight запрос при новом нажатии клавиши:
let controller = new AbortController();
function search(query: string) {
controller.abort();
controller = new AbortController();
return fetch(`/api/search?q=${query}`, { signal: controller.signal });
}TanStack Query автоматически передаёт AbortSignal через meta queryFn’а — запросы для старого ключа отменяются при изменении ключа.
Фикс 2: Debounce — ждать пока пользователь перестанет печатать перед запуском:
const debouncedQuery = useDebounce(inputValue, 300);
const { data } = useQuery({
queryKey: ['search', debouncedQuery],
queryFn: () => searchProducts(debouncedQuery),
enabled: debouncedQuery.length > 1,
});Задержка 300ms означает один fetch на паузу в наборе.
Фикс 3: queryKey versioning (встроено в TanStack Query) — каждый ответ помечен queryKey который его произвёл. Кэш хранит только данные последнего ключа. Устаревшие ответы для старых ключей отбрасываются автоматически.
Стратегии prefetch
Зачем ждать клика, если можно предсказать, куда идёт пользователь? Prefetch — это ответ. Но только если применять его избирательно.
Умный fetching может начать работу до того, как пользователь кликнет:
Hover prefetch: когда курсор входит в ссылку, начать fetch’ить цель. Пользователь обычно кликает через 100–300ms после hover — данные часто готовы до клика.
<Link
href={`/product/${id}`}
onMouseEnter={() => queryClient.prefetchQuery({
queryKey: ['product', id],
queryFn: () => fetchProduct(id),
})}
>
Смотреть товар
</Link>Viewport prefetch: когда ссылка прокручивается в область видимости, начать prefetch. Полезно для следующей страницы в infinite scroll списке.
Next.js Link по умолчанию: <Link href="..."> делает prefetch автоматически в production при нахождении в viewport. Отключить через prefetch={false} если важен трафик.
Компромисс: prefetch использует bandwidth спекулятивно. На мобильном с лимитированным трафиком избыточный prefetch дорог. Ограничить следующими вероятными целями; избегать prefetch крупных медиа.
Пагинация: cursor vs offset
Offset пагинация: ?page=2&limit=20. Просто, но ломается при конкурентных вставках: если новый элемент вставлен между fetch’ами страницы 1 и 2, элементы смещаются и пользователь видит дубликат или пропуск.
Cursor пагинация: ?cursor=abc123. Стабильна при вставках потому что cursor маркирует позицию в наборе данных, а не счётчик. TanStack Query useInfiniteQuery нативно обрабатывает cursor-based с getNextPageParam извлекающим следующий cursor из каждого ответа.
Использовать cursor пагинацию для любого списка под нагрузкой. Offset приемлем только для статических или редко изменяемых данных.
- LCP порог 'хорошо'
- менее 2.5 s
- LCP порог 'отлично'
- менее 1.5 s
- Задержка hover-to-click (типично)
- 100–300 ms
- Debounce для поисковых полей
- 200–400 ms
- Ускорение Promise.all при N=5
- ~5x vs последовательный
Страница имеет LCP 4.2s. DevTools показывает лесенку из 3 последовательных /api fetch'ей общим временем 1500ms. Какой лучший первый фикс?
Поисковое поле запускает fetch при каждом нажатии клавиши. Пользователь вводит 'react' и видит результаты 'reac'. Лучший фикс?
- 01Каковы LCP пороги 'хорошо' и 'отлично'?
- 02Что такое race condition в поисковом поле и как AbortController его исправляет?
- 03Почему cursor пагинация предпочтительнее offset для изменяемых данных?
LCP (Largest Contentful Paint — время до появления главного контент-элемента) измеряет когда основной контент-элемент страницы становится виден; Web Vitals цели — менее 2.5s (хорошо) и 1.5s (отлично). Самое большое улучшение LCP — перенести данные LCP-элемента с клиента на сервер, убирая 2–3 последовательных trip’а из critical path. Диагностировать LCP регрессии: ищи лесенку в Network panel — последовательные запросы где начало каждого совпадает с концом предыдущего. Фикс: Promise.all или RSC для независимых серверных fetch’ей; AbortController или debounce для клиентских интерактивных потоков. Prefetch при hover для почти мгновенных навигаций, но ограничить спекулятивный prefetch на мобильном для экономии трафика. Теперь, когда тимейт сообщит о «неправильных результатах поиска при быстрой печати», ты первым делом потянешься к AbortController — а не к спиннеру или сбросу кэша.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
встречается в204
- Почему 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
- Event loop: один поток, три очередиjunior
- Задачи, микрозадачи и scheduler.yield()middle
- Голодание микрозадач, длинные задачи и LoAFsenior
- Event loop Node.js: фазы, nextTick и задержка циклаsenior
- React, Vue и наблюдаемость INP в продакшенеsenior
- Render pipeline: шесть стадий от байтов до пикселейjunior
- Цена стадий и модель процесса рендерераmiddle
- Инвалидация, dirty-биты и containmiddle
- Слои композитора: продвижение, перекрытие и память GPUmiddle
- Флейм-стрип DevTools и жизненный цикл кадраmiddle
- Layout thrash: форсированная синхронная компоновкаsenior
- BeginMainFrame, анимации на потоке compositor и память GPUsenior
- Observability в проде: LoAF, INP и полная поверхность атакиsenior
- Что такое V8 и почему производительность различается в 100 разjunior
- Четырёхуровневый JIT-конвейер V8 и профилированная тиеризацияmiddle
- Hidden classes, деревья переходов и расположение в памятиmiddle
- Inline caches, состояния IC и деоптимизацияmiddle
- Orinoco GC: параллельный scavenger, конкурентная разметка и барьеры записиmiddle
- Спекулятивный движок TurboFan и ловушка deopt-loopsenior
- V8 в production: Isolates, сжатие указателей и реальные аварииsenior
- Жизненный цикл service worker и стратегии кешированияmiddle
- Граничные случаи service worker: version skew, долговременность и ловушка навигацииsenior
- Что делает реконсилер: render vs commitjunior
- Объект fiber и дерево с двойной буферизациейmiddle
- Чистота фазы render и подшаги фазы commitmiddle
- Реконсиляция: эвристики диффа и ловушка ключейmiddle
- Приоритетные lanes, time-slicing и useTransitionmiddle
- Bailout, мемоизация и tearingsenior
- React Profiler, компилятор и продакшн-наблюдаемостьsenior
- Стратегии рендеринга: SSG, SSR, ISR, streaming и гидратацияjunior
- SSG, SSR, ISR, streaming и RSC — как работает каждая стратегияmiddle
- Цена гидратации: selective, progressive, острова, resumabilitymiddle
- Hydration mismatch: причины, обнаружение и правило детерминизмаsenior
- RSC, стратегия на маршрут и production-наблюдаемостьsenior
- Core Web Vitals: что измеряют LCP, INP и CLSjunior
- CLS: почему происходят сдвиги лейаута и как их остановитьmiddle
- Трейдоффы метрик, RUM-атрибуция и цикл CI+полеsenior
- Общая картина: от URL до LCP до INP как эстафетаjunior
- Восемь слоёв трассировки: от service worker до второй навигацииmiddle
- Пять канонических поломок: где производство стабильно ломаетсяsenior
- Метод трёх треков: чтение трасс и построение системы мониторинга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
- Что такое движок 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
- Трёхстороннее рукопожатие 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
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.