Подробное описание рендерингаNG: BlinkNG

Стефан Загер
Stefan Zager
Крис Харрельсон
Chris Harrelson

Blink относится к реализации веб-платформы Chromium и охватывает все этапы рендеринга перед композицией, кульминацией которых является коммит компоновщика . Подробнее об архитектуре рендеринга Blink можно прочитать в предыдущей статье этой серии.

Blink начал свою жизнь как форк WebKit , который сам по себе является форком KHTML , созданным в 1998 году. Он содержит один из старейших (и наиболее важных) кодов в Chromium, и к 2014 году он определенно показал свой возраст. В том году мы приступили к реализации ряда амбициозных проектов под лозунгом того, что мы называем BlinkNG, с целью устранения давних недостатков в организации и структуре кода Blink. В этой статье мы рассмотрим BlinkNG и входящие в него проекты: почему мы их сделали, чего они достигли, руководящие принципы, которые сформировали их дизайн, и возможности для будущих улучшений, которые они предоставляют.

Конвейер рендеринга до и после BlinkNG.

Рендеринг до NG

Конвейер рендеринга в Blink всегда был концептуально разделен на этапы ( стиль , макет , рисование и т. д.), но барьеры абстракции были ненадежными. Грубо говоря, данные, связанные с рендерингом, состояли из долгоживущих изменяемых объектов. Эти объекты могли быть изменены (и были изменены) в любое время и часто перерабатывались и повторно использовались при последовательных обновлениях рендеринга. Невозможно было достоверно ответить на простые вопросы, такие как:

  • Нужно ли обновлять вывод стиля, макета или краски?
  • Когда эти данные получат свое «окончательное» значение?
  • Когда можно изменить эти данные?
  • Когда этот объект будет удален?

Тому есть множество примеров, в том числе:

Style будет генерировать ComputedStyle на основе таблиц стилей; но ComputedStyle не был неизменным; в некоторых случаях он будет изменен на более поздних этапах конвейера.

Стиль создаст дерево LayoutObject , а затем макет будет аннотировать эти объекты информацией о размере и расположении. В некоторых случаях макет может даже изменить древовидную структуру. Не было четкого разделения между входами и выходами макета .

Стиль будет генерировать дополнительные структуры данных, определяющие ход композиции , и эти структуры данных модифицируются на каждом этапе после создания стиля .

На более низком уровне типы данных рендеринга в основном состоят из специализированных деревьев (например, дерево DOM, дерево стилей, дерево макета, дерево свойств рисования); и этапы рендеринга реализованы как рекурсивные обходы дерева. В идеале обход дерева должен быть ограничен : при обработке данного узла дерева мы не должны получать доступ к какой-либо информации за пределами поддерева, корнем которого является этот узел. До RenderingNG этого никогда не было; Обход дерева часто обеспечивает доступ к информации от предков обрабатываемого узла. Это сделало систему очень хрупкой и подверженной ошибкам. Также было невозможно начать прогулку по дереву откуда-либо, кроме корня дерева.

Наконец, в коде было множество входов в конвейер рендеринга: принудительные макеты, запускаемые JavaScript, частичные обновления, запускаемые во время загрузки документа, принудительные обновления для подготовки к таргетированию событий, запланированные обновления, запрошенные системой отображения, и открытые специализированные API. только для проверки кода, и это лишь некоторые из них. Было даже несколько рекурсивных и повторно входящих путей в конвейер рендеринга (то есть переход к началу одного этапа из середины другого). Каждый из этих входов имел свое собственное своеобразное поведение, и в некоторых случаях результат рендеринга зависел от способа запуска обновления рендеринга.

Что мы изменили

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

  • Единая точка входа : мы всегда должны входить в конвейер в самом начале.
  • Функциональные этапы : каждый этап должен иметь четко определенные входы и выходы, а его поведение должно быть функциональным , то есть детерминированным и повторяемым, а результаты должны зависеть только от определенных входов.
  • Постоянные входы : входы любой ступени должны быть фактически постоянными во время работы ступени.
  • Неизменяемые выходные данные . После завершения этапа его выходные данные должны быть неизменными до конца обновления рендеринга.
  • Согласованность контрольной точки . В конце каждого этапа полученные на данный момент данные рендеринга должны находиться в самосогласованном состоянии.
  • Дедупликация работы : каждую задачу вычисляйте только один раз.

Полный список подпроектов BlinkNG может показаться утомительным, но ниже приведены некоторые из них, имеющие особое значение.

Жизненный цикл документа

Класс DocumentLifecycle отслеживает наш прогресс в конвейере рендеринга. Это позволяет нам выполнять базовые проверки, обеспечивающие соблюдение перечисленных ранее инвариантов, таких как:

  • Если мы изменяем свойство ComputedStyle, то жизненный цикл документа должен быть kInStyleRecalc .
  • Если состояние DocumentLifecycle — kStyleClean или более позднее, NeedsStyleRecalc() должен возвращать false для любого присоединенного узла.
  • При входе в фазу жизненного цикла рисования состояние жизненного цикла должно быть kPrePaintClean .

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

Если вы когда-нибудь заглядывали в кроличью нору, изучая код низкоуровневого рендеринга, вы вполне можете спросить себя: «Как я сюда попал?» Как упоминалось ранее, существует множество точек входа в конвейер рендеринга. Раньше сюда входили рекурсивные и реентерабельные пути вызовов, а также места, где мы входили в конвейер на промежуточном этапе, а не начиная с самого начала. В ходе BlinkNG мы проанализировали эти пути вызовов и определили, что все они сводятся к двум основным сценариям:

  • Все данные рендеринга необходимо обновить, например, при создании новых пикселей для отображения или при проверке попадания для таргетинга на события.
  • Нам нужно актуальное значение для конкретного запроса, на который можно ответить без обновления всех данных рендеринга. Сюда входит большинство запросов JavaScript, например node.offsetTop .

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

Стиль конвейерной обработки, макет и предварительная покраска

В совокупности этапы рендеринга перед покраской отвечают за следующее:

  • Запуск алгоритма каскада стилей для расчета окончательных свойств стиля для узлов DOM.
  • Создание дерева макета, представляющего иерархию блоков документа.
  • Определение размера и информации о положении для всех ящиков.
  • Округление или привязка субпиксельной геометрии к границам целых пикселей для рисования.
  • Определение свойств составных слоев (аффинное преобразование, фильтры, непрозрачность или что-то еще, что можно ускорить с помощью графического процессора).
  • Определение того, какой контент изменился со времени предыдущего этапа рисования и нуждается в дорисовке или перерисовке (аннулирование отрисовки).

Этот список не изменился, но до появления BlinkNG большая часть этой работы выполнялась разрозненно, распределяясь по нескольким этапам рендеринга, с большим количеством дублируемых функций и встроенных недостатков. Например, фаза стиля всегда в первую очередь отвечала за вычисление окончательных свойств стиля для узлов, но было несколько особых случаев, когда мы не определяли окончательные значения свойств стиля до завершения фазы стиля . В процессе рендеринга не было формального или обязательного момента, где мы могли бы с уверенностью сказать, что информация о стиле является полной и неизменной.

Еще одним хорошим примером проблем, возникших до BlinkNG, является недействительность отрисовки. Раньше аннулирование отрисовки было разбросано по всем этапам рендеринга, предшествующим рисованию. При изменении кода стиля или макета было трудно понять, какие изменения необходимы для логики недействительности отрисовки, и было легко допустить ошибку, приводящую к ошибкам недостаточной или чрезмерной недействительности. Подробнее о тонкостях старой системы инвалидации отрисовки вы можете прочитать в статье из этой серии, посвященной LayoutNG .

Привязка субпиксельной геометрии макета к границам целых пикселей для рисования — это пример того, как у нас было несколько реализаций одной и той же функциональности и мы проделали много избыточной работы. Существовал один путь кода привязки пикселей, используемый системой рисования, и совершенно отдельный путь кода, который использовался всякий раз, когда нам требовалось одноразовое вычисление координат привязки пикселей вне кода рисования. Стоит ли говорить, что в каждой реализации были свои ошибки, и их результаты не всегда совпадали. Поскольку эта информация не кэшировалась, система иногда неоднократно выполняла одно и то же вычисление — еще одна нагрузка на производительность.

Вот несколько значительных проектов, которые устранили архитектурные недостатки на этапах рендеринга перед покраской.

Команда проекта: конвейерная обработка фазы стиля

В этом проекте были устранены два основных недостатка на этапе разработки стиля, которые мешали его четкой конвейеризации:

Есть два основных результата фазы стиля: ComputedStyle , содержащий результат выполнения каскадного алгоритма CSS над деревом DOM; и дерево LayoutObjects , которое устанавливает порядок операций на этапе макета. Концептуально запуск каскадного алгоритма должен происходить строго до создания дерева макета; но раньше эти две операции чередовались. Project Squad удалось разделить эти два этапа на отдельные последовательные этапы.

Раньше ComputedStyle не всегда получал окончательное значение во время пересчета стиля; было несколько ситуаций, когда ComputedStyle обновлялся на более позднем этапе конвейера. Project Squad успешно провел рефакторинг этих путей кода, так что ComputedStyle никогда не изменяется после этапа стиля.

LayoutNG: конвейерная обработка фазы макета

Этот монументальный проект — один из краеугольных камней RenderingNG — представлял собой полную переработку этапа рендеринга макета. Мы не будем здесь отдавать должное всему проекту, но есть несколько примечательных аспектов всего проекта BlinkNG:

  • Ранее этап макета получал дерево LayoutObject , созданное на этапе стиля, и аннотировал дерево информацией о размере и положении. Таким образом, не было четкого разделения входов и выходов. LayoutNG представил дерево фрагментов , которое является основным выводом макета, доступным только для чтения, и служит основным входом для последующих этапов рендеринга.
  • LayoutNG привнес в макет свойство сдерживания : при вычислении размера и положения данного LayoutObject мы больше не смотрим за пределы поддерева, корнем которого является этот объект. Вся информация, необходимая для обновления макета данного объекта, рассчитывается заранее и предоставляется алгоритму в качестве входных данных только для чтения.
  • Раньше были крайние случаи, когда алгоритм макета не был строго функциональным: результат алгоритма зависел от самого последнего предыдущего обновления макета. LayoutNG устранил эти случаи.

Предпокрасочный этап

Раньше не было формального этапа рендеринга перед покраской, а был просто набор операций после макетирования. Фаза подготовки к рисованию возникла из осознания того, что существует несколько связанных функций, которые лучше всего реализовать путем систематического обхода дерева макета после завершения макета; самое главное:

  • Выдача недействительности отрисовки : очень сложно правильно выполнить аннулирование отрисовки в процессе макетирования, когда у нас есть неполная информация. Гораздо проще сделать все правильно, и это может быть очень эффективно, если разделить его на два отдельных процесса: во время стиля и макета контент может быть помечен простым логическим флагом как «возможно, требуется аннулирование отрисовки». Во время обхода дерева перед отрисовкой мы проверяем эти флаги и при необходимости выдаем аннулирование.
  • Создание деревьев свойств краски : процесс, описанный более подробно далее.
  • Вычисление и запись мест рисования с привязкой к пикселям : записанные результаты могут использоваться на этапе рисования, а также любым последующим кодом, который в них нуждается, без каких-либо избыточных вычислений.

Деревья свойств: последовательная геометрия

Деревья свойств были введены в начале RenderingNG, чтобы справиться со сложностью прокрутки, которая в Интернете имеет структуру, отличную от всех других видов визуальных эффектов. До появления деревьев свойств наборщик Chromium использовал единую иерархию «слоев» для представления геометрических отношений составного контента, но она быстро развалилась, когда стала очевидной вся сложность таких функций, как позиция: фиксированная. В иерархии слоев появились дополнительные нелокальные указатели, указывающие «родительский элемент прокрутки» или «родительский элемент клипа» слоя, и вскоре стало очень сложно понимать код.

Деревья свойств исправили это, представив аспекты прокрутки и обрезки содержимого отдельно от всех других визуальных эффектов. Это позволило правильно смоделировать реальную визуальную структуру и структуру прокрутки веб-сайтов. Далее «все», что нам нужно было сделать, — это реализовать алгоритмы поверх деревьев свойств, такие как преобразование экранного пространства составных слоев или определение того, какие слои прокручиваются, а какие нет.

Фактически, вскоре мы заметили, что в коде было много других мест, где поднимались аналогичные геометрические вопросы. ( В посте о ключевых структурах данных есть более полный список.) Некоторые из них имели дублирующиеся реализации того же, что делал код компоновщика; у всех были разные группы ошибок; и ни один из них не смоделировал должным образом истинную структуру веб-сайта. Затем решение стало очевидным: централизовать все алгоритмы геометрии в одном месте и провести рефакторинг всего кода для их использования.

Все эти алгоритмы, в свою очередь, зависят от деревьев свойств, поэтому деревья свойств являются ключевой структурой данных, то есть той, которая используется на протяжении всего конвейера RenderingNG. Таким образом, чтобы достичь этой цели централизованного геометрического кода, нам нужно было представить концепцию деревьев свойств гораздо раньше в конвейере — в предварительной отрисовке — и изменить все API, которые теперь от них зависели, чтобы они требовали запуска предварительной отрисовки, прежде чем они смогут выполниться. .

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

Композит после покраски: покраска трубопроводов и композитинг

Слои — это процесс определения того, какой контент DOM попадает в отдельный составной слой (который, в свою очередь, представляет собой текстуру графического процессора). До RenderingNG наложение слоев выполнялось до отрисовки, а не после (текущий конвейер см. здесь — обратите внимание на изменение порядка). Сначала мы решали, какие части DOM попадут в какой составной слой, и только затем рисовали списки отображения для этих текстур. Естественно, решения зависели от таких факторов, как какие элементы DOM анимировались, прокручивались или имели 3D-преобразования, а также какие элементы рисовались поверх каких.

Это вызывало серьезные проблемы, поскольку в той или иной степени требовалось наличие циклических зависимостей в коде, что является большой проблемой для конвейера рендеринга. Давайте посмотрим, почему на примере. Предположим, нам нужно сделать отрисовку недействительной (это означает, что нам нужно перерисовать список отображения, а затем снова растрировать его). Необходимость сделать недействительной может возникнуть из-за изменения в DOM или из-за изменения стиля или макета. Но, конечно, мы хотели бы признать недействительными только те части, которые действительно изменились. Это означало выяснить, какие составные слои были затронуты, а затем аннулировать часть или все списки отображения для этих слоев.

Это означает, что аннулирование зависело от DOM, стиля, макета и прошлых решений по слоям (прошлое: значение для предыдущего визуализированного кадра). Но нынешняя иерархия также зависит от всех этих вещей. А поскольку у нас не было двух копий всех данных по слоям, было трудно отличить прошлые и будущие решения по слоям. В итоге у нас получилось много кода с циклическими рассуждениями. Иногда это приводило к нелогичному или неправильному коду или даже к сбоям или проблемам с безопасностью, если мы не были очень осторожны.

Чтобы справиться с этой ситуацией, вначале мы представили концепцию объекта DisableCompositingQueryAsserts . В большинстве случаев, если код пытался запросить прошлые решения по иерархии, это приводило к сбою утверждения и сбою браузера, если он находился в режиме отладки. Это помогло нам избежать появления новых ошибок. И в каждом случае, когда код законно требовался для запроса прошлых решений по слоям, мы вставляем код, позволяющий это сделать, выделяя объект DisableCompositingQueryAsserts .

Наш план состоял в том, чтобы со временем избавиться от всех объектов DisableCompositingQueryAssert сайтов вызовов, а затем объявить код безопасным и правильным. Но мы обнаружили, что некоторые вызовы было практически невозможно удалить, поскольку наложение слоев происходило до отрисовки. (Наконец-то мы смогли удалить его только совсем недавно !) Это была первая причина, обнаруженная в проекте Composite After Paint. Мы узнали, что даже если у вас есть четко определенная фаза конвейера для операции, если она находится в неправильном месте конвейера, вы в конечном итоге застрянете.

Второй причиной проекта Composite After Paint была ошибка фундаментального композитинга. Один из способов указать на эту ошибку заключается в том, что элементы DOM не являются хорошим представлением 1:1 эффективной или полной схемы иерархии содержимого веб-страницы. А поскольку композитинг появился еще до рисования, он более или менее по своей сути зависел от элементов DOM, а не от списков отображения или деревьев свойств. Это очень похоже на причину, по которой мы ввели деревья свойств, и, как и в случае с деревьями свойств, решение выпадает сразу, если вы определите правильную фазу конвейера, запустите ее в нужное время и предоставите ей правильные ключевые структуры данных. И, как и в случае с деревьями свойств, это была хорошая возможность гарантировать, что после завершения этапа рисования его выходные данные будут неизменяемыми для всех последующих этапов конвейера.

Преимущества

Как вы видели, четко определенный конвейер рендеринга дает огромные долгосрочные преимущества. Их даже больше, чем вы думаете:

  • Значительно повышенная надежность . Это довольно просто. Чистый код с четко определенными и понятными интерфейсами легче понимать, писать и тестировать. Это делает его более надежным. Это также делает код более безопасным и стабильным, с меньшим количеством сбоев и ошибок, связанных с использованием после освобождения.
  • Расширенный тестовый охват : в ходе BlinkNG мы добавили в наш пакет очень много новых тестов. Сюда входят модульные тесты, обеспечивающие целенаправленную проверку внутренних компонентов; регрессионные тесты, которые не позволяют нам повторно вводить старые ошибки, которые мы исправили (так много!); и множество дополнений к общедоступному, коллективно поддерживаемому набору тестов веб-платформы , который все браузеры используют для измерения соответствия веб-стандартам.
  • Легче расширять : если система разбита на четкие компоненты, нет необходимости понимать другие компоненты на каком-либо уровне детализации, чтобы добиться прогресса в текущем. Это позволяет каждому повысить ценность кода рендеринга, не будучи глубоким экспертом, а также облегчает анализ поведения всей системы.
  • Производительность : Оптимизация алгоритмов, написанных на спагетти-коде, достаточно сложна, но без такого конвейера почти невозможно достичь еще больших целей, таких как универсальная потоковая прокрутка и анимация или процессы и потоки для изоляции сайта . Параллелизм может помочь нам значительно повысить производительность, но он также чрезвычайно сложен.
  • Уступка и сдерживание : благодаря BlinkNG стало возможным несколько новых функций, которые реализуют конвейер новыми и новыми способами. Например, что, если мы хотим запускать конвейер рендеринга только до истечения бюджета? Или пропустить рендеринг поддеревьев, которые, как известно, сейчас не актуальны для пользователя? Это то, что позволяет CSS-свойство content-visibility . А как насчет того, чтобы стиль компонента зависел от его макета? Это контейнерные запросы .

Практический пример: запросы к контейнеру

Контейнерные запросы — это долгожданная новая функция веб-платформы (на протяжении многих лет она была самой востребованной функцией среди разработчиков CSS). Если это так здорово, почему его до сих пор не существует? Причина в том, что реализация контейнерных запросов требует очень тщательного понимания и контроля взаимосвязи между стилем и кодом макета. Давайте посмотрим поближе.

Запрос контейнера позволяет стилям, применяемым к элементу, зависеть от размера предка. Поскольку размер макета вычисляется во время макета, это означает, что нам нужно запустить пересчет стиля после макета; но пересчет стиля выполняется перед макетом ! Этот парадокс курицы и яйца — единственная причина, по которой мы не могли реализовать контейнерные запросы до BlinkNG.

Как мы можем решить эту проблему? Разве это не обратная конвейерная зависимость, то есть та же самая проблема, которую решили такие проекты, как Composite After Paint? Хуже того, что, если новые стили изменят размер предка? Не приведет ли это иногда к бесконечному циклу?

В принципе, циклическую зависимость можно решить с помощью свойства CSS contains, которое позволяет рендерингу вне элемента не зависеть от рендеринга внутри поддерева этого элемента . Это означает, что новые стили, применяемые контейнером, не могут повлиять на размер контейнера, поскольку запросы контейнера требуют сдерживания .

Но на самом деле этого было недостаточно, и необходимо было ввести более слабый тип сдерживания, чем просто сдерживание по размерам. Это связано с тем, что обычно требуется, чтобы контейнер запросов контейнера мог изменять размер только в одном направлении (обычно блочном) на основе его встроенных размеров. Таким образом, была добавлена ​​концепция сдерживания встроенного размера . Но, как вы можете видеть из очень длинной заметки в этом разделе, долгое время было совершенно неясно, возможно ли ограничение встроенных размеров.

Одно дело описать сдерживание на языке абстрактных спецификаций, и совсем другое — правильно его реализовать. Напомним, что одной из целей BlinkNG было привнести принцип сдерживания в обходы дерева, составляющие основную логику рендеринга: при обходе поддерева не должна требоваться никакая информация извне поддерева. Как оказалось (ну, это не совсем случайность), реализовать сдерживание CSS гораздо чище и проще, если код рендеринга подчиняется принципу сдерживания.

Будущее: композитинг вне основного потока… и не только!

Показанный здесь конвейер рендеринга на самом деле немного опережает текущую реализацию RenderingNG. Он показывает, что многоуровневая структура находится вне основного потока, тогда как в настоящее время она все еще находится в основном потоке. Однако это лишь вопрос времени, поскольку теперь, когда Composite After Paint выпущен, а наложение слоев происходит после покраски.

Чтобы понять, почему это важно и к чему еще это может привести, нам нужно рассмотреть архитектуру движка рендеринга с несколько более высокой точки зрения. Одним из самых серьезных препятствий на пути повышения производительности Chromium является тот простой факт, что основной поток средства рендеринга обрабатывает как основную логику приложения (то есть запуск сценария), так и основную часть рендеринга. В результате основной поток часто перегружен работой, а перегрузка основного потока часто становится узким местом во всем браузере.

Хорошая новость в том, что так быть не должно! Этот аспект архитектуры Chromium восходит к временам KHTML , когда однопоточное выполнение было доминирующей моделью программирования. К тому времени, когда многоядерные процессоры стали обычным явлением в устройствах потребительского уровня, однопоточное предположение было полностью встроено в Blink (ранее WebKit). Мы давно хотели добавить больше потоков в движок рендеринга, но в старой системе это было просто невозможно. Одной из основных задач Rendering NG было выкопать себя из этой ямы и сделать возможным перенос работы по рендерингу, частично или полностью, в другой поток (или потоки).

Сейчас, когда BlinkNG близится к завершению, мы уже начинаем исследовать эту область; Non-Blocking Commit — это первая попытка изменить модель потоков рендеринга. Коммит композитора (или просто коммит ) — это шаг синхронизации между основным потоком и потоком композитора. Во время фиксации мы делаем копии данных рендеринга, которые создаются в основном потоке, для использования последующим кодом компоновки, выполняющимся в потоке компоновщика. Пока происходит эта синхронизация, выполнение основного потока останавливается, пока код копирования выполняется в потоке компоновщика. Это делается для того, чтобы основной поток не изменял свои данные рендеринга, пока поток наборщика копирует их.

Неблокирующая фиксация избавит основной поток от необходимости останавливаться и ждать завершения этапа фиксации — основной поток продолжит выполнять работу, в то время как фиксация выполняется одновременно в потоке компоновщика. Конечным эффектом неблокирующей фиксации будет сокращение времени, затрачиваемого на рендеринг работы в основном потоке, что уменьшит перегрузку основного потока и повысит производительность. На момент написания этой статьи (март 2022 г.) у нас есть рабочий прототип неблокирующей фиксации, и мы готовимся провести подробный анализ его влияния на производительность.

Своего часа ждет композитинг вне основного потока , цель которого заставить движок рендеринга соответствовать иллюстрации путем перемещения слоев из основного потока в рабочий поток. Как и неблокирующая фиксация, это уменьшит перегрузку основного потока за счет уменьшения рабочей нагрузки на его рендеринг. Подобный проект никогда не был бы возможен без архитектурных усовершенствований Composite After Paint.

И в разработке есть еще несколько проектов (каламбур)! Наконец-то у нас есть основа, позволяющая экспериментировать с перераспределением работы по рендерингу, и мы очень рады видеть, что это возможно!