Все материалы

Performance · Chromium · Viz

Почему Viz задерживает BeginFrame: поверхности, scheduler и порог в секунду

Автор: Grundy · Проверено: 25 октября 2026

В предыдущей статье мы нашли локальное ожидание между активацией и отправкой кадра. Здесь разбираем отдельную серию Chrome 153: после готовности CSS renderer ещё около половины секунды не отправлял main frame. Трассы и исходники помогли связать это ожидание с доставкой BeginFrame, несовпадением поверхностей и секундным порогом Viz. Это продолжение диагностики, а не исправление сайта или установленная причина медленных запусков Google PageSpeed.

1. Один абзац, отдельная версия Chrome и все 12 запусков

Воспроизводитель уменьшили до HTML на 259 байт и CSS на 91 байт: один абзац системным Arial, без приложения, веб-шрифтов, изображений и анимации. Сборщик через CDP добавлял пассивные наблюдатели времени. Использовали Chrome 153.0.8010.36 на Windows x64, headless, экран 412 × 823 с DPR 1,75, HTTP/2 и CPU ×1. На каждую загрузку создавали новый процесс, профиль и приватный контекст; viewport задавали до остальной настройки CDP. Сетевая эмуляция: одно правило Slow4G с задержкой 562,5 мс, загрузкой 188 743,68 байт/с и отдачей 86 400 байт/с, без дополнительной серверной задержки CSS. Сохранили стандартные флаги chrome-launcher 1.2.2, включая отключённый RenderDocument; сравнительной группы с его включением здесь нет. Из 12 успешных измерений четыре ждали 513,952–533,284 мс от снятия отсрочки main frame до его отправки, восемь — 0,303–0,974 мс. Ошибка настройки до навигации сохранена отдельно и повторена; отдельный пилот схемы исключён. Все успешные завершения трасс сообщили dataLossOccurred=false. Расширенная трассировка способна менять воспроизводимость, поэтому 4 из 12 — не частота проблемы у посетителей.

2. Браузер ждёт новую поверхность, renderer отправляет старую

В четырёх медленных случаях браузер ожидал LocalSurfaceId 11, а renderer продолжал отправлять 9. Это идентификаторы поверхности внутри приёмника кадров; FrameSinkId обозначает сам приёмник. В запуске 5 родительская поверхность сначала ждала нужнего потомка, затем её deadline истёк. После последней агрегации целевого кадра приняли и активировали ещё четыре кадра старой поверхности, но их flow ID отсутствуют в агрегации. Совпадающий код Chromium при истечении deadline снимает зависимости и активирует родителя; активация удаляет регистрацию blocked embedder. Поэтому исключение SendBlockedEmbedded не обязано продолжать защищать потомка от ограничения доставки, даже если новая поверхность ещё не появилась. Состояние регистрации восстановлено по событиям и исходникам, а не прочитано отладчиком. Таблица показывает порядок событий одного запуска; время отсчитано от navigationStart.

Запуск 5: зависимость поверхностей и доставка кадра
СобытиеВремя от navigationStart, мс
Браузер ожидает поверхность 11623,416
Родитель ждёт потомка645,971
Deadline родителя истекает705,409
Первый отказ undrawn в интервале доставки755,416
Отсрочка main frame снята1 237,809
Выдан следующий целевой BeginFrame1 755,390
Целевой main frame отправлен1 756,994
Viz получил поверхность 111 764,866
Поверхность 11 включена в агрегацию1 767,691

3. Что именно ограничивает Viz

В ShouldSendBeginFrame проверка для ещё не нарисованных кадров объединяет три условия ниже. Разность индексов больше трёх — не длина очереди и не число неподтверждённых отправок: индексы монотонно растут. Четыре принятых кадра без агрегации согласуются с этой проверкой, но не заменяют прямой замер внутренних счётчиков. last_frame_time_ обновляется при разрешённой доставке BeginFrame, а не при загрузке CSS. Источник продолжает выдавать такты; ограничивается доставка клиенту. Точная ветка ThrottleUndrawnFrames отличается от ThrottleUnresponsiveClient, упомянутой в заголовке обсуждения Lighthouse.

(frame_time - last_frame_time_ < base::Seconds(1)) &&
(active_frame_index - last_drawn_frame_index_ > 3) &&
active_metadata.may_throttle_if_undrawn_frames

4. Как секундный порог разрешает следующую доставку

Сравнили frame_time источника с предыдущим разрешённым BeginFrame, а не со временем выполнения callback. В запуске 5 отказ ещё записан при разнице 999 999 мкс. Следующий доступный такт пришёл при 1 016 739 мкс: строгое условие «меньше секунды» уже ложно. В остальных трёх случаях доставка возобновилась при 1 000 045–1 000 084 мкс. Во всех четырёх положительное решение — SendFrameTiming. Когда проверка ненарисованных кадров перестаёт блокировать доставку, эта ветка не требует нового успешного показа: сохранённая обратная связь может содержать только неудачные показы. Нужная поверхность 11 поступила уже после возобновления BeginFrame. Разрешённая доставка обновляет last_frame_time_, поэтому это периодическая возможность продолжить работу, а не постоянное выключение ограничения. Интервал около секунды считается от предыдущей доставки; оставшееся ожидание после CSS здесь составляло около 514–533 мс.

Все четыре медленных запуска: время источника и оставшееся ожидание
ЗапускРазница при последнем отказе, мксРазница при возобновлении, мксОт снятия отсрочки до отправки main frame, мс
5999 9991 016 739519,185
7983 3791 000 071522,903
8983 4141 000 084513,952
9983 3851 000 045533,284

Источник: опубликованный отчёт Viz и полные трассы в Lighthouse #17148. Это нативное время воспроизводителя при CPU ×1, не значения LCP из PageSpeed.

5. Почему готовый renderer не отправляет main frame

Сразу после снятия CSS-отсрочки scheduler сообщает состояние ниже: работа нужна, но main и impl остаются IDLE, а next_action — NONE. Проверка «main frame уже отправлен в этом интервале» не объясняет ожидание: did_send уже false. Подходящий guard в исходниках — ShouldBlockBeginMainFrameWhenIdle() && !urgent_begin_main_frame_pending_. Для асинхронного compositor первая часть проверяет IDLE у impl frame. Когда следующий BeginFrame доставлен, состояние меняется на INSIDE_BEGIN_FRAME и сразу выбирается SEND_BEGIN_MAIN_FRAME. В запуске 5 BeginFrame получен на 1756,325 мс, действие выбрано на 1756,940 мс, отправка произошла на 1756,994 мс. Urgent, pause_rendering и настройка синхронного compositor не экспортированы в этих снимках: их состояние восстановлено по начальным значениям и путям setter, записанным с момента до создания страницы. Нативной точки останова на возврате guard не было.

needs_begin_main_frame = true
defer_begin_main_frame = false
did_send_begin_main_frame_for_current_frame = false
has_pending_tree = false
pending_submit_frames = 0
begin_main_frame_state = IDLE
begin_impl_frame_state = IDLE
next_action = NONE

6. Что установлено, а что ещё требует проверки

Разрешённые доставки связаны с целевым renderer точными flow ID, а отправки кадров определяют уникальный FrameSinkId. В записях отказа ID приёмника отсутствует: нельзя присвоить каждый отказ нашей поверхности только по близости времени. Внутренние active_frame_index, last_drawn_frame_index и флаг may_throttle_if_undrawn_frames напрямую не сняты; MarkAsDrawn вызывается и при обработке copy-output, поэтому агрегацию нельзя механически считать его счётчиком. Зато наблюдаемый интервал при возобновлении сам по себе делает временное условие ложным независимо от неизвестных точных индексов. Исходная серия Chrome 155 показывала ожидание после активации, эта серия Chrome 153 — до отправки main frame. Их нельзя объединять в одну выборку. У нас нет полной трассы медленного Google PSI, и совпадение версии браузера не доказывает совпадение его внутренней конфигурации.

7. Отчёт, пять полных трасс и вопрос разработчикам

Дополнение опубликовано в Lighthouse #17148. К нему приложены архив отчёта и пять полных очищенных трасс: четыре медленных запуска и один быстрый контроль. В отчёте сохранены настройки сбора, производные цепочки событий, ссылки на исходники и контрольные суммы. Из потоков заменены личные пути и имя хоста воспроизводителя; числовые значения и идентификаторы сохранены, события не вырезаны. ZIP запуска 9 использует LZMA: его содержимое побайтно совпадает с исходным Deflate-архивом, для распаковки подходят 7-Zip или Python zipfile. Разработчиков попросили проверить переход от deadline родителя к ограничению потомка и добавить в событие отказа FrameSinkId, оба индекса, флаг metadata, время источника и последней доставки, состояние blocked embedder и использованное исключение. Это позволит проверить оставшиеся восстановленные поля напрямую. Обходное решение для production по этим данным не внедрялось.