awesome-everything EN
↑ Обратно к восхождению

Браузер и фронтенд-рантайм

React, Vue и наблюдаемость INP в продакшене

Суть Как конкурентный планировщик React 18 и микрозадачный бэтчер Vue 3 отображаются на event loop, и как построить конвейер поиска причин INP от LoAF-телеметрии до деплоя.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 15 min

Пользователи сообщают, что строка поиска на вашем React-дашборде «ощущается медленной». INP показывает 380 мс p75. Вы деплоите фикс, и INP падает до 90 мс. Три недели спустя вас не будят в два ночи, когда PR другого инженера откатывает улучшение — потому что вы так и не подключили LoAF-телеметрию к CI-конвейеру.

Как работает планировщик React 18

Планировщик React выполняет рекоцилиацию кооперативными кусками по 5 мс. После каждого куска он уступает браузеру с помощью MessageChannel.postMessage — единственного кросс-браузерного механизма, ставящего задачу без зажима 4 мс от setTimeout. Цикл браузера получает возможность запустить ввод, рендеринг и другие задачи; React возобновляется на следующей задаче для следующего куска 5 мс.

Это реализация «конкурентного React»: один рендер разбивается на множество задач, ни одна из которых не превышает порог длинной задачи 50 мс. useTransition помечает обновление состояния как низкоприоритетное, позволяя обновлениям ввода прерывать его. Понять цикл — и планировщик React перестаёт быть загадкой.

Детали конкурентного планировщика React 18
Размер куска работы
5 мс
Механизм уступки
MessageChannel.postMessage (без зажима 4 мс)
useTransition
помечает обновление как прерываемое вводом
Максимальная длительность задачи
< 50 мс (безопасно для long-task)
Возобновление
следующая задача в event loop

Планировщик Vue 3

Vue 3 пакетирует реактивные обновления в микрозадаче: запись в ref планирует микрозадачу, которая сбрасывает все ожидающие обновления в конце текущей задачи. Это означает, что множество записей в одной задаче сворачивается в одно обновление — но это также означает, что сброс является микрозадачей и может продлевать голодание, если он вызывает больше реактивных записей.

Vue предоставляет nextTick(callback) для кода, который хочет выполниться после сброса; под капотом это просто ещё одна микрозадача, запланированная после микрозадачи сброса.

Викторина

React 18 использует `MessageChannel.postMessage` для уступки между кусками рекоцилиации вместо `setTimeout(fn, 0)`. Почему?

Викторина

Компонент Vue 3 записывает три ref в одном синхронном блоке. Сколько DOM-обновлений последует?

Конвейер поиска причин INP в продакшене

Рецепт поиска причин INP в продакшене:

Подпишитесь на записи LoAF с PerformanceObserver. Для каждой записи фиксируйте firstUIEventTimestamp, renderStart записи и массив scripts с атрибуцией. Когда INP срабатывает для медленного взаимодействия (тоже через PerformanceObserver), сопоставьте по id взаимодействия: найдите LoAF, перекрывавший ввод, атрибутируйте время самому тяжёлому скрипту, отправьте в телеметрию с URL скрипта и именем функции (разрешённым через sourcemap на стороне сервера). Этот конвейер превращает «пользователи жалуются на тормоза» в «деплой 3a4f1c регрессировал поле поиска, добавив вызов редьюсера на 320 мс».

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.entryType === 'long-animation-frame') {
      const heaviest = entry.scripts
        .sort((a, b) => b.duration - a.duration)[0];
      sendToTelemetry({
        frameStart: entry.startTime,
        frameDuration: entry.duration,
        scriptUrl: heaviest?.sourceURL,
        functionName: heaviest?.sourceFunctionName,
        charPosition: heaviest?.sourceCharPosition,
      });
    }
  }
});
observer.observe({ type: 'long-animation-frame', buffered: true });

INP как CI-гейт. Продакшен-телеметрия ловит регрессии после того, как они достигли пользователей. Реалистичный гейт: синтетический тест Playwright/Puppeteer в headless Chrome воспроизводит критическое взаимодействие (набрать запрос в поиске, открыть меню, переключить вкладку), измеряет INP через тот же код PerformanceObserver, что и в продакшене, и блокирует слияние, если p75 превысил бюджет. Ключевая деталь: headless Chrome на CI-раннере обычно быстрее среднего телефона пользователя, поэтому без --cpu-throttling-rate=4 (или эквивалента CDP) тест проходит локально и падает в продакшене. Бюджеты размера бандла добавляют второй слой: размер JS напрямую конвертируется во время разбора+компиляции, которое конвертируется в длинные задачи при загрузке. Без всех трёх слоёв — синтетического INP-гейта, бюджета бандла и продакшен LoAF-алертов — регрессия проскальзывает тихо и живёт до следующего ручного профилирования.

Викторина

Массив `scripts` записи LoAF показывает длительность 280 мс, атрибутированную `reducerRootReducer` в `bundle.js:1:94821`. Каков следующий шаг для получения файл:строка в исходном коде?

Почему это работает

Трёхслойный стек наблюдаемости — синтетический CI-гейт, бюджет бандла, продакшен LoAF-алерты — аналог трёхслойного стека троттлинга таймеров. Обе проблемы имеют накапливающиеся факторы, которые ни один инструмент не поймает в одиночку. Синтетический тест ловит регрессии до того, как их увидят пользователи, но пропускает разнообразие устройств; продакшен LoAF-алерты ловят то, что пропустил синтетик, но приходят после того, как реальные пользователи пострадали; бюджеты бандлов предотвращают проблему до её появления в коде. Без всех трёх — оптимизируете один слой, пока другие текут.

Викторина

Ваш синтетический INP-тест в CI проходит при p75 = 120 мс. Реальный пользовательский INP p75 — 480 мс. Что наиболее вероятно объясняет это?

Вспомните перед уходом
  1. 01
    Объясните, как конкурентный рендеринг React 18 держит отдельные задачи ниже порога длинной задачи 50 мс.
  2. 02
    Опишите трёхслойный стек наблюдаемости INP и почему нужны все три.
  3. 03
    Компонент Vue 3 записывает 10 ref в цикле. Будет ли 10 микрозадачных сбросов или 1? Что происходит, если сброс вызывает больше реактивных записей?
Итог

Конкурентный планировщик React 18 использует MessageChannel.postMessage для уступки между кусками рекоцилиации по 5 мс — уступка уровня задачи без зажима 4 мс, чередующая работу React с вводом и шагами рендеринга браузера. useTransition помечает обновления как прерываемые, позволяя срочному вводу вклиниться перед медленной рекоцилиацией. Vue 3 пакетирует реактивные обновления в единственный микрозадачный сброс за синхронный блок через nextTick, что эффективно, но может создавать микрозадачные цепочки если колбэки сброса вызывают больше реактивных записей. В продакшене поиск причин INP требует трёх совместно работающих инструментов: PerformanceObserver LoAF, записывающий атрибуцию по скриптам; sourcemap-сервер, разрешающий URL скриптов и позиции символов к исходному файл:строка; и CI-гейтный синтетический INP-тест с троттлингом CPU, применяющий бюджет до достижения регрессий реальными пользователями. Без троттлинга CPU CI-раннеры слишком быстрые, чтобы поймать регрессии, которые появляются только на средних телефонах.

Связанные уроки
встречается в143
Продолжить восхождение ↑Event loop: тест с выбором ответа
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.