<?xml version="1.0" encoding="utf-8" ?><rss version="2.0" xmlns:tt="http://teletype.in/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Frontend Альманах</title><generator>teletype.in</generator><description><![CDATA[Авторский блог Романа Максимова. Веб разработчик с 2006. Frontend-эксперт, исследователь и ментор.]]></description><image><url>https://img2.teletype.in/files/df/d7/dfd73f9e-c6a5-434d-bf76-e18d6dc3ec90.png</url><title>Frontend Альманах</title><link>https://blog.frontend-almanac.ru/</link></image><link>https://blog.frontend-almanac.ru/?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=frontend_almanac_ru</link><atom:link rel="self" type="application/rss+xml" href="https://teletype.in/rss/frontend_almanac_ru?offset=0"></atom:link><atom:link rel="next" type="application/rss+xml" href="https://teletype.in/rss/frontend_almanac_ru?offset=10"></atom:link><atom:link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></atom:link><pubDate>Wed, 08 Apr 2026 00:11:58 GMT</pubDate><lastBuildDate>Wed, 08 Apr 2026 00:11:58 GMT</lastBuildDate><item><guid isPermaLink="true">https://blog.frontend-almanac.ru/react-lanes</guid><link>https://blog.frontend-almanac.ru/react-lanes?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=frontend_almanac_ru</link><comments>https://blog.frontend-almanac.ru/react-lanes?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=frontend_almanac_ru#comments</comments><dc:creator>frontend_almanac_ru</dc:creator><title>React Lanes</title><pubDate>Mon, 24 Mar 2025 10:12:52 GMT</pubDate><media:content medium="image" url="https://img2.teletype.in/files/55/15/551560bd-5fd8-4030-a635-85bf39d38665.png"></media:content><category>React ⚛️</category><description><![CDATA[<img src="https://img2.teletype.in/files/99/38/9938514f-984e-48f6-b0e5-80bda3f1c4eb.png"></img>React — одна из самых популярных библиотек для создания пользовательских интерфейсов. Однако при работе с большими объемами данных или сложными вычислениями разработчики часто сталкиваются с проблемой производительности. В этой статье мы рассмотрим концепцию React Lanes — механизм, который позволяет приоритизировать задачи рендеринга и сделать интерфейс более отзывчивым даже при выполнении тяжелых операций.]]></description><content:encoded><![CDATA[
  <figure id="YAKs" class="m_column">
    <img src="https://img2.teletype.in/files/99/38/9938514f-984e-48f6-b0e5-80bda3f1c4eb.png" width="1216" />
  </figure>
  <p id="MxYl">React — одна из самых популярных библиотек для создания пользовательских интерфейсов. Однако при работе с большими объемами данных или сложными вычислениями разработчики часто сталкиваются с проблемой производительности. В этой статье мы рассмотрим концепцию React Lanes — механизм, который позволяет приоритизировать задачи рендеринга и сделать интерфейс более отзывчивым даже при выполнении тяжелых операций.</p>
  <h2 id="8LSH">Что такое React Lanes?</h2>
  <p id="6jgW">Давайте рассмотрим типовую задачу для React. Пусть это будет тривиальный поиск по текстовым элементам. Для этого возьмем обычное поле ввода и список элементов, который фильтруется в зависимости от введенного значения в это поле.</p>
  <pre id="PfLL" data-lang="jsx">const SearchList = ({ items, filter }) =&gt; {
  // Heavy filtering operation (simulation)
  const filteredItems = items.filter((item) =&gt;
    item.toLowerCase().includes(filter.toLowerCase()),
  );

  return (
    &lt;ul&gt;
      {filteredItems.map((item, i) =&gt; (
        &lt;li key={i}&gt;{item}&lt;/li&gt;
      ))}
    &lt;/ul&gt;
  );
};

const App = () =&gt; {
  const [inputValue, setInputValue] = useState(&quot;&quot;);

  // Generate large list for demonstration
  const bigList = Array(10_000)
    .fill(null)
    .map((_, i) =&gt; &#x60;Item ${i + 1}&#x60;);

  const handleChange = (e) =&gt; {
    setInputValue(e.target.value);
  };

  return (
    &lt;div&gt;
      &lt;input
        type=&quot;text&quot;
        value={inputValue}
        onChange={handleChange}
        placeholder=&quot;Search...&quot;
      /&gt;

      &lt;SearchList items={bigList} filter={inputValue} /&gt;
    &lt;/div&gt;
  );
}</pre>
  <p id="mPMp">Для наглядности, пусть список будет состоять из большого числа элементов. Что, очевидно, будет провоцировать тяжелую операцию фильтрации массива. Получившееся приложение можно потрогать руками ниже.</p>
  <figure id="8AMe" class="m_column">
    <iframe src="https://codepen.io/rmaksimov/embed/dPybOBV?default-tab=js,result"></iframe>
  </figure>
  <p id="QXkK">С количеством элементов можно поэкспериментировать в зависимости от мощности устройства и среды, где исполняется приложение. Но так или иначе, если быстро вводить значение в строку поиска, можно заметить некоторый лаг, когда в выборке оказывается много элементов.</p>
  <p id="6i8e">Происходит это из-за того, что каждое изменение значения приводит к ререндеру компонента <code>SearchList</code>, который в свою очередь должен отрисовать необходимое количество дочерних элементов.</p>
  <h2 id="zC3W">Синхронные обновления</h2>
  <p id="ywQd">Пример выше демонстрирует типичную синхронную работу по обновлению дерева компонентов. Подробно о процессе обновления я писал в статье <a href="https://blog.frontend-almanac.ru/z1S4MzuquZd" target="_blank">Детальный React. Реконсиляция, рендеры, Fiber, виртуальное дерево</a>. Движок <strong>Fiber</strong> пытается выполнить все необходимые изменения за один раз. Другими словами, фаза <code>render</code> не завершится, пока реконсилер не переберет всё запланированные синхронные изменения.</p>
  <p id="7Q9z">Конкретно в нашем примере, каждое изменение текстового поля вызывает реконсиляцию <code>SearchList</code>, а он в свою очередь ставит рендеры дочерних компонентов в той же фазе. И пока все дочерние ноды не будут обработаны, <strong>основной поток будет заблокирован</strong> работой движка. Если выводимых компонентов слишком много, время блокировки становится существенным и проявляется в виде задержки отклика на пользовательский ввод. </p>
  <figure id="cG6H" class="m_column">
    <img src="https://img2.teletype.in/files/d1/35/d135e2a3-13c9-44b8-93e0-059cc35d3cb9.png" width="2176" />
  </figure>
  <p id="AR1r">На скриншоте выше в текстовое поле был введен символ <code>0</code>, что дает 2620 дочерних элементов. Реконсиляция такого количества элементов заняла <code>253ms</code>, учитывая, что задержки более 100ms считаются заметными человеческому глазу, а расчетное время браузерного фрейма 16.6ms (при частоте обновления 60 fps). </p>
  <figure id="8Wvn" class="m_column">
    <img src="https://img1.teletype.in/files/c0/31/c03136ad-7ab1-42f9-ae84-aa37a16b2ab6.png" width="2178" />
  </figure>
  <p id="g7p5">При добавлении еще одного &quot;0&quot; выводится 181 элемент. Реконсиляция заняла уже 146ms. Однако, это всё равно много. </p>
  <figure id="mZHO" class="m_column">
    <img src="https://img2.teletype.in/files/9c/70/9c703da6-73d6-4259-96ed-0e3fc321a9e2.png" width="2174" />
  </figure>
  <p id="rEOL">Продолжим вводить нули. Следующий &quot;0&quot; даст только 10 элементов и тут мы видим уже приемлемую продолжительность в <code>10.6ms</code> </p>
  <figure id="OWzf" class="m_column">
    <img src="https://img4.teletype.in/files/bd/d5/bdd5d1dd-65df-4087-85ef-5d55d3def487.png" width="2174" />
  </figure>
  <p id="kysy">Четвертый ноль оставит только один единственный элемент и продолжительность задачи составила всего <code>2ms</code>.</p>
  <figure id="v3K7" class="m_column">
    <img src="https://img1.teletype.in/files/c6/33/c633a128-d1a0-4d99-993d-3ad431b9d477.png" width="2174" />
  </figure>
  <p id="8QSs">Что не на много больше, чем при значения &quot;00000&quot;, при котором нет ни одного выводимого элемента.</p>
  <p id="9zi2">Теперь проделаем обратную процедуру и будем удалять по одному символу из текстового поля.</p>
  <figure id="ayMg" class="m_column">
    <img src="https://img1.teletype.in/files/c7/89/c78934eb-dd54-41fa-a06e-715f385d50fe.png" width="2176" />
  </figure>
  <p id="PBsN">Значение &quot;0000&quot;. Продолжительность <code>0.9ms</code>.</p>
  <figure id="OtXC" class="m_column">
    <img src="https://img4.teletype.in/files/3b/dd/3bdde961-dabf-4a18-bad6-b1cc768d3a1a.png" width="2176" />
  </figure>
  <p id="9oPF">Значение &quot;000&quot;. Продолжительность <code>1.2ms</code>.</p>
  <figure id="cg3D" class="m_column">
    <img src="https://img3.teletype.in/files/2a/77/2a770fd5-c31e-4396-887f-aa21d4697530.png" width="2178" />
  </figure>
  <p id="HCL2">Значение &quot;00&quot;. Продолжительность <code>12.8ms</code>.</p>
  <figure id="5H0M" class="m_column">
    <img src="https://img4.teletype.in/files/73/e6/73e65c4b-db71-4a06-945f-78bb4f3a34d5.png" width="2176" />
  </figure>
  <p id="4B6l">Значение &quot;0&quot;. Продолжительность <code>178ms</code>.</p>
  <figure id="6Aoi" class="m_column">
    <img src="https://img3.teletype.in/files/a5/ae/a5ae2266-bbb7-44c2-bc22-5079951af9bb.png" width="2178" />
  </figure>
  <p id="wZe9">И наконец, пустая строка. Продолжительность <code>699ms</code>.</p>
  <h2 id="isYJ">Приоритеты задач</h2>
  <p id="oO9f">Проблема, как говорится, налицо. Блокирующие операции довольно долго оставались проблемой, с которой надо было что-то делать. В качестве решения можно было бы предложить разделить тяжелые операции на потоки и выполнить их параллельно. Но JavaScript-задача не может выполняться в нескольких потоках.</p>
  <h3 id="j8e3">ReactPriorityLevel</h3>
  <p id="dUwg">Первые попытки решить проблему были предприняты в 2016-м. В версии 15.2.1 было впервые введено понятие <code>ReactPriorityLevel</code>. Раз нельзя разделить задачи на параллельные потоки, можно хотя бы расставить их в порядке важности. Суть <code>ReactPriorityLevel</code> в том, чтобы проставить флаг приоритета каждому обновлению дерева.</p>
  <p id="s061">Изначально таких приоритетов было 5, от наиболее важного к менее важному:</p>
  <ul id="p8lc">
    <li id="x60p"><strong>SynchronousPriority</strong> - для контролируемого обновления инпутов и синхронных операций</li>
    <li id="leTk"><strong>AnimationPriority</strong> - анимации должны завершать расчет очередного шага в текущем фрейме</li>
    <li id="hHOf"><strong>HighPriority</strong> - обновления, которые должны быть выполнены как можно быстрее для сохранения оптимального отклика</li>
    <li id="nSQ3"><strong>LowPriority</strong> - для запросов и получения ответа из хранилищ</li>
    <li id="2MM4"><strong>OffscreenPriority</strong> - для элементов, которые были скрыты на странице, но снова становятся видимыми</li>
  </ul>
  <h3 id="gumY">Expiration Times</h3>
  <p id="Z8Fu">В целом, концепция приоритетов дала определенный результат. Но не все проблемы были решены. <code>ReactPriorityLevel</code> разделял задачи по группам, но внутри группы задачи по-прежнему имеют одинаковый приоритет. Например, на странице вполне может оказаться несколько анимируемых элементов. В конце 2017-го была представлена версия React 16.1, в которой вместо флагов приоритетов предлагалось выставлять время, до которого задача должна быть обработана. Чем важнее задача, тем меньший <code>expirationTime</code> она имела и, соответственно, бралась в работу раньше остальных.</p>
  <h3 id="BtKX">Lanes</h3>
  <p id="xEej">Концепция Expiration Times оказалась вполне рабочей. React 16 жил с ней следующие 3 года. Суть модели заключалась в том, что имея приоритеты A  &gt; B &gt; C, нельзя взять в работу B, не выполнив при этом A. Аналогично, нельзя работать над C, не взяв в работу A и B.</p>
  <p id="tnIs">Такой подход хорошо себя показывал вплоть до того, пока не появился <code>Suspense</code>. Приоритизация работает покуда все задачи выполняются линейно. Когда в процесс вмешивается отложенная задача (такая, как <code>Suspense</code>), появляется ситуация, когда отложенная задача с высоким приоритетом блокирует менее приоритетные основные.</p>
  <p id="J2jG">В плане выражения группы сразу из нескольких приоритетов, модель Expiration Times довольно ограничена. Чтобы понять, включать ли задачу в объем работ на текущей итерации, достаточно сравнить относительные приоритеты:</p>
  <pre id="cxWR" data-lang="javascript">const isTaskIncludedInBatch = priorityOfTask &gt;= priorityOfBatch;</pre>
  <p id="d1jd">Чтобы решить проблему Suspense, можно было бы использовать <code>Set</code> с приоритетами. Но это было бы очень накладно с точки зрения производительности и свело бы на нет все преимущества приоритетизации.</p>
  <p id="FLUj">В качестве компромисса можно было бы ввести <strong>диапазоны приоритетов</strong> примерно следующим образом:</p>
  <pre id="QiyK" data-lang="javascript">const isTaskIncludedInBatch = taskPriority &lt;= highestPriorityInRange &amp;&amp; taskPriority &gt;= lowestPriorityInRange;</pre>
  <p id="Nie7">Но даже если закрыть глаза на то, что в таком случае потребуется держать два поля, этот подход всё равно не решает всех проблем. К примеру, как удалить задачу в середине диапазона? Какое-то решение конечно можно придумать. Но как бы то ни было, любые манипуляции с диапазонами будут неизбежно влиять на другие группы и их поддержка и стабильность превротяться в настоящий кошмар.</p>
  <p id="2cug">Чтобы избежать всех этих проблем, разработчики React решили разделить две концепции приоретизации и группировки. И предложили выражать группы задач относительными числами, представляющими собой битовую маску.</p>
  <pre id="YEZa" data-lang="javascript">const isTaskIncludedInBatch = (task &amp; batchOfTasks) !== 0;</pre>
  <p id="YUHZ">Тип битовой маски представляющий задачу, называется <strong>Lane</strong>. А битовая маска представляющая группу задач - <strong>Lanes</strong>.</p>
  <h2 id="iYNQ">Как работают Lanes</h2>
  <p id="FH9w">В официальном релизе <code>Lanes</code> впервые появились в октябре 2020 с версии React 17.0.0. Это был довольно большой рефакторинг, который существенно повлиял на работу <code>Fiber</code>.</p>
  <p id="TR8q">На данный момент последняя стабильная версия React v19.0.0. Большая часть работы Lanes инкапсулирована в модуле <code>ReactFiberLane</code> реконсилятора.</p>
  <p id="nNLW">Фактически, <code>Lane</code> является 32-битным числом, где каждый бит указывает на принадлежность задачи к какой-либо из полос.</p>
  <p id="Pz4P"><a href="https://github.com/facebook/react/blob/v19.0.0/packages/react-reconciler/src/ReactFiberLane.js#L39" target="_blank">/packages/react-reconciler/src/ReactFiberLane.js#L39</a></p>
  <pre id="ONTs" data-lang="flow">export const TotalLanes = 31;

export const NoLanes: Lanes = /*                        */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /*                          */ 0b0000000000000000000000000000000;

export const SyncHydrationLane: Lane = /*               */ 0b0000000000000000000000000000001;
export const SyncLane: Lane = /*                        */ 0b0000000000000000000000000000010;
export const SyncLaneIndex: number = 1;

export const InputContinuousHydrationLane: Lane = /*    */ 0b0000000000000000000000000000100;
export const InputContinuousLane: Lane = /*             */ 0b0000000000000000000000000001000;

export const DefaultHydrationLane: Lane = /*            */ 0b0000000000000000000000000010000;
export const DefaultLane: Lane = /*                     */ 0b0000000000000000000000000100000;

export const SyncUpdateLanes: Lane =
  SyncLane | InputContinuousLane | DefaultLane;
  
const TransitionHydrationLane: Lane = /*                */ 0b0000000000000000000000001000000;
const TransitionLanes: Lanes = /*                       */ 0b0000000001111111111111110000000;
const TransitionLane1: Lane = /*                        */ 0b0000000000000000000000010000000;
const TransitionLane2: Lane = /*                        */ 0b0000000000000000000000100000000;
const TransitionLane3: Lane = /*                        */ 0b0000000000000000000001000000000;
const TransitionLane4: Lane = /*                        */ 0b0000000000000000000010000000000;
const TransitionLane5: Lane = /*                        */ 0b0000000000000000000100000000000;
const TransitionLane6: Lane = /*                        */ 0b0000000000000000001000000000000;
const TransitionLane7: Lane = /*                        */ 0b0000000000000000010000000000000;
const TransitionLane8: Lane = /*                        */ 0b0000000000000000100000000000000;
const TransitionLane9: Lane = /*                        */ 0b0000000000000001000000000000000;
const TransitionLane10: Lane = /*                       */ 0b0000000000000010000000000000000;
const TransitionLane11: Lane = /*                       */ 0b0000000000000100000000000000000;
const TransitionLane12: Lane = /*                       */ 0b0000000000001000000000000000000;
const TransitionLane13: Lane = /*                       */ 0b0000000000010000000000000000000;
const TransitionLane14: Lane = /*                       */ 0b0000000000100000000000000000000;
const TransitionLane15: Lane = /*                       */ 0b0000000001000000000000000000000;

const RetryLanes: Lanes = /*                            */ 0b0000011110000000000000000000000;
const RetryLane1: Lane = /*                             */ 0b0000000010000000000000000000000;
const RetryLane2: Lane = /*                             */ 0b0000000100000000000000000000000;
const RetryLane3: Lane = /*                             */ 0b0000001000000000000000000000000;
const RetryLane4: Lane = /*                             */ 0b0000010000000000000000000000000;

export const SomeRetryLane: Lane = RetryLane1;

export const SelectiveHydrationLane: Lane = /*          */ 0b0000100000000000000000000000000;

const NonIdleLanes: Lanes = /*                          */ 0b0000111111111111111111111111111;

export const IdleHydrationLane: Lane = /*               */ 0b0001000000000000000000000000000;
export const IdleLane: Lane = /*                        */ 0b0010000000000000000000000000000;

export const OffscreenLane: Lane = /*                   */ 0b0100000000000000000000000000000;
export const DeferredLane: Lane = /*                    */ 0b1000000000000000000000000000000;</pre>
  <p id="X3Dx">Условно, все полосы можно разделить на <strong>синхронные</strong> и <strong>отложенные</strong>. </p>
  <h2 id="o76f">Синхронные полосы</h2>
  <p id="oMcy">К синхронным полосам относятся <code>SyncLane</code>, <code>InputContinuousLane</code> и <code>DefaultLane</code>. Задачи, поставленные в эти полосы имеют наивысший приоритет и выполняются синхронно в текущей фазе реконсилятора.</p>
  <p id="9hnn">Исходя из названий можно предположить, что разные типы задач могут иметь разный приоритет даже в синхронной фазе. Cамыми приоритетными считаются:</p>
  <ul id="A1qc">
    <li id="HxRW"><strong>Размонтирование рута</strong>, что приводит к немедленной очистке стейта React и инициирует асинхронное удаление элементов дерева</li>
    <li id="FQ2o"><strong>Hot Reload</strong>, т.к. JS модули должны быть немедленно заменены на новые</li>
    <li id="tvwI">Любые <strong>оверрайды в Dev Tools</strong> всегда должны происходить в первую очередь</li>
    <li id="AU3o">Хук <a href="https://react.dev/reference/react/useSyncExternalStore" target="_blank">useSyncExternalStore</a> синхронно читает данные из внешнего хранилища, это должно происходить в самом начале реконсиляции файбера</li>
    <li id="ygcD">Хук <a href="https://react.dev/reference/react/useOptimistic" target="_blank">useOptimistic</a> гарантирует синхронное обновление оптимистичного стейта</li>
    <li id="p50n">В текущей версии React официально пока еще нет компонента <code>&lt;Activity /&gt;</code> (ранее носил название <code>&lt;Offscreen /&gt;</code>), но в движке он присутствует и когда-нибудь в будущем станет доступным. Этот компонент имеет три режима: &quot;hidden&quot;, &quot;visible&quot; и &quot;manual&quot;. В отличие от первых двух, режим manual предполагает, что разработчик сам отвечает за скрытие/показ компонента и вручную вызывает <code>Activity.attach()</code> и <code>Activity.detach()</code>. Эти два метода так же выполняются синхронно, чтобы React мог немедленно поставить в очередь работу по монтированию/размонтированию компонента.</li>
  </ul>
  <p id="Tpr8">Все выше перечисленные процессы происходят в полосе <code>SyncLane</code>.</p>
  <h3 id="g47l">Приоритеты событий</h3>
  <p id="4Ek8">Неотъемлемым диспатчером реакций в React являются события. Точнее, синтетические события. И среди всего множества потенциальных событий есть более приоритетные и менее приоритетные. Поэтому React разделяет их все на три условных группы:</p>
  <ul id="qkBO">
    <li id="il9e"><strong>Дискретные</strong> - события, вызванные напрямую пользователем (например, <code>MouseEvent</code> или <code>KeyboardEvent</code>) и при этом все события в последовательности - намеренные, например &quot;click&quot;. Таким событиям присваивается приоритет <code>DiscreteEventPriority</code> и они выполнятся в <strong>SyncLane</strong>. Эти события могут прерывать фоновые задачи, но не могут быть сгруппированы на временной дистанции (каждое событие происходит здесь и сейчас). </li>
  </ul>
  <pre id="6FRh" data-lang="jsx">import { useState } from &quot;react&quot;;
import { createRoot } from &quot;react-dom/client&quot;;

function App() {
  const [count, setCount] = useState(0);
  const handleClick = () =&gt; {
    setCount(count + 1);
  };
  return &lt;button onClick={handleClick}&gt;Update state in the SyncLane&lt;/button&gt;;
}

const root = createRoot(document.getElementById(&quot;root&quot;));
root.render(&lt;App /&gt;);</pre>
  <p id="3x9j">На данный момент список дискретных событий выглядит так:</p>
  <pre id="sdz9" data-lang="javascript">beforetoggle
cancel
click
close
contextmenu
copy
cut
auxclick
dblclick
dragend
dragstart
drop
focusin
focusout
input
invalid
keydown
keypress
keyup
mousedown
mouseup
paste
pause
play
pointercancel
pointerdown
pointerup
ratechange
reset
resize
seeked
submit
toggle
touchcancel
touchend
touchstart
volumechange
change
selectionchange
textInput
compositionstart
compositionend
compositionupdate
beforeblur
afterblur
beforeinput
blur
fullscreenchange
focus
hashchange
popstate
select
selectstart</pre>
  <ul id="02zv">
    <li id="DMCQ"><strong>Продолжительные</strong> - события, вызванные напрямую пользователем, но пользователь не может различить отдельные события в последовательности (например, <code>mouseover</code>). Таким событиям присваивается приоритет <code>ContinuousEventPriority</code> и они выполняются в <strong>InputContinuousLane</strong>. Эти события могут прерывать фоновые задачи и могут быть сгруппированы на протяжении времени.</li>
  </ul>
  <pre id="nwKW" data-lang="jsx">import { useState } from &quot;react&quot;;
import { createRoot } from &quot;react-dom/client&quot;;

function App() {
  const [count, setCount] = useState(0);
  const handleMouseOver = () =&gt; setCount(count + 1);

  return (
    &lt;div onMouseOver={handleMouseOver}&gt;
      Update state in the InputContinuousLane
    &lt;/div&gt;
  );
}

const root = createRoot(document.getElementById(&quot;root&quot;));
root.render(&lt;App /&gt;);</pre>
  <p id="yWfY">Список продолжительных событий:</p>
  <pre id="QYfI">drag
dragenter
dragexit
dragleave
dragover
mousemove
mouseout
mouseover
pointermove
pointerout
pointerover
scroll
touchmove
wheel
mouseenter
mouseleave
pointerenter
pointerleave</pre>
  <ul id="dGiP">
    <li id="FYh6"><strong>Остальные </strong>- события, которые не попадают в первые две группы (кроме <code>message</code>, у них приоритет определяется динамически в зависимости от текущего приоритета планировщика). Таким событиям присваивается приоритет <code>DefaultEventPriority</code> и они выполняются в <strong>DefaultLane</strong> соответственно. Отсутствие события так же считается <code>DefaultEventPriority</code>.</li>
  </ul>
  <pre id="CRxH" data-lang="jsx">import { createRoot } from &quot;react-dom/client&quot;;

const root = createRoot(document.getElementById(&quot;root&quot;));

root.render(
  &lt;div&gt;
    No events emitted. Common rendering uses the DefaultLane
  &lt;/div&gt;
);</pre>
  <h2 id="nzO6">Отложенные полосы</h2>
  <p id="grQT">Отложенные полосы, как не трудно догадаться из названия, могут выполняться асинхронно с задержкой. Они имеют <strong>более низкий приоритет</strong> по сравнению с синхронными полосами и выполняются в фоне после того, как все синхронные полосы были отработаны. Задачи, выполняемые в отложенных полосах считаются <strong>неблокирующими</strong>.</p>
  <h3 id="QbcM">TransitionLane</h3>
  <p id="vdIU">Пожалуй самый очевидный способ поставить отложенную задачу - через <code>startTransition</code>.</p>
  <pre id="Lw6I" data-lang="jsx">import { startTransition, useEffect, useState } from &quot;react&quot;;
import { createRoot } from &quot;react-dom/client&quot;;

function App() {
  const [, setHigh] = useState(0);
  const [, setLow] = useState(0);
  
  useEffect(() =&gt; {
    startTransition(() =&gt; {
      // 2. TransitionLane will be proceeded in a background after a sync lane
      setLow((prevLow) =&gt; prevLow + 1);
    });
    
    // 1. DefaultLane will be done first
    setHigh((prevHigh) =&gt; prevHigh + 1);
  }, []);
  
  return null;
}

const root = createRoot(document.getElementById(&quot;root&quot;));
root.render(&lt;App /&gt;);</pre>
  <figure id="qg9t" class="m_column">
    <img src="https://img3.teletype.in/files/ab/b8/abb8d987-10c2-4de7-bfc0-5eca2daa0316.png" width="2014" />
  </figure>
  <p id="zfBI">Задачи, которые ставятся в <code>TransitionLane</code>, могут <strong>параллелиться</strong>. Для этого предусмотрено целых 15 полос <code>TransitionLane1..15</code>. В отличие от синхронных полос, где параллелизм большого смысла не имеет, их эффективнее объединять в пакеты, чем параллелить.</p>
  <pre id="gmpu" data-lang="jsx">import { startTransition, useEffect, useState } from &quot;react&quot;;
import { createRoot } from &quot;react-dom/client&quot;;

function App() {
  const [, setHigh] = useState(0);
  const [, setLow] = useState(0);
  
  useEffect(() =&gt; {
    // TransitionLane1
    startTransition(() =&gt; {
      setLow((prevLow) =&gt; prevLow + 1);
    });

    // TransitionLane2
    setTimeout(() =&gt; {
      startTransition(() =&gt; {
        setLow((prevLow) =&gt; prevLow + 1);
      });
    }, 0);

    // TransitionLane3
    setTimeout(() =&gt; {
      startTransition(() =&gt; {
        setLow((prevLow) =&gt; prevLow + 1);
      });
    }, 0);
    
    // DefaultLane
    setHigh((prevHigh) =&gt; prevHigh + 1);
  }, []);
  
  return null;
}

const root = createRoot(document.getElementById(&quot;root&quot;));
root.render(&lt;App /&gt;);</pre>
  <figure id="SdPX" class="m_column">
    <img src="https://img4.teletype.in/files/32/9e/329e7afd-49b8-43bd-8503-f4c3bc8df137.png" width="2016" />
  </figure>
  <h3 id="X7kb">RetryLane</h3>
  <p id="Njxz">Ещё один способ выполнить отложенную задачу - <code>Suspense</code>. </p>
  <pre id="tl3b" data-lang="jsx">import { Suspense, use } from &quot;react&quot;;
import { createRoot } from &quot;react-dom/client&quot;;

const cache = new Map();

function fetchData() {
  if (!cache.has(&quot;data&quot;)) {
    cache.set(&quot;data&quot;, getData());
  }
  return cache.get(&quot;data&quot;);
}

async function getData() {
  // Add a fake delay to make waiting noticeable.
  await new Promise((resolve) =&gt; {
    setTimeout(resolve, 1_000);
  });
  
  return [&quot;item1&quot;, &quot;item2&quot;, &quot;item3&quot;];
}

function AsyncComponent() {
  const items = use(fetchData());
  
  return (
    &lt;ul&gt;
      {items.map((item) =&gt; (
        &lt;li key={item}&gt;{item}&lt;/li&gt;
      ))}
    &lt;/ul&gt;
  );
}

function App() {
  return (
    &lt;&gt;
      &lt;h1&gt;Retry lanes&lt;/h1&gt;
      &lt;Suspense fallback=&quot;Loading...&quot;&gt;
        &lt;AsyncComponent /&gt;
      &lt;/Suspense&gt;
    &lt;/&gt;
  );
}

const root = createRoot(document.getElementById(&quot;root&quot;));
root.render(&lt;App /&gt;);</pre>
  <figure id="hFFa" class="m_column">
    <img src="https://img4.teletype.in/files/3e/16/3e16474b-93c8-49eb-8b29-a0864f9a3442.png" width="2192" />
  </figure>
  <p id="WhZ1">Говоря простым языком, <code>Suspense</code> рендерит сначала fallback компонент. Далее, когда один или несколько промисов внутри Suspended компонента зарезолвятся, Fiber попытается повторить рендеринг, поставив задачу в <code>RetryLane</code>. Как и <code>TransitionLane</code>, <code>RetryLane</code> может параллелиться и имеет четыре полосы <code>RetryLane1..4</code>.</p>
  <pre id="dm5I" data-lang="jsx">function AsyncComponent() {
  const items = use(fetchData());
  const meta = use(fetchMeta());
  
  return (
    &lt;ul&gt;
      {items.map((item) =&gt; (
        &lt;li key={items}&gt;{item}&lt;/li&gt;
      ))}
    &lt;/ul&gt;
  );
}

function App() {
  return (
    &lt;&gt;
      &lt;h1&gt;Retry lanes&lt;/h1&gt;
      &lt;Suspense fallback=&quot;Loading...&quot;&gt;
        &lt;AsyncComponent /&gt;
      &lt;/Suspense&gt;
    &lt;/&gt;
  );
}

const root = createRoot(document.getElementById(&quot;root&quot;));
root.render(&lt;App /&gt;);</pre>
  <figure id="gqsf" class="m_column">
    <img src="https://img3.teletype.in/files/a6/69/a66984da-47e0-493d-bcb5-f99682fa7390.png" width="2194" />
  </figure>
  <p id="5wMY">Аналогичная работа происходит и с компонентами <code>SuspenseList</code> и <code>Activity</code>. Но они пока не включены в последний, на сегодняшний день релиз React. Поэтому рассматривать их подробно сейчас не имеет смысла.</p>
  <h3 id="y5JK">IdleLane</h3>
  <p id="uHVX">Следующая по приоритету полоса - <code>IdleLane</code>. Идея <code>IdleLane</code> заключается в том, чтобы некоторые задачи выполнять только тогда, когда движок простаивает и не занят другими, более важными задачами. Это можно сравнить с Idle-периодом в браузерном планировщике (я писал про это подробнее в статье <a href="https://blog.frontend-almanac.ru/chromium-rendering" target="_blank">Chromium. Отрисовка страницы с помощью Blink, CC и планировщика</a>), только на уровне движка Fiber и React-планировщика.</p>
  <p id="qRae">На данный момент нет ни React DOM API, которые могли бы вызвать обновление с idle-приоритетом, ни нативных DOM событий с idle-приоритетом. Поэтому пока этой полосой можно воспользоваться только через внутренние методы движка, не доступные в prod-сборке React.</p>
  <h3 id="9OR7">OffscreenLane</h3>
  <p id="zmUu">Чуть выше, когда я писал про синхронные полосы, я уже упоминал так называемый компонент <code>Offscreen</code>, который теперь носит название <code>Activity</code>. Его идея заключается в том, чтобы определять, находится ли компонент в данный момент в видимой части страницы, или за её пределами. Если компонент сейчас не виден на экране, то и задачи его имеют низкий приоритет. Собственно, для таких задач и была выделена полоса <code>OffscreenLane</code>.</p>
  <p id="dl7r">К сожалению, это еще один компонент, который пока не вошел в текущий релиз React. Но у него есть все шансы попасть в один из следующих. Будем надеяться, что в обозримом будущем мы его таки увидим.</p>
  <h3 id="DBRv">DeferredLane</h3>
  <p id="PvLl">Не трудно догадаться, что полоса <code>DeferredLane</code> предназначена для хука <a href="https://react.dev/reference/react/useDeferredValue" target="_blank">useDeferredValue</a>. Однако, с ней не всё так просто, как может показаться.</p>
  <pre id="dqpO" data-lang="jsx">import { useDeferredValue } from &quot;react&quot;;
import { createRoot } from &quot;react-dom/client&quot;;

function App() {
  const text = useDeferredValue(&quot;Final&quot;);
  
  return &lt;div&gt;{text}&lt;/div&gt;;
}

const root = createRoot(document.getElementById(&quot;root&quot;));
root.render(&lt;App /&gt;);</pre>
  <figure id="dJZW" class="m_column">
    <img src="https://img3.teletype.in/files/69/d5/69d5d3ba-99e9-4b59-912b-f6217fcf894c.png" width="2192" />
  </figure>
  <p id="Ja7e">Если у хука не указан второй аргумент - <code>initialValue</code>, ему не с чем будет сравнивать новое значение при монтировании и он выполнится синхронно в <code>DefaultLane</code>. Однако, если у хука имеется предыдущее значение, с которым он может сравнить текущее, например когда хук смонтирован и обновляется повторно или если указать <code>initialValue</code>, мы увидим следующую картину.</p>
  <pre id="uUWQ" data-lang="jsx">import { useDeferredValue } from &quot;react&quot;;
import { createRoot } from &quot;react-dom/client&quot;;

function App() {
  const text = useDeferredValue(&quot;Final&quot;, &quot;Initial&quot;);
  
  return &lt;div&gt;{text}&lt;/div&gt;;
}

const root = createRoot(document.getElementById(&quot;root&quot;));
root.render(&lt;App /&gt;);</pre>
  <figure id="IC9x" class="m_column">
    <img src="https://img4.teletype.in/files/78/b5/78b50eff-b2ea-4246-b361-ab8264fe9d41.png" width="2194" />
  </figure>
  <p id="5b7n">Работа по факту была выполнена в <code>TransitionLane</code>. Если бы мы попробовали выполнить хук в <code>Offscreen</code> компоненте, то увидели бы, что он выполнен в <code>OffscreenLane</code>. Дело в том, что <code>DeferredLane</code> является промежуточной технической полосой. Она служит только для логического отделения отложенных задач от остальных. Собственного приоритета выполнения эта задача не имеет и всегда подмешивается к другим полосам исходя из ситуации.</p>
  <h2 id="xvs0">Неблокирующий рендеринг</h2>
  <p id="I68M">Давайте теперь вернемся к нашему исходному примеру и попробуем избавиться от блокировок интерфейса тяжелыми операциями.</p>
  <pre id="9MCt" data-lang="jsx">const SearchList = ({ items, filter }) =&gt; {
  // Heavy filtering operation (simulation)
  const filteredItems = items.filter((item) =&gt;
    item.toLowerCase().includes(filter.toLowerCase()),
  );

  return (
    &lt;ul&gt;
      {filteredItems.map((item, i) =&gt; (
        &lt;li key={i}&gt;{item}&lt;/li&gt;
      ))}
    &lt;/ul&gt;
  );
};

const App = () =&gt; {
  const [inputValue, setInputValue] = useState(&quot;&quot;);
  const deferredValue = useDeferredValue(inputValue);
  
  // Generate large list for demonstration
  const bigList = Array(10_000)
    .fill(null)
    .map((_, i) =&gt; &#x60;Item ${i + 1}&#x60;);

  const handleChange = (e) =&gt; {
    setInputValue(e.target.value);
  };

  return (
    &lt;div&gt;
      &lt;input
        type=&quot;text&quot;
        value={inputValue}
        onChange={handleChange}
        placeholder=&quot;Search...&quot;
      /&gt;

      &lt;SearchList items={bigList} filter={deferredValue} /&gt;
    &lt;/div&gt;
  );
}</pre>
  <figure id="wD1b" class="m_column">
    <iframe src="https://codepen.io/rmaksimov/embed/zxYOoyr?default-tab=js,result"></iframe>
  </figure>
  <figure id="OtDJ" class="m_column">
    <img src="https://img1.teletype.in/files/cb/ce/cbce822d-4eeb-4ef4-97f3-2729b2a43f4a.png" width="2194" />
  </figure>
  <figure id="beay" class="m_column">
    <img src="https://img2.teletype.in/files/d4/50/d450f271-1a21-4773-b940-00bf3c7c75ff.png" width="2196" />
  </figure>
  <p id="HPVJ">Всё, что мы сделали, в <code>SearchList</code> стали передавать <code>deferredValue</code> вместо <code>inputValue</code>. Это позволило нам поставить задачу по расчету массива элементов в отложенных <code>TransitionLane</code> и разблокировать синхронные обновления элемента input.</p>
  <p id="w8z5">Конечно, чудес от React ждать не стоит. <strong>Тяжелая операция по-прежнему остается тяжелой.</strong> А React, будучи просто JavaScript-библиотекой, по-прежнему выполняется в основном потоке браузера. Система Lanes по своей сути является лишь способом приоритизации задач. Более важные задачи выполняются в первую очередь, менее важные - откладываются на потом. Однако, если тяжелая отложенная задача взята в работу, она всё равно будет блокировать основной поток.</p>
  <h2 id="bOux">Практические рекомендации по использованию Lanes</h2>
  <p id="fFhJ">Понимание системы Lanes в React может существенно помочь в оптимизации производительности приложений. Вот несколько практических рекомендаций:</p>
  <ol id="zMO6">
    <li id="MXhb"><strong>Используйте useDeferredValue для тяжелых операций рендеринга</strong>. Как мы видели в примере с поиском, отложенное значение позволяет сохранить отзывчивость интерфейса даже при работе с большими объемами данных.</li>
    <li id="Mcfd"><strong>Применяйте startTransition для неприоритетных обновлений UI</strong>. Когда вам нужно выполнить обновление, которое не требует мгновенной реакции (например, переключение вкладок или загрузка дополнительного контента), оборачивайте его в <code>startTransition</code>.</li>
    <li id="vY26"><strong>Разделяйте синхронные и асинхронные части вашего приложения</strong>. Элементы, требующие немедленной реакции (поля ввода, кнопки), должны обновляться синхронно, а тяжелые вычисления и отображение больших списков лучше отложить.</li>
    <li id="q70M"><strong>Используйте Suspense для асинхронной загрузки данных</strong>. Это позволит React автоматически управлять приоритетами задач при загрузке данных с сервера.</li>
    <li id="s0J4"><strong>Помните о вложенности компонентов</strong>. Чем глубже вложен компонент, тем больше времени может занять его обновление. Старайтесь выносить тяжелые компоненты ближе к корню дерева.</li>
  </ol>
  <h2 id="TXvC">Будущее React Lanes</h2>
  <p id="iq8r">Система Lanes продолжает развиваться. В будущих версиях React мы можем ожидать:</p>
  <ul id="tji0">
    <li id="yQB5">Появление официального API для компонента <code>Activity</code>, который позволит более гибко управлять приоритетами рендеринга в зависимости от видимости компонентов.</li>
    <li id="Td1p">Возможное внедрение поддержки Web Workers для выполнения тяжелых вычислений в отдельных потоках.</li>
    <li id="FLYs">Дальнейшее развитие API для более тонкой настройки приоритетов задач.</li>
    <li id="YzzU">Улучшение интеграции с инструментами профилирования для более наглядного отображения работы системы приоритетов.</li>
  </ul>
  <h2 id="bHzC">Заключение</h2>
  <p id="JMaM">Система Lanes в React представляет собой мощный инструмент для оптимизации пользовательского опыта. Она не делает ваше приложение быстрее в абсолютном смысле, но позволяет более разумно распределять вычислительные ресурсы, отдавая приоритет тому, что важно для пользователя прямо сейчас.</p>
  <p id="iXfc">Правильное использование <code>useDeferredValue</code>, <code>startTransition</code> и <code>Suspense</code> может значительно улучшить воспринимаемую производительность вашего приложения, делая его более отзывчивым даже при выполнении сложных операций. Это особенно важно для современных веб-приложений, которые часто работают с большими объемами данных и сложными интерфейсами.</p>
  <p id="mMXz">В конечном счете, понимание внутреннего устройства React и принципов работы системы приоритетов позволяет разработчикам создавать более эффективные и дружелюбные к пользователю приложения, что является ключевой целью любой фронтенд-разработки.</p>
  <p id="yc2h"></p>
  <hr />
  <p id="bdw6"></p>
  <p id="7pWR"><strong>Мои телеграмм-каналы:</strong></p>
  <section style="background-color:hsl(hsl(55,  86%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="YnAq">EN - <a href="https://t.me/frontend_almanac" target="_blank">https://t.me/frontend_almanac</a><br />RU - <a href="https://t.me/frontend_almanac_ru" target="_blank">https://t.me/frontend_almanac_ru</a></p>
  </section>
  <p id="Ri5D"><em>English version: <a href="https://blog.frontend-almanac.com/react-lanes" target="_blank">https://blog.frontend-almanac.com/react-lanes</a></em></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.frontend-almanac.ru/style-setproperty-vs-setattribute</guid><link>https://blog.frontend-almanac.ru/style-setproperty-vs-setattribute?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=frontend_almanac_ru</link><comments>https://blog.frontend-almanac.ru/style-setproperty-vs-setattribute?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=frontend_almanac_ru#comments</comments><dc:creator>frontend_almanac_ru</dc:creator><title>style.setProperty vs setAttribute</title><pubDate>Mon, 18 Nov 2024 07:58:04 GMT</pubDate><description><![CDATA[<img src="https://img3.teletype.in/files/e5/22/e522a213-9a7d-45e7-83a3-384906f209a9.png"></img>На днях столкнулся с интересным вопросом. Что быстрее element.style.setProperty(свойство, значение) или element.setAttribute('style', 'свойство: значение')? На первый взгляд ответ кажется очевидным. Логика говорит нам, что setProperty должен устанавливать значение сразу в CSSOM, тогда как setAttribute выставляет сначала атрибут style и уже потом значение атрибута будет разобрано в CSSOM. Таким образом, setProperty должен быть быстрее. Но действительно ли всё так однозначно? Давайте разбираться.]]></description><content:encoded><![CDATA[
  <p id="yUJC">На днях столкнулся с интересным вопросом. Что быстрее <code>element.style.setProperty(свойство, значение)</code> или <code>element.setAttribute(&#x27;style&#x27;, &#x27;свойство: значение&#x27;)</code>? На первый взгляд ответ кажется очевидным. Логика говорит нам, что <code>setProperty</code> должен устанавливать значение сразу в CSSOM, тогда как <code>setAttribute</code> выставляет сначала атрибут <code>style</code> и уже потом значение атрибута будет разобрано в CSSOM. Таким образом, <code>setProperty</code> должен быть быстрее. Но действительно ли всё так однозначно? Давайте разбираться.</p>
  <p id="nG4P">Начнем с того, что немного освежим мат. часть. Мы знаем, что стили описываются с помощью языка CSS. Получив строковое описание стилей на языке CSS, браузер разбирает его и составляет объект CSSOM. Интерфейс этого объекта представлен спецификацией <a href="https://www.w3.org/TR/cssom-1" target="_blank">https://www.w3.org/TR/cssom-1</a>. Он следует принципам каскадности и наследования, изложенным в <a href="https://www.w3.org/TR/css-cascade-4" target="_blank">https://www.w3.org/TR/css-cascade-4</a>.</p>
  <p id="3w1z">Из выше указанных спецификаций мы знаем, что основной единицей CSS является &quot;свойство&quot;. Свойству присваивается значение, характерное конкретно этому свойству. Если значение не задано явным образом, оно наследуется от выше стоящего стиля или, если нет вышестоящего, будет установлено <a href="https://drafts.csswg.org/css-cascade-4/#initial-value" target="_blank">initial value</a>.</p>
  <p id="oZMK">Набор свойств для элемента собирается в правила <a href="https://www.w3.org/TR/cssom-1/#css-rules" target="_blank">CSSRule</a>. Правила бывают разных типов. Наиболее популярный тип - <code>CSSStyleRule</code>, определяющий свойства элемента. Такое правило начинается с указания одного из валидных селекторов и последующих фигурных скобок с набором свойств и значений <code>&lt;selector&gt;: { ... }</code> Имеются и другие типы правил, например CSSFontFaceRule, описывающий параметры подключаемого шрифта <code>@font-face { ... }</code>, CSSMediaRule - <code>@media { ... }</code> и др. Полный список в спецификации <a href="https://www.w3.org/TR/cssom-1/#css-rules" target="_blank">https://www.w3.org/TR/cssom-1/#css-rules</a>.</p>
  <p id="Rd4L">Правила собираются в так называемый <a href="https://www.w3.org/TR/cssom-1/#css-style-sheets" target="_blank">CSSStyleSheet</a>, который фактически является абстрактным представлением тэга <code>&lt;style&gt;</code>. В свою очередь, style sheets собираются в коллекцию и привязываются к документу.</p>
  <p id="zlAi">Все эти уровни абстракции и являются моделью <strong>CSS Object Model (CSSOM)</strong>. И хотя спецификация описывает в основном только синтаксис модели, на практике в неё же входит и непосредственная реализация CSS API браузера. Такие функции, как computed style, методы кодировки цвета, математические операции и многое другое - тоже часть CSSOM.</p>
  <h2 id="YGig">Установка свойства в Blink</h2>
  <p id="DMOb">С теорией разобрались, теперь давайте немного заглянем под капот браузера. А точнее в движок рендеринга Blink, используемый браузерами Chromium, Opera, WebView и др. Blink является частью репозитория Сhromium и не поставляется самостоятельно. Поэтому эксперименты будем проводить именно в Chromium (версия <a href="https://chromium.googlesource.com/chromium/src.git/+/refs/tags/132.0.6812.1/" target="_blank">132.0.6812.1</a> на момент написания статьи).</p>
  <h3 id="gv2b">style.setProperty</h3>
  <p id="QmaB">Начнем с метода <code>style.setProperty</code>.</p>
  <pre id="eY2Z" data-lang="javascript">element.style.setProperty(&quot;background-color&quot;, &quot;red&quot;);</pre>
  <p id="N8LE">Чтобы понять, что происходит под капотом Blink, стоит заглянуть внутрь самого Blink.</p>
  <p id="x6ON"><a href="https://chromium.googlesource.com/chromium/src.git/+/refs/tags/132.0.6812.1/third_party/blink/renderer/core/css/abstract_property_set_css_style_declaration.cc#130" target="_blank">/third_party/blink/renderer/core/css/abstract_property_set_css_style_declaration.cc#130</a></p>
  <pre id="RjAv" data-lang="cpp">void AbstractPropertySetCSSStyleDeclaration::setProperty(
    const ExecutionContext* execution_context,
    const String&amp; property_name,
    const String&amp; value,
    const String&amp; priority,
    ExceptionState&amp; exception_state) {
  CSSPropertyID property_id =
      UnresolvedCSSPropertyID(execution_context, property_name);
  if (!IsValidCSSPropertyID(property_id) || !IsPropertyValid(property_id)) {
    return;
  }
  
  bool important = EqualIgnoringASCIICase(priority, &quot;important&quot;);
  if (!important &amp;&amp; !priority.empty()) {
    return;
  }
  
  const SecureContextMode mode = execution_context
                                     ? execution_context-&gt;GetSecureContextMode()
                                     : SecureContextMode::kInsecureContext;
  SetPropertyInternal(property_id, property_name, value, important, mode,
                      exception_state);
}</pre>
  <p id="O8No">Первым делом проходит базовая проверка переданного свойства на валидность. Далее проверяется приоритет стиля и вызывается внутренний метод <code>SetPropertyInternal</code>.</p>
  <p id="LqzZ"><a href="https://chromium.googlesource.com/chromium/src.git/+/refs/tags/132.0.6812.1/third_party/blink/renderer/core/css/abstract_property_set_css_style_declaration.cc#236" target="_blank">/third_party/blink/renderer/core/css/abstract_property_set_css_style_declaration.cc#236</a></p>
  <pre id="F8aY" data-lang="cpp">void AbstractPropertySetCSSStyleDeclaration::SetPropertyInternal(
    CSSPropertyID unresolved_property,
    const String&amp; custom_property_name,
    StringView value,
    bool important,
    SecureContextMode secure_context_mode,
    ExceptionState&amp;) {
  StyleAttributeMutationScope mutation_scope(this);
  WillMutate();
  
  MutableCSSPropertyValueSet::SetResult result;
  if (unresolved_property == CSSPropertyID::kVariable) {
    AtomicString atomic_name(custom_property_name);
    
    bool is_animation_tainted = IsKeyframeStyle();
    result = PropertySet().ParseAndSetCustomProperty(
        atomic_name, value, important, secure_context_mode, ContextStyleSheet(),
        is_animation_tainted);
  } else {
    result = PropertySet().ParseAndSetProperty(unresolved_property, value,
                                               important, secure_context_mode,
                                               ContextStyleSheet());
  }
  
  if (result == MutableCSSPropertyValueSet::kParseError ||
      result == MutableCSSPropertyValueSet::kUnchanged) {
    DidMutate(kNoChanges);
    return;
  }
  
  CSSPropertyID property_id = ResolveCSSPropertyID(unresolved_property);
  
  if (result == MutableCSSPropertyValueSet::kModifiedExisting &amp;&amp;
      CSSProperty::Get(property_id).SupportsIncrementalStyle()) {
    DidMutate(kIndependentPropertyChanged);
  } else {
    DidMutate(kPropertyChanged);
  }
  
  mutation_scope.EnqueueMutationRecord();
}</pre>
  <p id="4Ad2">В этом внутреннем методе запускается процесс мутации, парсится строковое значение и устанавливается данному свойству. После чего изменения попадают в CSSOM. В целом, процесс не хитрый.</p>
  <h3 id="KqS6">setAttribute</h3>
  <p id="MU3S">Теперь взглянем на <code>setAttribute</code>.</p>
  <pre id="Du6V" data-lang="javascript">element.setAttribute(&quot;style&quot;, &quot;background-color: red&quot;);</pre>
  <p id="x8KH">Данная операция выходит за рамки CSSOM и затрагивает элемент и его атрибуты, что ведет к изменения в DOM. В рамках DOM существует класс <a href="https://dom.spec.whatwg.org/#interface-attr" target="_blank">Attr</a>, описывающий один атрибут элемента. Существует несколько способов установить значение атрибута.</p>
  <p id="X7C3"><a href="https://chromium.googlesource.com/chromium/src.git/+/refs/tags/132.0.6812.1/third_party/blink/renderer/core/dom/attr.cc#73" target="_blank">/third_party/blink/renderer/core/dom/attr.cc#73</a></p>
  <pre id="hSHZ" data-lang="cpp">void Attr::setValue(const AtomicString&amp; value,
                    ExceptionState&amp; exception_state) {
  // Element::setAttribute will remove the attribute if value is null.
  DCHECK(!value.IsNull());
  if (element_) {
    element_-&gt;SetAttributeWithValidation(GetQualifiedName(), value,
                                         exception_state);
  } else {
    standalone_value_or_attached_local_name_ = value;
  }
}</pre>
  <p id="aByR">Данный метод позволяет нам присваивать значение существующему атрибуту. Другими словами, мы можем сделать так.</p>
  <pre id="QqpI" data-lang="javascript">element.style = &quot;background-color: red&quot;;</pre>
  <p id="WEYp">Что приведет к вызову метода <code>SetAttributeWithValidation</code>, о котором поговорим чуть ниже.</p>
  <p id="En1C">В нашем же исходном случае мы не обращаемся к атрибуту <code>style</code> напрямую, а вызываем метод <code>setAttribute</code> из класса <a href="https://dom.spec.whatwg.org/#interface-element" target="_blank">Element</a>.</p>
  <p id="VTKa"><a href="https://chromium.googlesource.com/chromium/src.git/+/refs/tags/132.0.6812.1/third_party/blink/renderer/core/dom/element.h#282" target="_blank">/third_party/blink/renderer/core/dom/element.h#282</a></p>
  <pre id="dWsS" data-lang="cpp">void setAttribute(const QualifiedName&amp; name, const AtomicString&amp; value) {
  SetAttributeWithoutValidation(name, value);
}</pre>
  <p id="jcka">Который обращается уже к другому методу <code>SetAttributeWithoutValidation</code>.</p>
  <p id="NIQH">Итак, что же это за методы <code>SetAttributeWithoutValidation</code> и <code>SetAttributeWithValidation</code>?</p>
  <p id="q3Dh"><a href="https://chromium.googlesource.com/chromium/src.git/+/refs/tags/132.0.6812.1/third_party/blink/renderer/core/dom/element.cc#10314" target="_blank">/third_party/blink/renderer/core/dom/element.cc#10314</a></p>
  <pre id="TnWk" data-lang="cpp">void Element::SetAttributeWithoutValidation(const QualifiedName&amp; name,
                                            const AtomicString&amp; value) {
  SynchronizeAttribute(name);
  SetAttributeInternal(FindAttributeIndex(name), name, value,
                       AttributeModificationReason::kDirectly);
}

void Element::SetAttributeWithValidation(const QualifiedName&amp; name,
                                         const AtomicString&amp; value,
                                         ExceptionState&amp; exception_state) {
  SynchronizeAttribute(name);
  
  AtomicString trusted_value(TrustedTypesCheckFor(
      ExpectedTrustedTypeForAttribute(name), value, GetExecutionContext(),
      &quot;Element&quot;, &quot;setAttribute&quot;, exception_state));
  if (exception_state.HadException()) {
    return;
  }
  
  SetAttributeInternal(FindAttributeIndex(name), name, trusted_value,
                       AttributeModificationReason::kDirectly);
}</pre>
  <p id="hCue">Как мы можем видеть, оба метода в сущности делают одно и то же. Сначала синхронизируют текущее актуальное значение атрибута, на случай если где-то есть незавершенная мутация. После чего уже непосредственно переходят к процессу установки нового значения. Разница лишь в том, что  во втором случае перед установкой нового значения проводится его валидация на соответствие данному атрибуту. Другими словами, вот так не получится.</p>
  <pre id="ZfLD" data-lang="javascript">element.style = &quot;invalid-prop: red&quot;
// &lt;div style&gt;&lt;/div&gt;</pre>
  <p id="t2RW">Попытка присвоить атрибуту <code>style</code> невалидный стиль приведет к удалению значения в атрибуте.</p>
  <p id="Qmuh">Однако, нам ничто не помешает сделать вот так.</p>
  <pre id="5HiG" data-lang="javascript">element.setAttribute(&quot;style&quot;, &quot;invalid-prop: red&quot;)
// &lt;div style=&quot;invalid-prop: red&quot;&gt;&lt;/div&gt;</pre>
  <p id="AcWH">В этом случае <code>style</code> будет установлен вне зависимости от того, что мы указали в качестве значения.</p>
  <p id="BzXr">Выглядит довольно странно. Почему бы не валидировать значения атрибутов всегда? Ответ довольно простой. Метод <code>setAttribute</code>, это своего рода backdor  для атрибутов. Он призван оперировать не только встроенными атрибутами, но и произвольными, такими как например, <a href="https://developer.mozilla.org/ru/docs/Web/HTML/Global_attributes/data-*" target="_blank">data-*</a>.</p>
  <pre id="za3o" data-lang="javascript">element.setAttribute(&quot;data-ship-id&quot;, &quot;324&quot;);
element.setAttribute(&quot;data-weapons&quot;, &quot;laserI laserII&quot;);
element.setAttribute(&quot;data-shields&quot;, &quot;72%&quot;);
element.setAttribute(&quot;data-x&quot;, &quot;414354&quot;);
element.setAttribute(&quot;data-y&quot;, &quot;85160&quot;);
element.setAttribute(&quot;data-z&quot;, &quot;31940&quot;);
element.setAttribute(&quot;onclick&quot;, &quot;spaceships[this.dataset.shipId].blasted()&quot;);</pre>
  <p id="Mypj"><code>setAttribute</code> работает на уровне DOM элемента и его задача просто присвоить значение атрибуту без каких либо валидаций. Подробный алгоритм описан в <a href="https://dom.spec.whatwg.org/#dom-element-setattribute" target="_blank">DOM стандарте</a>.</p>
  <h2 id="tUEt">Что быстрее?</h2>
  <p id="CPtl">На основании вышесказанного можно сделать предположение, что <code>style.setProperty</code> должен работать быстрее, так как в отличие от <code>setAttribute</code> движку не требуется искать ссылку на объект атрибута в таблице атрибутов и он может сразу приступить к установке значения. С другой стороны, расходы на валидацию самого значения могут оказаться существенными.</p>
  <h3 id="lIP6">Испытание 1. Одно свойство</h3>
  <p id="iLN4">Чтобы определиться, что же все таки быстрее, проведем эксперимент. Для этого нам понадобится HTML-страница с тестовым элементом и парой кнопок.</p>
  <pre id="X9n5" data-lang="html">&lt;div id=&quot;test-element&quot;&gt;&lt;/div&gt;
&lt;button onclick=&quot;handleSetProperty()&quot;&gt;style.setProperty&lt;/button&gt;
&lt;button onclick=&quot;handleSetAttribute()&quot;&gt;setAttribute&lt;/button&gt;</pre>
  <p id="5Ank">И не сложный JS скрипт.</p>
  <pre id="S9cW" data-lang="javascript">const N = 100;

function sleep(ms) {
  return new Promise((resolve) =&gt; setTimeout(resolve, ms));
}

function createElement() {
  const el = document.getElementById(&quot;test-element&quot;);
  
  const newEl = document.createElement(&quot;div&quot;);
  newEl.setAttribute(&quot;id&quot;, &quot;test-element&quot;);
  newEl.setAttribute(&quot;width&quot;, 100);
  newEl.setAttribute(&quot;height&quot;, 100);
  
  el.replaceWith(newEl);
  
  return newEl;
}

async function handleSetProperty() {
  const durations = [];

  for await (const i of new Array(N).fill(true).map((_, index) =&gt; index)) {
    const el = createElement();

    await sleep(300);

    const startTime = performance.now();
    el.style.setProperty(&quot;background-color&quot;, &quot;red&quot;);
    const duration = performance.now() - startTime;

    durations.push(duration);

    await sleep(300);

    console.log(i);
  }

  console.log(durations);
}

async function handleSetAttribute() {
  const durations = [];

  for await (const i of new Array(N).fill(true).map((_, index) =&gt; index)) {
    const el = createElement();

    await sleep(300);

    const startTime = performance.now();
    el.setAttribute(&quot;style&quot;, &quot;background-color: red&quot;);
    const duration = performance.now() - startTime;

    durations.push(duration);

    await sleep(300);

    console.log(i);
  }

  console.log(durations);
}</pre>
  <p id="sAg0">Данный скрипт по нажатию на одну из кнопок будет запускать цикл из 100 итераций по установке значения style тестовому элементу. Для чистоты эксперимента на каждой итерации будем создавать новый тестовый элемент и делать паузу после создания элемента и после установки значения, дабы исключить браузерные оптимизации и гарантировать отрисовку элемента после каждой итерации. Замеры будем делать отдельно, после жесткой перезагрузки страницы.</p>
  <p id="dNtQ">Собрав замеры ста итераций, вычислим простое среднее и получим следующий результат.</p>
  <pre id="YftO">avgSetProperty     0.07400000274181366
avgSetAttribute    0.08999999761581420</pre>
  <p id="GCxL">В данном эксперименте уверенно лидирует <code>style.setProperty</code>. Однако, мы пытались устанавливать только одно единственное CSS-свойство.</p>
  <h3 id="Awqt">Испытание 2. Два свойства</h3>
  <p id="QPT2">Для объективности, повторим эксперимент сразу с двумя свойствами. Для этого немного изменим тестовые функции.</p>
  <pre id="3PVh" data-lang="javascript">async function handleSetProperty() {
  const durations = [];

  for await (const i of new Array(N).fill(true).map((_, index) =&gt; index)) {
    const el = createElement();

    await sleep(300);

    const startTime = performance.now();
    el.style.setProperty(&quot;background-color&quot;, &quot;red&quot;);
    el.style.setProperty(&quot;border&quot;, &quot;1px solid blue&quot;);
    const duration = performance.now() - startTime;

    durations.push(duration);

    await sleep(300);

    console.log(i);
  }

  console.log(durations);
}

async function handleSetAttribute() {
  const durations = [];

  for await (const i of new Array(N).fill(true).map((_, index) =&gt; index)) {
    const el = createElement();

    await sleep(300);

    const startTime = performance.now();
    el.setAttribute(&quot;style&quot;, &quot;background-color: red; border: 1px solid blue;&quot;);
    const duration = performance.now() - startTime;

    durations.push(duration);

    await sleep(300);

    console.log(i);
  }

  console.log(durations);
}</pre>
  <p id="8tkx">Запустив циклы еще раз, получаем следующий результат.</p>
  <pre id="f7uY">avgSetProperty     0.10900000214576722
avgSetAttribute    0.10399999618530273</pre>
  <p id="nanZ">Картина изменилась. Средняя скорость обоих вариантов приблизительно сравнялась. &quot;setAttribute&quot; оказался даже немного быстрее, но эту разницу можно списать на погрешность. И причина тому вовсе даже не валидация, здесь она пока еще не значительна. В данном случае в игру вступает еще один процесс - мутация объекта <a href="https://drafts.csswg.org/cssom/#the-cssstyledeclaration-interface" target="_blank">CSSStyleDeclaration</a>. Этот интерфейс описывает структуру набор стилей, такого как например, тело тэга <code>&lt;style&gt;</code>. Такой же объект хранит и стиль отдельно взятого элемента. Я уже приводил листинг метода установки значения свойства в <code>CSSStyleDeclaration</code>. Давайте взглянем на него еще раз.</p>
  <p id="kuUL"><a href="https://chromium.googlesource.com/chromium/src.git/+/refs/tags/132.0.6812.1/third_party/blink/renderer/core/css/abstract_property_set_css_style_declaration.cc#236" target="_blank">/third_party/blink/renderer/core/css/abstract_property_set_css_style_declaration.cc#236</a></p>
  <pre id="21IN" data-lang="cpp">void AbstractPropertySetCSSStyleDeclaration::SetPropertyInternal(
    CSSPropertyID unresolved_property,
    const String&amp; custom_property_name,
    StringView value,
    bool important,
    SecureContextMode secure_context_mode,
    ExceptionState&amp;) {
  StyleAttributeMutationScope mutation_scope(this);
  WillMutate();

  MutableCSSPropertyValueSet::SetResult result;
  if (unresolved_property == CSSPropertyID::kVariable) {
    AtomicString atomic_name(custom_property_name);

    bool is_animation_tainted = IsKeyframeStyle();
    result = PropertySet().ParseAndSetCustomProperty(
        atomic_name, value, important, secure_context_mode, ContextStyleSheet(),
        is_animation_tainted);
  } else {
    result = PropertySet().ParseAndSetProperty(unresolved_property, value,
                                               important, secure_context_mode,
                                               ContextStyleSheet());
  }

  if (result == MutableCSSPropertyValueSet::kParseError ||
      result == MutableCSSPropertyValueSet::kUnchanged) {
    DidMutate(kNoChanges);
    return;
  }

  CSSPropertyID property_id = ResolveCSSPropertyID(unresolved_property);

  if (result == MutableCSSPropertyValueSet::kModifiedExisting &amp;&amp;
      CSSProperty::Get(property_id).SupportsIncrementalStyle()) {
    DidMutate(kIndependentPropertyChanged);
  } else {
    DidMutate(kPropertyChanged);
  }

  mutation_scope.EnqueueMutationRecord();
}</pre>
  <p id="zC25">Первым делом объект блокируется. Далее парсится и сохраняется значение свойства. После чего блокировка снимается.</p>
  <p id="IYNL">А вот так выглядит установка стиля целиком, через текстовую строку.</p>
  <p id="V7py"><a href="https://chromium.googlesource.com/chromium/src.git/+/refs/tags/132.0.6812.1/third_party/blink/renderer/core/css/abstract_property_set_css_style_declaration.cc#54" target="_blank">/third_party/blink/renderer/core/css/abstract_property_set_css_style_declaration.cc#54</a></p>
  <pre id="2FID" data-lang="cpp">void AbstractPropertySetCSSStyleDeclaration::setCSSText(
    const ExecutionContext* execution_context,
    const String&amp; text,
    ExceptionState&amp;) {
  StyleAttributeMutationScope mutation_scope(this);
  WillMutate();

  const SecureContextMode mode = execution_context
                                     ? execution_context-&gt;GetSecureContextMode()
                                     : SecureContextMode::kInsecureContext;
  PropertySet().ParseDeclarationList(text, mode, ContextStyleSheet());

  DidMutate(kPropertyChanged);

  mutation_scope.EnqueueMutationRecord();
}</pre>
  <p id="JPNe">Тот же самый объект так же блокируется. Дальше текстовая строка парсится целиком и разбирается на свойства. Значения сохраняются и объект разблокируется.</p>
  <p id="Hppm">Таким образом, вызывая <code>style.setProperty</code> два раза, мы инициируем процесс блокировки-парсинга-разблокировки дважды. В отличие от <code>setAttribute</code>, который способен распарсить весь стиль за один раз.</p>
  <p id="bAjx">Не могу не отметить один момент, касающийся оптимизации парсинга стилей.</p>
  <p id="1xV0"><a href="https://chromium.googlesource.com/chromium/src.git/+/refs/tags/132.0.6812.1/third_party/blink/renderer/core/css/parser/css_parser_impl.cc#255" target="_blank">/third_party/blink/renderer/core/css/parser/css_parser_impl.cc#255</a></p>
  <pre id="OYuT" data-lang="cpp"> static ImmutableCSSPropertyValueSet* CreateCSSPropertyValueSet(
    HeapVector&lt;CSSPropertyValue, 64&gt;&amp; parsed_properties,
    CSSParserMode mode,
    const Document* document) {
  if (mode != kHTMLQuirksMode &amp;&amp;
      (parsed_properties.size() &lt; 2 ||
       (parsed_properties.size() == 2 &amp;&amp;
        parsed_properties[0].Id() != parsed_properties[1].Id()))) {
    // Fast path for the situations where we can trivially detect that there can
    // be no collision between properties, and don&#x27;t need to reorder, make
    // bitsets, or similar.
    ImmutableCSSPropertyValueSet* result =
        ImmutableCSSPropertyValueSet::Create(parsed_properties, mode);
    parsed_properties.clear();
    return result;
  }
  
  ...
}</pre>
  <p id="Poqt">Парсер, разобрав строку, получает некий массив свойств. Однако, этот массив может содержать дублирующиеся свойства. Для того, чтобы создать набор уникальных свойств, потребуется занова пройти по массиву и собрать сет. Однако, если в исходном массиве всего два свойства, проверить их на уникальность можно простым if-ом сравнив их по id. Мелочь, а приятно.</p>
  <h3 id="Aphg">Испытание 3. Множество свойств</h3>
  <p id="uBsu">Проведем еще одно испытание. На этот раз возьмем свойств побольше. Скажем, семь. Почему именно семь? Это число выбрано неспроста. Но об этом чуть ниже.</p>
  <pre id="BIuF" data-lang="javascript">async function handleSetProperty() {
  const durations = [];

  for await (const i of new Array(N).fill(true).map((_, index) =&gt; index)) {
    const el = createElement();

    await sleep(300);

    const startTime = performance.now();
    el.style.setProperty(&quot;background-color&quot;, &quot;red&quot;);
    el.style.setProperty(&quot;border&quot;, &quot;1px solid blue&quot;);
    el.style.setProperty(&quot;position&quot;, &quot;relative&quot;);
    el.style.setProperty(&quot;display&quot;, &quot;flex&quot;);
    el.style.setProperty(&quot;align-items&quot;, &quot;center&quot;);
    el.style.setProperty(&quot;text-align&quot;, &quot;center&quot;);
    el.style.setProperty(&quot;text-transform&quot;, &quot;uppercase&quot;);
    const duration = performance.now() - startTime;

    durations.push(duration);

    await sleep(300);

    console.log(i);
  }

  console.log(durations);
}

async function handleSetAttribute() {
  const durations = [];

  for await (const i of new Array(N).fill(true).map((_, index) =&gt; index)) {
    const el = createElement();

    await sleep(300);

    const startTime = performance.now();
    el.setAttribute(
      &quot;style&quot;,
      &quot;background-color: red; border: 1px solid blue; position: relative; display: flex; align-items: center; text-align: center; text-transform: uppercase;&quot;,
    );
    const duration = performance.now() - startTime;

    durations.push(duration);

    await sleep(300);

    console.log(i);
  }

  console.log(durations);
}</pre>
  <p id="43hi">Результаты этого испытания вполне логичны.</p>
  <pre id="HytZ">avgSetProperty     0.17899999499320984
avgSetAttribute    0.12500000596046448</pre>
  <p id="sAsu">На этот раз <code>setAttribute</code> одержал безоговорочную победу в скорости по выше упомянутым причинам.</p>
  <h3 id="Tshp">Испытание 4. Множество числовых значений</h3>
  <p id="TlgG">Вы наверняка заметили, что в предыдущем испытании все свойства имели строковые значения. Дело в том, что числовые значения, пришедшие из JavaScript не требуют их дополнительного парсинга из строки. Что позволяет сделать еще одну оптимизацию и немного сэкономить на этом.</p>
  <p id="OSJL">К сожалению, таких свойств не много. Большинство привычных нам свойств, которые могут принимать числа, умеют работать, например, с разными единицами измерения, делать разные математические операции и т.д. Что заставляет в любом случае конвертировать их числовое значение во что-то иное. Поэтому разработчики ограничили набор возможных свойств с числовыми значениями. На данный момент таких свойств всего семь: <a href="https://developer.mozilla.org/ru/docs/Web/CSS/opacity" target="_blank">opacity</a>, <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/fill-opacity" target="_blank">fill-opacity</a>, <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/flood-opacity" target="_blank">flood-opacity</a>, <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/stop-opacity" target="_blank">stop-opacity</a>, <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/stroke-opacity" target="_blank">stroke-opacity</a>, <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/shape-image-threshold" target="_blank">shape-image-threshold</a> и <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/box-flex" target="_blank">-webkit-box-flex</a>.</p>
  <p id="H3hv">Повторим наш предыдущий эксперимент, но теперь только с числовыми свойствами. Собственно, именно по этому я и взял в прошлый раз только семь штук, дабы была возможность сравнить результаты.</p>
  <pre id="zHVU" data-lang="javascript">async function handleSetProperty() {
  const durations = [];

  for await (const i of new Array(N).fill(true).map((_, index) =&gt; index)) {
    const el = createElement();

    await sleep(300);

    const startTime = performance.now();
    el.style.setProperty(&quot;opacity&quot;, 0.5);
    el.style.setProperty(&quot;fill-opacity&quot;, 0.5);
    el.style.setProperty(&quot;flood-opacity&quot;, 0.5);
    el.style.setProperty(&quot;stop-opacity&quot;, 0.5);
    el.style.setProperty(&quot;stroke-opacity&quot;, 0.5);
    el.style.setProperty(&quot;shape-image-threshold&quot;, 0.5);
    el.style.setProperty(&quot;-webkit-box-flex&quot;, 1);

    const duration = performance.now() - startTime;

    durations.push(duration);

    await sleep(300);

    console.log(i);
  }

  console.log(durations);
}

async function handleSetAttribute() {
  const durations = [];

  for await (const i of new Array(N).fill(true).map((_, index) =&gt; index)) {
    const el = createElement();

    await sleep(300);

    const startTime = performance.now();
    el.setAttribute(
      &quot;style&quot;,
      &quot;opacity: 0.5; fill-opacity: 0.5; flood-opacity: 0.5; stop-opacity: 0.5; stroke-opacity: 0.5; shape-image-threshold: 0.5; -webkit-box-flex: 1;&quot;,
    );
    const duration = performance.now() - startTime;

    durations.push(duration);

    await sleep(300);

    console.log(i);
  }

  console.log(durations);
}</pre>
  <p id="cPZO">И теперь результат.</p>
  <pre id="ek7t">avgSetProperty     0.17099999666213989
avgSetAttribute    0.09600000023841858</pre>
  <p id="0tpg"><code>setAttribute</code> все так же быстрее. Всё таки, расходы на конвертацию строки в число по производительности не идут в сравнение с процессом блокировки-разблокировки и записи значения. Но тем не менее, в обоих случаях результат оказался немного лучше, чем в испытании 3.</p>
  <h2 id="LXBc">Вывод</h2>
  <p id="feF7">Подытожим всё выше сказанное.</p>
  <ol id="JV3W">
    <li id="KiOd"><code>style.setProperty</code> показывает себя быстрее, в случае установки одного единственного свойства. Если устанавливается одновременно 2 и более свойств, быстрее будет <code>setAttribute</code>.</li>
    <li id="HHTw"><code>setAttribute</code> не валидирует значения. Это может быть как плюсом, так и минусом. Если нам важно, чтобы выставлялись только корректные значения, можно воспользоваться сеттером <code>element.style = &quot;&lt;стиль&gt;&quot;</code>. Однако, если нам требуется установить на элементе произвольный атрибут или заведомо не корректное значение, без <code>setAttribute</code> не обойтись.</li>
    <li id="Fj9S">Существует семь свойств, которым разрешено принимать числовые значения из JS-скрипта. Установка значений этих свойств будет немного быстрее за счет исключения операции конвертации строки в число.</li>
  </ol>
  <p id="6UYS"></p>
  <hr />
  <p id="MKYX"></p>
  <p id="cELV"><strong>Мои телеграмм-каналы:</strong></p>
  <section style="background-color:hsl(hsl(55,  86%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="YnAq">EN - <a href="https://t.me/frontend_almanac" target="_blank">https://t.me/frontend_almanac</a><br />RU - <a href="https://t.me/frontend_almanac_ru" target="_blank">https://t.me/frontend_almanac_ru</a></p>
  </section>
  <p id="Ri5D"><em>English version: <a href="https://blog.frontend-almanac.com/style-setproperty-vs-setattribute" target="_blank">https://blog.frontend-almanac.com/style-setproperty-vs-setattribute</a></em></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.frontend-almanac.ru/v8-strings</guid><link>https://blog.frontend-almanac.ru/v8-strings?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=frontend_almanac_ru</link><comments>https://blog.frontend-almanac.ru/v8-strings?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=frontend_almanac_ru#comments</comments><dc:creator>frontend_almanac_ru</dc:creator><title>V8. Работа со строкам. Пополняем словарный запас</title><pubDate>Tue, 20 Aug 2024 07:41:44 GMT</pubDate><media:content medium="image" url="https://img2.teletype.in/files/d3/b3/d3b3fc23-12ac-4cdf-bc22-ade0076d9c8a.png"></media:content><category>v8</category><description><![CDATA[<img src="https://img4.teletype.in/files/7e/2e/7e2e28f6-c0b8-4bcb-93a4-085b41075a9d.png"></img>Для того чтобы лучше понять, что происходит под капотом V8, для начала стоит вспомнить немного теории.]]></description><content:encoded><![CDATA[
  <h2 id="g8eO">Что такое строка</h2>
  <p id="OEes">Для того чтобы лучше понять, что происходит под капотом V8, для начала стоит вспомнить немного теории.</p>
  <p id="A5Pl">Спецификация <a href="https://tc39.es/ecma262/#sec-ecmascript-language-types-string-type" target="_blank">ECMA-262</a> гласит:</p>
  <blockquote id="pi4d">The String type is the set of all ordered sequences of zero or more 16-bit unsigned integer values (“elements”) up to a maximum length of 2**53 - 1 elements.</blockquote>
  <p id="6YCh">Тип String — это набор всех упорядоченных последовательностей из нуля или более 16-разрядных целых беззнаковых чисел (“элементов”) максимальной длиной <code>2**53 - 1</code> элементов.</p>
  <p id="xXWH">На практике в машинной памяти вместе со строками необходимо хранить дополнительную информацию, чтобы иметь возможность определить конец строки в общей куче. Для этой цели существует два подхода. Первый — массив символов: структура, представляющая собой последовательность элементов и отдельное поле — длину этой последовательности. Второй — метод завершающего байта, то есть в конце последовательности элементов должен стоять некий служебный символ, обозначающий конец строки. В качестве завершающего символа, в зависимости от системы, может выступать байт <code>0xFF</code> или, например, ASCII-код символа. Но наибольшее распространение получил байт <code>0x00</code>. А строки с таким завершающим элементом получили название <a href="https://ru.wikipedia.org/wiki/%D0%9D%D1%83%D0%BB%D1%8C-%D1%82%D0%B5%D1%80%D0%BC%D0%B8%D0%BD%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%B0%D1%8F_%D1%81%D1%82%D1%80%D0%BE%D0%BA%D0%B0" target="_blank">нуль-терминированные</a>. Оба метода имеют свои плюсы и минусы, поэтому в современном мире часто оба метода объединяют, и строки в памяти могут одновременно быть нуль-терминированными и хранить значение длины. Например, в языке C++ начиная с версии 11.</p>
  <p id="oqQa">Кроме общего определения строки, спецификация ECMA-262 имеет ряд других требований наряду с остальными типами данных языка JavaScript. Подробно о типах и их представлении внутри движка V8 я писал в статье &quot;Глубокий JS. В память о типах и данных&quot;. В этой статье мы узнали, что все типы имеют свой скрытый класс, который хранит всю необходимую атрибутику, служебную информацию, а также инкапсулирует логику работы с этим типом. Очевидно, что для работы со строками стандартной библиотеки C++ <strong>std::string</strong> — недостаточно, и они преобразуются во внутренний класс <strong>v8::String</strong>.</p>
  <h2 id="JD8T">Какие бывают строки</h2>
  <p id="fNpU">Дабы обеспечить все требования спецификации и в силу различных оптимизаций, строки внутри V8 делятся на разные типы.</p>
  <h3 id="jxeT">Однобайтные / Двубайтные</h3>
  <p id="UsyR">Несмотря на то что спецификация прямо определяет все строки как последовательность 16-разрядных элементов, с точки зрения оптимизации это довольно расточительно. Ведь далеко не всем символам требуется 2 байта для представления. Стандартная таблица ASCII содержит 128 элементов, среди которых арабские цифры, буквы латинского алфавита в верхнем и нижнем регистрах без диакритических знаков, основные знаки препинания, математические знаки и управляющие символы. Вся эта таблица предполагает кодирование всего 8 битами, при этом сам набор символов является широко распространенным и встречается на практике наиболее часто. В связи с этим разработчиками V8 было принято решение кодировать те строки, которые содержат только 8-разрядные символы, отдельно от стандартных 16-разрядных. Это позволяет существенно экономить память.</p>
  <h3 id="Kf8s">Интернализированные / Экстернализированные строки</h3>
  <p id="vNtq">Помимо разрядности, строки различаются местом хранения. После преобразования во внутреннюю структуру строка попадает в так называемую таблицу строк (<strong>StringTable</strong>), о которой мы поговорим чуть ниже. Пока обозначу, что таких таблиц три. Собственно, сама StringTable хранит строки внутри одного <strong>Isolate</strong> (грубо говоря, внутри одного таба браузера). Кроме того, движок позволяет хранить строки за пределами кучи, и даже за пределами самого движка во внешнем хранилище. Указатели на такие строки помещаются в отдельную таблицу <strong>ExternalStringTable</strong>. Дополнительно есть еще одна таблица — <strong>StringForwardingTable</strong> для нужд сборщика мусора. Да, строки, как и объекты, участвуют в процессе сборки мусора, и так как хранятся они в отдельных таблицах, им для этого нужны специфические механизмы. Об этом тоже чуть ниже.</p>
  <h3 id="pMdB">Строки по назначение</h3>
  <p id="nbXT">Язык JavaScript является довольно гибким. Во время исполнения кода строки могут многократно трансформироваться и участвовать в смежных процессах. Для большей производительности им выделено несколько функциональных типов. К ним относятся: <strong>SeqString</strong>, <strong>ConsString</strong>, <strong>ThinString</strong>, <strong>SlicedString</strong> и <strong>ExternalString</strong>. О каждом типе мы поговорим подробно далее. А пока давайте соберём всё вышесказанное вместе.</p>
  <figure id="m1Bw" class="m_column">
    <img src="https://img4.teletype.in/files/7e/2e/7e2e28f6-c0b8-4bcb-93a4-085b41075a9d.png" width="1534" />
  </figure>
  <p id="5Lh8">Так выглядит общая классификация типов строк в V8. Помимо основных, есть ещё несколько производных типов. Например, SeqString и ExternalString могут быть размещены в общей Shared-куче, тогда их типы могут быть <code>SharedSeq&lt;one|two&gt;ByteString</code> и <code>SharedExternal&lt;one|two&gt;ByteString</code> соответственно. Кроме того, ExternalString может быть некэшируемым и иметь дополнительные типы <code>UncachableExternal&lt;one|two&gt;ByteString</code> и даже <code>SharedUncachableExternal&lt;one|two&gt;ByteString</code>.</p>
  <h2 id="Y7Or">AST</h2>
  <p id="h0kb">Прежде чем строка приобретёт свой окончательный вид в недрах V8, движок должен её сначала прочитать и интерпретировать. Делается это тем же механизмом, что и интерпретация всех остальных синтаксических единиц языка. А именно, посредством составления абстрактного синтаксического дерева (AST), про которое я говорил в статье <a href="https://blog.frontend-almanac.ru/UH_MQVhvQ7t#ZPiB" target="_blank">Глубокий JS. Области тьмы или где живут переменные</a>.</p>
  <p id="zFmx">Получив на вход строковую ноду через строковый литерал или каким-то другим образом, V8 создаёт экземпляр класса <strong>AstRawString</strong>. Если строка является конкатенацией двух или более других строк, то <strong>AstConsString</strong>. Эти классы предназначены для хранения строк вне кучи V8, в так называемой <strong>AstValueFactory</strong>. Позже этим строкам будет определён тип, и они будут интернализированы, т.е. перемещены в кучу V8.</p>
  <h2 id="5HOS">Длина строки</h2>
  <p id="zeTi">Размер каждой строки в памяти зависит от её длины. Чем длиннее строка, тем больше памяти она занимает. Но существуют ли какие-то ограничения на максимальный размер строки со стороны движка? Спецификация прямо указывает на то, что строка не может быть больше <code>2**53 - 1</code> элементов. Однако на практике это число может быть другим. Ведь тогда максимальная однобайтная строка весила бы <code>8 192 ТБ (8 ПБ)</code>, а двубайтная, соответственно, <code>4 096 ТБ (4 ПБ)</code>. Очевидно, что ни один персональный компьютер не имеет такого количества оперативной памяти, поэтому JS-движки имеют свои собственные лимиты, которые значительно строже требований спецификации. Конкретно в V8 (версия V8 на момент написания статьи <a href="https://chromium.googlesource.com/v8/v8.git/+/refs/heads/12.8.325" target="_blank">12.8.325</a>) максимальная длина строки зависит от архитектуры системы. </p>
  <p id="TaZ8"><a href="https://chromium.googlesource.com/v8/v8.git/+/refs/heads/12.8.325/include/v8-primitive.h#126" target="_blank">/include/v8-primitive.h#126</a></p>
  <pre id="r3Ly" data-lang="cpp">static constexpr int kMaxLength =
    internal::kApiSystemPointerSize == 4 ? (1 &lt;&lt; 28) - 16 : (1 &lt;&lt; 29) - 24;</pre>
  <p id="x58F">Для 32-разрядных систем это число <strong>2**28 - 16</strong>, а в 64-разрядных — чуть больше: <strong>2**29 - 24</strong>. Такие ограничения не позволят в 32-разрядных системах одной однобайтной строке занять более 256 МБ, а двубайтной — более 512 МБ. В 64-разрядных системах максимальные значения составляют соответственно не более 512 МБ и не более 1024 МБ. В случае если строка превысит лимит длины, движок вернёт ошибку <code>Invalid string length</code>.</p>
  <pre id="8rmB" data-lang="javascript">const length = (1 &lt;&lt; 29) - 24;
const longString = &#x27;&quot;&#x27; + new Array(length - 2).join(&#x27;x&#x27;);

String.prototype.link(longString); // RangeError: Invalid string length</pre>
  <p id="gzkh">Объективности ради стоит отметить, что косвенную роль играет и максимальный размер самой кучи, так как он не является константой и может регулироваться системой или конфигурацией движка.</p>
  <pre id="ncZa" data-lang="javascript">// d8 --max-heap-size=128

const length = (1 &lt;&lt; 27) + 24; // 129 MB
const longString = &#x27;&quot;&#x27; + new Array(length - 2).join(&#x27;x&#x27;);

String.prototype.link(longString);

// Fatal JavaScript out of memory: Reached heap limit</pre>
  <h2 id="9oto">StringTable</h2>
  <p id="DnuZ">В ходе выполнения JS-программа может оперировать большим количеством как самих строк, так и переменных, которые на них ссылаются. Как мы увидели, каждая строка может потреблять значительный объём памяти. При этом сами строки в ходе выполнения JS-программы могут многократно клонироваться, изменяться, склеиваться и вообще трансформироваться самыми разными способами. В результате этого в памяти могут оказаться дубликаты строковых значений. Хранить множество копий одной и той же последовательности символов было бы крайне расточительно, поэтому в мире системного программирования уже давно активно применяется практика хранения строковых значений в некой отдельной структуре, которая заботится о том, чтобы в ней не было дублирования значений, а все строковые переменные ссылались на эту структуру. В Java и .NET, например, такая структура называется &quot;StringPool&quot;, а в JavaScript - <strong>StringTable</strong>.</p>
  <p id="HZnp">Суть в том, что после того, как строка попала в AST-дерево, для неё на основании типа и значения генерируется хэш-ключ. Сгенерированный ключ сохраняется в объекте строки, и далее по нему запускается проверка наличия строки в специальной таблице строк, размещённой в куче. Если такой ключ есть в таблице, соответствующая JS-переменная получит ссылку на существующий объект строки. В противном случае будет создан новый объект строки, а ключ будет помещён в таблицу. Этот процесс называется интернмазацией.</p>
  <h2 id="5cGK">InternalizedString</h2>
  <p id="Ss0h">Чуть выше я говорил, что во внутреннем представлении V8 существует множество типов строк. Интернализированные строки в своём самом базовом варианте получают тип <strong>InternalizedString</strong>, который указывает на то, что строка находится в StringTable.</p>
  <p id="SPpW">Как обычно, давайте посмотрим на структуру строки внутри движка, скомпилировав V8 в режиме debug и запустив её с флагом <code>--allow-natives-syntax</code>.</p>
  <pre id="3EBD" data-lang="cpp">d8&gt; %DebugPrint(&quot;FrontendAlmanac&quot;)

DebugPrint: 0x28db000d9b99: [String] in OldSpace: #FrontendAlmanac
0x28db000003d5: [Map] in ReadOnlySpace
 - map: 0x28db000004c5 &lt;MetaMap (0x28db0000007d &lt;null&gt;)&gt;
 - type: INTERNALIZED_ONE_BYTE_STRING_TYPE
 - instance size: variable
 - elements kind: HOLEY_ELEMENTS
 - enum length: invalid
 - stable_map
 - non-extensible
 - back pointer: 0x28db00000061 &lt;undefined&gt;
 - prototype_validity cell: 0
 - instance descriptors (own) #0: 0x28db00000701 &lt;DescriptorArray[0]&gt;
 - prototype: 0x28db0000007d &lt;null&gt;
 - constructor: 0x28db0000007d &lt;null&gt;
 - dependent code: 0x28db000006dd &lt;Other heap object (WEAK_ARRAY_LIST_TYPE)&gt;
 - construction counter: 0</pre>
  <p id="JI6W">Первое, что мы здесь видим, это тип <code>INTERNALIZED_ONE_BYTE_STRING_TYPE</code>, что означает, что строка является однобайтной и помещена в StringTable. Давайте попробуем создать ещё один строковый литерал с тем же значением.</p>
  <pre id="Nhwk" data-lang="cpp">d8&gt; const string2 = &quot;FrontendAlmanac&quot;;
d8&gt; %DebugPrint(string2);

DebugPrint: 0x28db000d9b99: [String] in OldSpace: #FrontendAlmanac
0x28db000003d5: [Map] in ReadOnlySpace
 - map: 0x28db000004c5 &lt;MetaMap (0x28db0000007d &lt;null&gt;)&gt;
 - type: INTERNALIZED_ONE_BYTE_STRING_TYPE
 - instance size: variable
 - elements kind: HOLEY_ELEMENTS
 - enum length: invalid
 - stable_map
 - non-extensible
 - back pointer: 0x28db00000061 &lt;undefined&gt;
 - prototype_validity cell: 0
 - instance descriptors (own) #0: 0x28db00000701 &lt;DescriptorArray[0]&gt;
 - prototype: 0x28db0000007d &lt;null&gt;
 - constructor: 0x28db0000007d &lt;null&gt;
 - dependent code: 0x28db000006dd &lt;Other heap object (WEAK_ARRAY_LIST_TYPE)&gt;
 - construction counter: 0</pre>
  <p id="uVxq">Обратите внимание, что константа <code>string2</code> ссылается ровно на тот же экземпляр строки по адресу <code>0x28db000d9b99</code>.</p>
  <p id="my3n">Аналогичную картину мы можем увидеть и в браузере Chrome.</p>
  <pre id="lXVD" data-lang="javascript">// Замкнем значения в функции
function V8Snapshot() {
  this.string1 = &quot;FrontendAlmanac&quot;;
  this.string2 = &quot;FrontendAlmanac&quot;;
}

const v8Snapshot = new V8Snapshot();</pre>
  <figure id="6jCO" class="m_column">
    <img src="https://img4.teletype.in/files/ba/03/ba03e791-195e-40e2-bb3a-d99f82443d85.png" width="1882" />
  </figure>
  <p id="Q9KU">Оба свойства V8Snapshot ссылаются на один и тот же адрес <code>@61559</code>. Содержимое всей StringTable можно увидеть тут же, в слепке в объекте <code>(string)</code>.</p>
  <figure id="Wd2i" class="m_column">
    <img src="https://img4.teletype.in/files/36/27/36271ec0-630a-41f1-89ad-827a065a0f1a.png" width="1884" />
  </figure>
  <p id="TuKQ">Здесь мы можем найти нашу строку и посмотреть, какие переменные на неё ссылаются.</p>
  <p id="s65e">Вы наверняка заметили, что в таблице помимо нашей строки есть ещё более 4к других значений. Дело в том, что V8 хранит здесь, в том числе, свои служебные строки, такие как ключевые слова, различные названия системных методов и функций, и даже тексты JS-скриптов.</p>
  <figure id="YuSO" class="m_column">
    <img src="https://img2.teletype.in/files/99/5c/995c6dac-af5a-4b10-836c-910940d29073.png" width="1886" />
  </figure>
  <h2 id="fF4R">SeqString</h2>
  <p id="Jit8">Понятие <strong>InternalizedString</strong> появилось сравнительно недавно, в 2018 году, в рамках оптимизирующего компилятора TurboFan. До этого простые строки имели тип <strong>SeqString</strong>. Технически, InternalizedString отличается от SeqString своей внутренней структурой. Если точнее, в классах <strong>SeqOneByteString</strong> и <strong>SeqTwoByteString</strong> имеется указатель на массив символов <code>chars</code> и ряд методов, которые умеют взаимодействовать с ним. Имплементация класса SeqString выглядит буквально следующим образом:</p>
  <p id="YaV7"><a href="https://chromium.googlesource.com/v8/v8.git/+/refs/heads/12.8.325/src/objects/string.h#733" target="_blank">/src/objects/string.h#733</a></p>
  <pre id="7sV0" data-lang="cpp">// The SeqString abstract class captures sequential string values.
class SeqString : public String {
 public:
  // Truncate the string in-place if possible and return the result.
  // In case of new_length == 0, the empty string is returned without
  // truncating the original string.
  V8_WARN_UNUSED_RESULT static Handle&lt;String&gt; Truncate(Isolate* isolate,
                                                       Handle&lt;SeqString&gt; string,
                                                       int new_length);
                                                       
  struct DataAndPaddingSizes {
    const int data_size;
    const int padding_size;
    bool operator==(const DataAndPaddingSizes&amp; other) const {
      return data_size == other.data_size &amp;&amp; padding_size == other.padding_size;
    }
  };
  DataAndPaddingSizes GetDataAndPaddingSizes() const;
  
  // Zero out only the padding bytes of this string.
  void ClearPadding();
  
  EXPORT_DECL_VERIFIER(SeqString)
};</pre>
  <p id="j3ih">А один из его наследников, SeqOneByteString, так:</p>
  <p id="VwAy"><a href="https://chromium.googlesource.com/v8/v8.git/+/refs/heads/12.8.325/src/objects/string.h#763" target="_blank">/src/objects/string.h#763</a></p>
  <pre id="8ivV" data-lang="cpp">// The OneByteString class captures sequential one-byte string objects.
// Each character in the OneByteString is an one-byte character.
V8_OBJECT class SeqOneByteString : public SeqString {
 public:
  static const bool kHasOneByteEncoding = true;
  using Char = uint8_t;
  
  V8_INLINE static constexpr int32_t DataSizeFor(int32_t length);
  V8_INLINE static constexpr int32_t SizeFor(int32_t length);
  
  // Dispatched behavior. The non SharedStringAccessGuardIfNeeded method is also
  // defined for convenience and it will check that the access guard is not
  // needed.
  inline uint8_t Get(int index) const;
  inline uint8_t Get(int index,
                     const SharedStringAccessGuardIfNeeded&amp; access_guard) const;
  inline void SeqOneByteStringSet(int index, uint16_t value);
  inline void SeqOneByteStringSetChars(int index, const uint8_t* string,
                                       int length);
                                       
  // Get the address of the characters in this string.
  inline Address GetCharsAddress() const;
  
  // Get a pointer to the characters of the string. May only be called when a
  // SharedStringAccessGuard is not needed (i.e. on the main thread or on
  // read-only strings).
  inline uint8_t* GetChars(const DisallowGarbageCollection&amp; no_gc);
  
  // Get a pointer to the characters of the string.
  inline uint8_t* GetChars(const DisallowGarbageCollection&amp; no_gc,
                           const SharedStringAccessGuardIfNeeded&amp; access_guard);
                           
  DataAndPaddingSizes GetDataAndPaddingSizes() const;
  
  // Initializes padding bytes. Potentially zeros tail of the payload too!
  inline void clear_padding_destructively(int length);
  
  // Maximal memory usage for a single sequential one-byte string.
  static const int kMaxCharsSize = kMaxLength;
  
  inline int AllocatedSize() const;
  
  // A SeqOneByteString have different maps depending on whether it is shared.
  static inline bool IsCompatibleMap(Tagged&lt;Map&gt; map, ReadOnlyRoots roots);
  
  class BodyDescriptor;
  
 private:
  friend struct OffsetsForDebug;
  friend class CodeStubAssembler;
  friend class ToDirectStringAssembler;
  friend class IntlBuiltinsAssembler;
  friend class StringBuiltinsAssembler;
  friend class StringFromCharCodeAssembler;
  friend class maglev::MaglevAssembler;
  friend class compiler::AccessBuilder;
  friend class TorqueGeneratedSeqOneByteStringAsserts;
  
  FLEXIBLE_ARRAY_MEMBER(Char, chars);
} V8_OBJECT_END;</pre>
  <p id="hV1Z">Для сравнения, класс InternalizedString выглядит следующим образом:</p>
  <p id="KzPa"><a href="https://chromium.googlesource.com/v8/v8.git/+/refs/heads/12.8.325/src/objects/string.h#758" target="_blank">/src/objects/string.h#758</a></p>
  <pre id="zKx9" data-lang="cpp">V8_OBJECT class InternalizedString : public String{

  // TODO(neis): Possibly move some stuff from String here.
} V8_OBJECT_END;</pre>
  <p id="SaO4">Как видите, здесь вообще нет никакой реализации, так как само понятие InternalizedString было введено лишь для удобства терминологии. Фактически, вся логика по выделению памяти, кодированию и декодированию, сравнению и модификации строк лежит в базовом классе <code>String</code>. Символы хранятся не в виде массива, а непосредственно в виде 32-разрядной или 64-разрядной последовательности кодов символов в памяти. Сам класс имеет только системный указатель на начало соответствующей области. Такая структура называется &quot;FlatContent&quot;, а такие строки соответственно — плоскими.</p>
  <pre id="7MkR" data-lang="cpp">V8_OBJECT class String : public Name {
  ...
 private:
  union {
    const uint8_t* onebyte_start;
    const base::uc16* twobyte_start;
  };
  ...
}</pre>
  <p id="4loA">Так что же такое SeqString на практике?</p>
  <pre id="DgWG" data-lang="javascript">d8&gt; const seqString = [
  &quot;F&quot;, &quot;r&quot;, &quot;o&quot;, &quot;n&quot;, &quot;t&quot;, &quot;e&quot;, &quot;n&quot;, &quot;d&quot;,
  &quot;A&quot;, &quot;l&quot;, &quot;m&quot;, &quot;a&quot;, &quot;n&quot;, &quot;a&quot;, &quot;c&quot;
].join(&quot;&quot;);
d8&gt; 
d8&gt; %DebugPrint(seqString);

DebugPrint: 0x2353001c94e1: [String]: &quot;FrontendAlmanac&quot;
0x235300000105: [Map] in ReadOnlySpace
 - map: 0x2353000004c5 &lt;MetaMap (0x23530000007d &lt;null&gt;)&gt;
 - type: SEQ_ONE_BYTE_STRING_TYPE
 - instance size: variable
 - elements kind: HOLEY_ELEMENTS
 - enum length: invalid
 - stable_map
 - non-extensible
 - back pointer: 0x235300000061 &lt;undefined&gt;
 - prototype_validity cell: 0
 - instance descriptors (own) #0: 0x235300000701 &lt;DescriptorArray[0]&gt;
 - prototype: 0x23530000007d &lt;null&gt;
 - constructor: 0x23530000007d &lt;null&gt;
 - dependent code: 0x2353000006dd &lt;Other heap object (WEAK_ARRAY_LIST_TYPE)&gt;
 - construction counter: 0</pre>
  <p id="pGBi">В примере выше строка создана посредством объединения элементов массива. Так как во время этой операции массив в любом случае должен был создаться, а итоговую строку нельзя проверить в StringTable, пока она не будет объединена, переменная получает тип SeqString.</p>
  <p id="uqlj">Давайте немного изменим предыдущий пример.</p>
  <pre id="2egZ" data-lang="javascript">function V8Snapshot() {
  this.string1 = &quot;FrontendAlmanac&quot;;
  this.string2 = [
    &quot;F&quot;, &quot;r&quot;, &quot;o&quot;, &quot;n&quot;, &quot;t&quot;, &quot;e&quot;, &quot;n&quot;, &quot;d&quot;,
    &quot;A&quot;, &quot;l&quot;, &quot;m&quot;, &quot;a&quot;, &quot;n&quot;, &quot;a&quot;, &quot;c&quot;
  ].join(&quot;&quot;);
}

const v8Snapshot = new V8Snapshot();</pre>
  <p id="gAx1">Как будет выглядеть таблица строк в этом случае?</p>
  <figure id="jdkX" class="m_column">
    <img src="https://img4.teletype.in/files/b8/66/b8666fe6-3912-4ffa-9147-56f505dbbe10.png" width="1880" />
  </figure>
  <p id="YNqy">Так как <code>string1</code> и <code>string2</code> — два разных объекта с разными типами строк, каждая из них имеет свой собственный адрес. Более того, у каждой из них будет свой уникальный хэш-ключ. Поэтому в таблице мы можем видеть два одинаковых значения с разными ключами. Другими словами, имеются дубликаты строк. Вообще, все подобные дубликаты можно увидеть, применив фильтр <code>Duplicated strings</code> в слепке.</p>
  <p id="YT04">Более того, если мы попытаемся создать несколько SeqString с одинаковым значением, их дубликаты также не будут в таблице.</p>
  <pre id="g88A" data-lang="javascript">function V8Snapshot() {
  this.string1 = &quot;FrontendAlmanac&quot;;
  this.string2 = [
    &quot;F&quot;, &quot;r&quot;, &quot;o&quot;, &quot;n&quot;, &quot;t&quot;, &quot;e&quot;, &quot;n&quot;, &quot;d&quot;,
    &quot;A&quot;, &quot;l&quot;, &quot;m&quot;, &quot;a&quot;, &quot;n&quot;, &quot;a&quot;, &quot;c&quot;
  ].join(&quot;&quot;);
  this.string3 = [
    &quot;F&quot;, &quot;r&quot;, &quot;o&quot;, &quot;n&quot;, &quot;t&quot;, &quot;e&quot;, &quot;n&quot;, &quot;d&quot;,
    &quot;A&quot;, &quot;l&quot;, &quot;m&quot;, &quot;a&quot;, &quot;n&quot;, &quot;a&quot;, &quot;c&quot;
  ].join(&quot;&quot;);
}

const v8Snapshot = new V8Snapshot();</pre>
  <figure id="dEdR" class="m_column">
    <img src="https://img4.teletype.in/files/fb/7c/fb7c2f92-d5ce-4651-ab51-2a8690380d0d.png" width="1960" />
  </figure>
  <h2 id="4sGZ">ConsString</h2>
  <p id="A7fB">Рассмотрим еще один пример</p>
  <pre id="qTYc" data-lang="javascript">d8&gt; const consString = &quot;Frontend&quot; + &quot;Almanac&quot;;
d8&gt; %DebugPrint(consString);

DebugPrint: 0xf0001c952d: [String]: c&quot;FrontendAlmanac&quot;
0xf000000155: [Map] in ReadOnlySpace
 - map: 0x00f0000004c5 &lt;MetaMap (0x00f00000007d &lt;null&gt;)&gt;
 - type: CONS_ONE_BYTE_STRING_TYPE
 - instance size: 20
 - elements kind: HOLEY_ELEMENTS
 - enum length: invalid
 - non-extensible
 - back pointer: 0x00f000000061 &lt;undefined&gt;
 - prototype_validity cell: 0
 - instance descriptors (own) #0: 0x00f000000701 &lt;DescriptorArray[0]&gt;
 - prototype: 0x00f00000007d &lt;null&gt;
 - constructor: 0x00f00000007d &lt;null&gt;
 - dependent code: 0x00f0000006dd &lt;Other heap object (WEAK_ARRAY_LIST_TYPE)&gt;
 - construction counter: 0</pre>
  <p id="lzgN">Переменная <code>consString</code> образована конкатенацией двух строковых литералов. Такие строки получают тип ConsString. </p>
  <p id="JKZP"><a href="https://chromium.googlesource.com/v8/v8.git/+/refs/heads/12.8.325/src/objects/string.h#916" target="_blank">/src/objects/string.h#916</a></p>
  <pre id="B8yW" data-lang="cpp">V8_OBJECT class ConsString : public String {
 public:
  inline Tagged&lt;String&gt; first() const;
  inline void set_first(Tagged&lt;String&gt; value,
                        WriteBarrierMode mode = UPDATE_WRITE_BARRIER);
  
  inline Tagged&lt;String&gt; second() const;
  inline void set_second(Tagged&lt;String&gt; value,
                         WriteBarrierMode mode = UPDATE_WRITE_BARRIER);
  
  // Doesn&#x27;t check that the result is a string, even in debug mode.  This is
  // useful during GC where the mark bits confuse the checks.
  inline Tagged&lt;Object&gt; unchecked_first() const;
  
  // Doesn&#x27;t check that the result is a string, even in debug mode.  This is
  // useful during GC where the mark bits confuse the checks.
  inline Tagged&lt;Object&gt; unchecked_second() const;
  
  V8_INLINE bool IsFlat() const;
  
  // Dispatched behavior.
  V8_EXPORT_PRIVATE uint16_t
  Get(int index, const SharedStringAccessGuardIfNeeded&amp; access_guard) const;
  
  // Minimum length for a cons string.
  static const int kMinLength = 13;
  
  DECL_VERIFIER(ConsString)
  
 private:
  friend struct ObjectTraits&lt;ConsString&gt;;
  friend struct OffsetsForDebug;
  friend class V8HeapExplorer;
  friend class CodeStubAssembler;
  friend class ToDirectStringAssembler;
  friend class StringBuiltinsAssembler;
  friend class maglev::MaglevAssembler;
  friend class compiler::AccessBuilder;
  friend class TorqueGeneratedConsStringAsserts;
  
  friend Tagged&lt;String&gt; String::GetUnderlying() const;
  
  TaggedMember&lt;String&gt; first_;
  TaggedMember&lt;String&gt; second_;
} V8_OBJECT_END;</pre>
  <p id="2sPW">Класс ConsString имеет два указателя на другие строки, которые, в свою очередь, могут быть любого типа, в том числе и ConsString. В результате этого данный тип может образовывать бинарное дерево из ConsString, листья которого не ConsString. Итоговое значение такой строки получается конкатенацией листьев дерева слева направо, от самого глубокого узла к первому.</p>
  <pre id="72rL" data-lang="javascript">function V8Snapshot() {
  this.string1 = &quot;FrontendAlmanac&quot;;
  this.string2 = &quot;Frontend&quot; + &quot;Almanac&quot;;
  this.string3 = &quot;Frontend&quot; + &quot;Almanac&quot;;
}

const v8Snapshot = new V8Snapshot();</pre>
  <figure id="OcqV" class="m_column">
    <img src="https://img3.teletype.in/files/24/26/24260f8c-f9df-44f7-8e1b-cbccc9fd9f52.png" width="1954" />
  </figure>
  <p id="OWLw">Как и в случае с SeqString, каждая конкатенированная строка имеет свой собственный экземпляр класса и, соответственно, хэш-ключ. Такие строки также могут дублироваться в таблице строк. Однако, в таблице мы также сможем найти интернализированные листья этой конкатенации.</p>
  <figure id="PNJb" class="m_column">
    <img src="https://img2.teletype.in/files/58/3e/583ef330-0bac-411a-866c-83d69ab784f6.png" width="1954" />
  </figure>
  <figure id="V0Jr" class="m_column">
    <img src="https://img3.teletype.in/files/e7/7d/e77d28a9-f6c8-4ae4-ac24-6c4c9a815ba6.png" width="1956" />
  </figure>
  <p id="W7ze">Важно сделать оговорку. Не любая операция конкатенации приводит к созданию ConsString.</p>
  <pre id="7QgY" data-lang="javascript">d8&gt; const string = &quot;a&quot; + &quot;b&quot;;
d8&gt; %DebugPrint(string);

DebugPrint: 0x1d48001c9ec5: [String]: &quot;ab&quot;
0x1d4800000105: [Map] in ReadOnlySpace
 - map: 0x1d48000004c5 &lt;MetaMap (0x1d480000007d &lt;null&gt;)&gt;
 - type: SEQ_ONE_BYTE_STRING_TYPE
 - instance size: variable
 - elements kind: HOLEY_ELEMENTS
 - enum length: invalid
 - stable_map
 - non-extensible
 - back pointer: 0x1d4800000061 &lt;undefined&gt;
 - prototype_validity cell: 0
 - instance descriptors (own) #0: 0x1d4800000701 &lt;DescriptorArray[0]&gt;
 - prototype: 0x1d480000007d &lt;null&gt;
 - constructor: 0x1d480000007d &lt;null&gt;
 - dependent code: 0x1d48000006dd &lt;Other heap object (WEAK_ARRAY_LIST_TYPE)&gt;
 - construction counter: 0</pre>
  <p id="4cx7">В примере выше строка представлена типом SeqString. Дело в том, что процедуры чтения и записи в структуру ConsString не бесплатны. Хотя сама структура считается эффективной с точки зрения оптимизации памяти, все преимущества проявляются только на относительно длинных строках. В случае же коротких строк накладные расходы по содержанию бинарного дерева сводят на нет все эти преимущества. Поэтому разработчики V8 эмпирическим путем определили критическую длину строки, короче которой структура ConsString неэффективна. Это число — <strong>13</strong>.</p>
  <p id="U0By"><a href="https://chromium.googlesource.com/v8/v8.git/+/refs/heads/12.8.325/src/objects/string.h#940" target="_blank">/src/objects/string.h#940</a></p>
  <pre id="NS1Q" data-lang="cpp">// Minimum length for a cons string.
static const int kMinLength = 13;</pre>
  <h2 id="TdGQ">SlicedString</h2>
  <pre id="7gXS" data-lang="javascript">d8&gt; const parentString = &quot; FrontendAlmanac FrontendAlmanac &quot;;
d8&gt; const slicedString1 = parentString.substring(1, 16);
d8&gt; const slicedString2 = parentString.slice(1, 16);
d8&gt; const slicedString3 = parentString.trim();
d8&gt; 
d8&gt; %DebugPrint(slicedString1);

DebugPrint: 0x312b001c9509: [String]: &quot;FrontendAlmanac&quot;
0x312b000001a5: [Map] in ReadOnlySpace
 - map: 0x312b000004c5 &lt;MetaMap (0x312b0000007d &lt;null&gt;)&gt;
 - type: SLICED_ONE_BYTE_STRING_TYPE
 - instance size: 20
 - elements kind: HOLEY_ELEMENTS
 - enum length: invalid
 - stable_map
 - non-extensible
 - back pointer: 0x312b00000061 &lt;undefined&gt;
 - prototype_validity cell: 0
 - instance descriptors (own) #0: 0x312b00000701 &lt;DescriptorArray[0]&gt;
 - prototype: 0x312b0000007d &lt;null&gt;
 - constructor: 0x312b0000007d &lt;null&gt;
 - dependent code: 0x312b000006dd &lt;Other heap object (WEAK_ARRAY_LIST_TYPE)&gt;
 - construction counter: 0</pre>
  <p id="QmWg">Этот тип присваивается строкам, образованным как результат подстроки другой строки. К таким операциям относятся <code>substring()</code>, <code>slice()</code> и методы тримминга <code>trim()</code>, <code>trimStart()</code>, <code>trimEnd()</code>.</p>
  <p id="eKEb">Класс SlicedString хранит только указатель на родительскую строку, а также смещение и длину последовательности в родительской строке. Это позволяет значительно сократить расход памяти и время при работе с такими строками. Однако здесь действует то же правило, что и в ConsString: длина подстроки не должна быть меньше 13 символов. В противном случае эта оптимизация не имеет смысла, так как, помимо памяти, SlicedString требует распаковки родительской строки и последующего добавления смещения к стартовому адресу последовательности.</p>
  <p id="8iqy">Еще одной важной особенностью является то, что SlicedString не может быть вложенной.</p>
  <pre id="p9FJ" data-lang="javascript">function V8Snapshot() {
  this.parentString = &quot; FrontendAlmanac FrontendAlmanac &quot;;
  this.slicedString1 = this.parentString.trim();
  this.slicedString2 = this.slicedString1.substring(0, 15);
}

const v8Snapshot = new V8Snapshot();</pre>
  <p id="1YNn">В примере выше мы пытаемся создать SlicedString от другой SlicedString. Однако <code>slicedString1</code> не может выступать в роли родительской строки, так как сама является SlicedString.</p>
  <figure id="7Biz" class="m_column">
    <img src="https://img1.teletype.in/files/03/b2/03b20f48-82d1-4def-9f06-4b84def16aab.png" width="2024" />
  </figure>
  <p id="hQ0t">Поэтому родительской строкой для обоих свойств будет <code>parentString</code>.</p>
  <h2 id="55w5">ThinString</h2>
  <p id="c9nu">Бывает так, что нужно провести интернализацию строки, но по какой-то причине сделать это прямо сейчас нельзя. В таком случае создаётся новый объект строки, интернализируется, а оригинальная строка конвертируется в тип <strong>ThinString</strong>, который, по сути, является ссылкой на свою интернализированную версию. ThinString очень похож на ConsString, только с одной ссылкой.</p>
  <pre id="tgHc" data-lang="javascript">d8&gt; const string = &quot;Frontend&quot; + &quot;Almanac&quot;; // создаем ConsString
d8&gt; const obj = {};
d8&gt; 
d8&gt; obj[string]; // конвертируем в ThinString
d8&gt; 
d8&gt; %DebugPrint(string);

DebugPrint: 0x335f001c947d: [String]: &gt;&quot;FrontendAlmanac&quot;
0x335f00000425: [Map] in ReadOnlySpace
 - map: 0x335f000004c5 &lt;MetaMap (0x335f0000007d &lt;null&gt;)&gt;
 - type: THIN_ONE_BYTE_STRING_TYPE
 - instance size: 16
 - elements kind: HOLEY_ELEMENTS
 - enum length: invalid
 - stable_map
 - non-extensible
 - back pointer: 0x335f00000061 &lt;undefined&gt;
 - prototype_validity cell: 0
 - instance descriptors (own) #0: 0x335f00000701 &lt;DescriptorArray[0]&gt;
 - prototype: 0x335f0000007d &lt;null&gt;
 - constructor: 0x335f0000007d &lt;null&gt;
 - dependent code: 0x335f000006dd &lt;Other heap object (WEAK_ARRAY_LIST_TYPE)&gt;
 - construction counter: 0</pre>
  <p id="qsNC">В примере выше мы первым делом создаём ConsString. Далее мы используем эту строку в качестве ключа объекта. Чтобы найти ключ в массиве свойств объекта, строка должна быть плоской, однако сейчас она представляет собой бинарное дерево с двумя узлами. В этом случае движок вынужден вычислить плоское значение из ConsString, создать новый объект строки и интернализировать его, а оригинальную строку сконвертировать в ThinString. Подобный кейс, кстати, упоминался в статье про <a href="https://habr.com/ru/articles/449368/" target="_blank">очистку строк</a> на Хабре, правда, не было объяснено, почему так происходит.</p>
  <figure id="LKCn" class="m_column">
    <img src="https://img4.teletype.in/files/71/b9/71b97002-be26-4474-b272-98fdc3ee2110.png" width="2056" />
  </figure>
  <p id="yzP1">Давайте посмотрим в DevTools. ThinString тут мы не увидим. Но можно заметить, что свойство <code>string</code> представлено как <strong>InternalizedString</strong>, так как ThinString — это, повторюсь, лишь ссылка на интернализированную версию строки.</p>
  <h2 id="ibXA">ExternalString</h2>
  <p id="nOSB">Еще одним типом строк является <strong>ExternalString</strong>. Движок V8 предусматривает возможность создания и хранения строк за пределами самого движка. Специально для этого был введён тип ExternalString и соответствующее API. Ссылки на эти строки хранятся в отдельной таблице ExternalStringTable в куче. Как правило, такие строки создаются потребителем движка для каких-либо собственных нужд. Например, браузеры могут таким образом хранить какие-то внешние ресурсы. Также потребитель может полностью контролировать жизненный цикл этих ресурсов, но с одной оговоркой: потребитель должен гарантировать, что такая строка не будет деаллоцирована, пока объект ExternalString жив в куче V8.</p>
  <figure id="R4HQ" class="m_column">
    <img src="https://img4.teletype.in/files/7d/5b/7d5b18ec-7d86-4269-b9fc-d45942684eec.png" width="2052" />
  </figure>
  <p id="lSKV">На скриншоте выше как раз одна из таких строк, созданных браузером. Но мы можем создать и свою собственную. Для этого можно воспользоваться внутренним API-методом <strong>externalizeString</strong> (V8 должен быть запущен с флагом <code>--allow-natives-syntax</code>).</p>
  <pre id="TjCw" data-lang="javascript">//&gt; d8 --allow-natives-syntax --expose-externalize-string
d8&gt; const string = &quot;FrontendAlmanac&quot;;
d8&gt; 
d8&gt; externalizeString(string);
d8&gt; 
d8&gt; %DebugPrint(string);

DebugPrint: 0x7d6000da08d: [String] in OldSpace: #FrontendAlmanac
0x7d600000335: [Map] in ReadOnlySpace
 - map: 0x07d6000004c5 &lt;MetaMap (0x07d60000007d &lt;null&gt;)&gt;
 - type: EXTERNAL_INTERNALIZED_ONE_BYTE_STRING_TYPE
 - instance size: 20
 - elements kind: HOLEY_ELEMENTS
 - enum length: invalid
 - stable_map
 - non-extensible
 - back pointer: 0x07d600000061 &lt;undefined&gt;
 - prototype_validity cell: 0
 - instance descriptors (own) #0: 0x07d600000701 &lt;DescriptorArray[0]&gt;
 - prototype: 0x07d60000007d &lt;null&gt;
 - constructor: 0x07d60000007d &lt;null&gt;
 - dependent code: 0x07d6000006dd &lt;Other heap object (WEAK_ARRAY_LIST_TYPE)&gt;
 - construction counter: 0</pre>
  <h2 id="uxNy">Сборка мусора</h2>
  <p id="9zGE">Строки, как и любые другие переменные, участвуют в процессе сборки мусора. Об этом я довольно подробно писал в статье <a href="https://blog.frontend-almanac.ru/v8-garbage-collection" target="_blank">Сборка мусора в V8</a>, поэтому останавливаться на этом не буду. Гораздо интереснее, что сама StringTable тоже участвует в процессе. Если говорить точнее, любые трансформации и удаления строк в StringTable происходят во время Full GC. Для этого используется временная таблица StringForwardingTable, в которую во время сборки мусора попадают только актуальные строки. После чего ссылка на StringTable меняется на новую таблицу.</p>
  <h2 id="WAZa">Заключение</h2>
  <p id="k1IY">Итак, мы познакомились с организацией строк внутри движка V8. Узнали больше про таблицу строк и разные типы самих строк.</p>
  <p id="xPMT">Немного основных моментов и выводов.</p>
  <ul id="2yNB">
    <li id="FmCh">Строки бывают однобайтные и двубайтные. Двубайтным требуется примерно в два раза больше памяти, так как каждый символ такой строки кодируется двумя байтами вне зависимости от того, является ли он символом ASCII или нет. Поэтому, если есть выбор, какую строку использовать, однобайтная в большинстве случаев будет предпочтительнее.</li>
  </ul>
  <pre id="WRgG" data-lang="javascript">const myMap = {
  &quot;Frontend Almanac&quot;: undefined, // однобайтный ключ
  &quot;Frontend Альманах&quot;: undefined, // двубайтный ключ
}</pre>
  <ul id="kSsd">
    <li id="2OuG">Несмотря на указанное в спецификации число <code>2**53 - 1</code>, фактическая длина строки на реальных системах гораздо ниже. В 32-разрядных системах V8 позволяет хранить строки длиной не более <code>2**28 - 16</code> символов, а в 64-разрядных — не более <code>2**29 - 24</code>.</li>
    <li id="cYrY">Seq, Cons и Sliced строки могут дублироваться в таблице строк.</li>
  </ul>
  <pre id="kqqL" data-lang="javascript">const string1 = &quot;FrontendAlmanac&quot;; // создает интернализированную строку
const string2 = &quot;FrontendAlmanac&quot;; // не создает новую строку
const string3 = &quot;Frontend&quot; + &quot;Almanac&quot;; // создает дубликат строки</pre>
  <ul id="3bSb">
    <li id="mOIZ">SlicedString не могут быть вложенными. Они всегда ссылаются на исходную интернализированную строку. Родитель жив, пока жива хотя бы одна его дочерняя SlicedString.</li>
  </ul>
  <pre id="mkQk" data-lang="javascript">let parentString = &quot;FrontendAlmanac&quot;; // родительская строка
let slicedString1 = parentString.slice(1); // ссылается на parentString
let slicedString2 = slicedString1.slice(1); // тоже ссылается на parentString
let slicedString3 = slicedString2.slice(1); // и эта тоже

slicedString1 = undefind;
slicedString2 = undefind;
// parentString всё еще не собрана сборщиком мусора,
// так как пока жива slicedString3</pre>
  <ul id="Bbj0">
    <li id="p6q2">В случае, если строка требует интернализации, но &quot;на месте&quot; этого сделать нельзя, например, если она Cons или Sliced, движок вычислит плоское значение, создаст новый интернализированный объект строки и сконвертирует ссылку на эту интернализированную версию.</li>
  </ul>
  <pre id="ufff" data-lang="javascript">const string = &quot;Frontend&quot; + &quot;Almanac&quot;; // ConsString

const obj = {};
obj[string]; // ThinString

// строка больше не ConsString, теперь она ThinString и ссылается на
// плоское интернализированное значение в StringTable</pre>
  <ul id="VQuH">
    <li id="UmOk">Предыдущий пример можно использовать для избавления от дубликатов строк в StringTable, как, например, предложено в статье про <a href="https://habr.com/ru/articles/449368/" target="_blank">очистку строк</a>.</li>
  </ul>
  <pre id="dRnA" data-lang="javascript">const string1 = &quot;FrontendAlmanac&quot;;
const string2 = &quot;Frontend&quot; + &quot;Almanac&quot;; // создает дубликат
const string3 = &quot;Frontend&quot; + &quot;Almanac&quot;; // создает дубликат

const obj = {};
obj[string2]; // ThinSting
obj[string3]; // ThinString

// Теперь string2 и string3 больше не ConsString и ссылаются
// свою интернализированную версию, в данном случае на string1,
// которая уже присутствует в таблице строк.</pre>
  <p id="fytB"></p>
  <p id="LmHc">Я описал только некоторые случаи работы со строками. Формата статьи не хватит, чтобы покрыть все возможные особенности и частные случаи. Поэтому приглашаю всех желающих описать свои случаи и задать вопросы в комментариях в моём телеграм-канале.</p>
  <p id="lJlp"></p>
  <hr />
  <p id="3LV0"></p>
  <p id="MKYX"><strong>Мои телеграмм-каналы:</strong></p>
  <section style="background-color:hsl(hsl(55,  86%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="YnAq">EN - <a href="https://t.me/frontend_almanac" target="_blank">https://t.me/frontend_almanac</a><br />RU - <a href="https://t.me/frontend_almanac_ru" target="_blank">https://t.me/frontend_almanac_ru</a></p>
  </section>
  <p id="Ri5D"><em>English version: <a href="https://blog.frontend-almanac.com/v8-strings" target="_blank">https://blog.frontend-almanac.com/v8-strings</a></em></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.frontend-almanac.ru/react-memoization</guid><link>https://blog.frontend-almanac.ru/react-memoization?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=frontend_almanac_ru</link><comments>https://blog.frontend-almanac.ru/react-memoization?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=frontend_almanac_ru#comments</comments><dc:creator>frontend_almanac_ru</dc:creator><title>React. Обновление узлов и мемоизация</title><pubDate>Wed, 12 Jun 2024 11:58:50 GMT</pubDate><category>React ⚛️</category><description><![CDATA[<img src="https://img2.teletype.in/files/10/e1/10e18547-c53e-49d1-9ae9-995ed13d4888.png"></img>В данной статье мы заглянем &quot;под капот&quot; движка React и увидем, как именно происходит обновление узлов. Параллельно рассмотрим и основные принципы мемоизации.]]></description><content:encoded><![CDATA[
  <p id="Yw7o">В процессе разработки современных веб-приложений производительность часто становится одним из ключевых аспектов, которые волнуют и разработчиков, и пользователей. Пользователи ожидают молниеносного отклика, а разработчики стремятся создать приложения, которые работают быстро и эффективно.</p>
  <p id="Wv1C">Одним из мощных инструментов, позволяющих достигнуть высокой производительности в React-приложениях, является мемоизация. Мемоизация помогает значительно сократить количество вычислений и, соответственно, обновлений интерфейса, что положительно сказывается на общей скорости и отзывчивости приложения.</p>
  <p id="1hiH">В данной статье мы заглянем &quot;под капот&quot; движка React и увидем, как именно происходит обновление узлов. Параллельно рассмотрим и основные принципы мемоизации и её применение в различных типах компонентов.</p>
  <h2 id="oM0i">Что такое мемоизация?</h2>
  <p id="RWXZ">Начнем, пожалуй, с теории. Формально, мемоизацию можно определить как оптимизационную технику, применяемую для увеличения производительности программ за счёт сохранения результатов вызова функции и возвращения сохранённого результата при повторных вызовах с теми же аргументами.</p>
  <p id="K1om">В контексте React мемоизация особенно полезна для предотвращения лишних перерисовок и повышения производительности приложений. Другими словами, она помогает избегать ненужных обновлений компонентов, что делает приложение более реактивным и эффективным. Вторым не менее важным свойством мемоизации является сокращение количества дорогостоящих вычислений. Вместо того, чтобы производить их на каждом рендере компонента, можно &quot;запомнить результат&quot; и пересчитывать его только в случае изменения соответствующих входных параметров.</p>
  <h2 id="1Win">Где конкретно хранится мемоизированный результат?</h2>
  <p id="T5M8">Чтобы ответить на этот вопрос, нам понадобится понятие React Fiber. Подробно я описывал эту структуру в статье <a href="https://blog.frontend-almanac.ru/z1S4MzuquZd" target="_blank">Детальный React. Реконсиляция, рендеры, Fiber, виртуальное дерево</a>. Если коротко, Fiber - это некая абстрактная сущность движка React, с помощью которой описывается любой обрабатываемый узел React, будь то компонент, хук или host root. У Fiber, кроме прочего, имеются свойства <code>pendingProps</code>, <code>memoizedProps</code> и <code>memoizedState</code>. Именно в этих свойствах и хранится мемоизированный результат. Как именно? Посмотрим далее.</p>
  <h2 id="FznF">Мемоизируемые компоненты</h2>
  <p id="ue9Z">Хоть принципы мемоизации в React одинаковы для всех сущностей, детали реализации, все таки могут отличаться, в зависимости от типа Fiber. Для понимания процесса нам потребуется пройтись по каждому типу отдельно. И начнем мы с компонентов, как самой &quot;старой&quot; структуры React.</p>
  <p id="k5nE">Еще с первых версий React мы привыкли делить компоненты на классовые и функциональные. На самом деле, внутри движка типов компонентов куда больше. Тип компонента является типом Fiber и указывается в свойстве <code>Fiber.tag</code>. <a href="https://blog.frontend-almanac.ru/z1S4MzuquZd" target="_blank">В выше указанной статье</a> я приводил полный список типов для версии React 18.2, где в общей сложности насчитывалось 28 типов . В версии 18.3.1 список немного изменился, теперь их всего 26 штук.</p>
  <pre id="TFTK" data-lang="flow">export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // Before we know whether it is function or class
export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
export const HostComponent = 5;
export const HostText = 6;
export const Fragment = 7;
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
export const DehydratedFragment = 18;
export const SuspenseListComponent = 19;
export const ScopeComponent = 21;
export const OffscreenComponent = 22;
export const LegacyHiddenComponent = 23;
export const CacheComponent = 24;
export const TracingMarkerComponent = 25;</pre>
  <p id="I8C3">Для понимания процессов мемоизации, из этого списка нам понадобятся только три:</p>
  <pre id="LXIa" data-lang="flow">export const FunctionComponent = 0;
export const ClassComponent = 1;
export const MemoComponent = 14;</pre>
  <h2 id="8RtS">Когда происходит мемоизация?</h2>
  <p id="DIEy">Прежде чем перейти непосредственно к типам Fiber, стоит разобраться в какой конкретно момент запускается сам процесс мемоизации. <a href="https://blog.frontend-almanac.ru/z1S4MzuquZd" target="_blank">В предыдущей статье</a> я выделил четыре фазы обработки узла. Первая фаза <strong>begin</strong> определяет тип Fiber, в зависимости от которого будет запущен нужный жизненный цикл. Узлы, которые еще небыли смонтированы имеют тип <code>IndeterminateComponent</code>, <code>LazyComponent</code> или <code>IncompleteClassComponent</code>. Для таких Fiber будет запущен процесс монтирования. В остальных же случаях реконсиллер попытается обновить существующий узел. Если быть более конкретным, дерево решений принимается функцией <code>beginWork</code> реконсиллера.</p>
  <p id="oIxO"><a href="https://github.com/facebook/react/blob/v18.3.1/packages/react-reconciler/src/ReactFiberBeginWork.new.js#L3699" target="_blank">/packages/react-reconciler/src/ReactFiberBeginWork.new.js#L3699</a></p>
  <pre id="aeC2" data-lang="flow">function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  ...
  
  switch (workInProgress.tag) {
    case IndeterminateComponent: {
      return mountIndeterminateComponent(
        current,
        workInProgress,
        workInProgress.type,
        renderLanes,
      );
    }
    case LazyComponent: {
      const elementType = workInProgress.elementType;
      return mountLazyComponent(
        current,
        workInProgress,
        elementType,
        renderLanes,
      );
    }
    case FunctionComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    case ClassComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      return updateClassComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    case HostRoot:
      return updateHostRoot(current, workInProgress, renderLanes);
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes);
    case HostText:
      return updateHostText(current, workInProgress);
    case SuspenseComponent:
      return updateSuspenseComponent(current, workInProgress, renderLanes);
    case HostPortal:
      return updatePortalComponent(current, workInProgress, renderLanes);
    case ForwardRef: {
      const type = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === type
          ? unresolvedProps
          : resolveDefaultProps(type, unresolvedProps);
      return updateForwardRef(
        current,
        workInProgress,
        type,
        resolvedProps,
        renderLanes,
      );
    }
    case Fragment:
      return updateFragment(current, workInProgress, renderLanes);
    case Mode:
      return updateMode(current, workInProgress, renderLanes);
    case Profiler:
      return updateProfiler(current, workInProgress, renderLanes);
    case ContextProvider:
      return updateContextProvider(current, workInProgress, renderLanes);
    case ContextConsumer:
      return updateContextConsumer(current, workInProgress, renderLanes);
    case MemoComponent: {
      const type = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      // Resolve outer props first, then resolve inner props.
      let resolvedProps = resolveDefaultProps(type, unresolvedProps);
      resolvedProps = resolveDefaultProps(type.type, resolvedProps);
      return updateMemoComponent(
        current,
        workInProgress,
        type,
        resolvedProps,
        renderLanes,
      );
    }
    case SimpleMemoComponent: {
      return updateSimpleMemoComponent(
        current,
        workInProgress,
        workInProgress.type,
        workInProgress.pendingProps,
        renderLanes,
      );
    }
    case IncompleteClassComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      return mountIncompleteClassComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    case SuspenseListComponent: {
      return updateSuspenseListComponent(current, workInProgress, renderLanes);
    }
    case ScopeComponent: {
      if (enableScopeAPI) {
        return updateScopeComponent(current, workInProgress, renderLanes);
      }
      break;
    }
    case OffscreenComponent: {
      return updateOffscreenComponent(current, workInProgress, renderLanes);
    }
    case LegacyHiddenComponent: {
      if (enableLegacyHidden) {
        return updateLegacyHiddenComponent(
          current,
          workInProgress,
          renderLanes,
        );
      }
      break;
    }
    case CacheComponent: {
      if (enableCache) {
        return updateCacheComponent(current, workInProgress, renderLanes);
      }
      break;
    }
    case TracingMarkerComponent: {
      if (enableTransitionTracing) {
        return updateTracingMarkerComponent(
          current,
          workInProgress,
          renderLanes,
        );
      }
      break;
    }
  }
  
  ...
}</pre>
  <p id="ZvXO">С точки зрения мемоизации, монитруется компонент или обновляется, большой разницы нет. В конечном итоге, соответствующие методы обновления компонента будут вызваны в любом случае. Разница только в том, будут ли в эти методы переданы предыдущие значения свойств, состояния или зависимостей. Оставлю процесс трансформации монтируемыех типов в обновляемые для следующих публикаций и предлагаю перейти непосредственно к методам обновления конкретных узлов.</p>
  <h2 id="9bcU">ClassComponent</h2>
  <p id="c2m2">Из наименования понятно, что данный тип присваивается классовым компонентам. На всякий случай, вспомним, как создается классовый компонент.</p>
  <pre id="D5gP" data-lang="typescript">class MyComponent extends React.Component&lt;{ prop1: string }&gt; {
  render() {
    return (
      &lt;div&gt;
        {this.props.prop1}
      &lt;/div&gt;
    );
  }
}</pre>
  <p id="0dMt">В приведенном выше <code>beginWork</code> обновление классовых компонентов начинается следующим образом.</p>
  <pre id="8fNp" data-lang="flow">const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
  workInProgress.elementType === Component
    ? unresolvedProps
    : resolveDefaultProps(Component, unresolvedProps);
return updateClassComponent(
  current,
  workInProgress,
  Component,
  resolvedProps,
  renderLanes,
);</pre>
  <p id="O757">Первое, что тут бросается в глаза, это константа <code>resolvedProps</code>. Не смотря на аж 5 строк кода, эта константа всего лишь берет свойство <code>Fiber.pendingProps</code>, которое является ничем иным, как объектом с новыми свойствами компонента. Дабы не оставлять неясности, приведу и код функции <code>resolveDefaultProps</code>.</p>
  <p id="3DsC"><a href="https://github.com/facebook/react/blob/v18.3.1/packages/react-reconciler/src/ReactFiberLazyComponent.new.js#L12" target="_blank">/packages/react-reconciler/src/ReactFiberLazyComponent.new.js#L12</a></p>
  <pre id="FjVb" data-lang="flow">export function resolveDefaultProps(Component: any, baseProps: Object): Object {
  if (Component &amp;&amp; Component.defaultProps) {
    // Resolve default props. Taken from ReactElement
    const props = assign({}, baseProps);
    const defaultProps = Component.defaultProps;
    for (const propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
    return props;
  }
  return baseProps;
}</pre>
  <p id="5189">В переводе на человеческий язык, эта функция всего лишь присваивает дефолтные значения свойствам, если таковые были указаны при создании компонента.</p>
  <pre id="V7Mv" data-lang="typescript">class MyComponent extends React.Component&lt;{ color?: string }&gt; {
  static defaultProps = {
    color: &#x27;blue&#x27;,
  };

  render() {
    return (
      &lt;div&gt;
        {this.props.color}
      &lt;/div&gt;
    );
  }
}

&lt;MyComponent /&gt;              // &lt;div&gt;blue&lt;/div&gt;
&lt;MyComponent color=&quot;red&quot; /&gt;  // &lt;div&gt;red&lt;/div&gt;</pre>
  <p id="77qY">Сам же механизм обновления вынесен в функцию <code>updateClassComponent</code>. Приведу её код в сокращенном виде, так как часть функционала в ней отвечает за обработку провайдера контекста и некоторые служебные операции для dev-mode.</p>
  <p id="oeaf"><a href="https://github.com/facebook/react/blob/v18.3.1/packages/react-reconciler/src/ReactFiberBeginWork.new.js#L1062" target="_blank">/packages/react-reconciler/src/ReactFiberBeginWork.new.js#L1062</a></p>
  <pre id="BeEp" data-lang="flow">function updateClassComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  renderLanes: Lanes,
) {
  ...
  const instance = workInProgress.stateNode;
  let shouldUpdate;
  if (instance === null) {
    resetSuspendedCurrentOnMountInLegacyMode(current, workInProgress);
    
    // In the initial pass we might need to construct the instance.
    constructClassInstance(workInProgress, Component, nextProps);
    mountClassInstance(workInProgress, Component, nextProps, renderLanes);
    shouldUpdate = true;
  } else if (current === null) {
    // In a resume, we&#x27;ll already have an instance we can reuse.
    shouldUpdate = resumeMountClassInstance(
      workInProgress,
      Component,
      nextProps,
      renderLanes,
    );
  } else {
    shouldUpdate = updateClassInstance(
      current,
      workInProgress,
      Component,
      nextProps,
      renderLanes,
    );
  }
  const nextUnitOfWork = finishClassComponent(
    current,
    workInProgress,
    Component,
    shouldUpdate,
    hasContext,
    renderLanes,
  );
  ...
  return nextUnitOfWork;
}</pre>
  <p id="sbV8">Первые два if здесь отвечают за процесс монтирования нового компонента. Как я уже говорил выше, отдельно эти процессы разбирать не будем, так как в конечном итоге цикл все равно прийдет к функции обновления и вся работа с мемоизация будет происходить именно там. А именно, в функции <code>updateClassInstance</code>, её я приведу целиком.</p>
  <p id="pBZq"><a href="https://github.com/facebook/react/blob/v18.3.1/packages/react-reconciler/src/ReactFiberClassComponent.new.js#L1123" target="_blank">/packages/react-reconciler/src/ReactFiberClassComponent.new.js#L1123</a></p>
  <pre id="Se51" data-lang="flow">// Invokes the update life-cycles and returns false if it shouldn&#x27;t rerender.
function updateClassInstance(
  current: Fiber,
  workInProgress: Fiber,
  ctor: any,
  newProps: any,
  renderLanes: Lanes,
): boolean {
  const instance = workInProgress.stateNode;
  
  cloneUpdateQueue(current, workInProgress);
  
  const unresolvedOldProps = workInProgress.memoizedProps;
  const oldProps =
    workInProgress.type === workInProgress.elementType
      ? unresolvedOldProps
      : resolveDefaultProps(workInProgress.type, unresolvedOldProps);
  instance.props = oldProps;
  const unresolvedNewProps = workInProgress.pendingProps;
  
  const oldContext = instance.context;
  const contextType = ctor.contextType;
  let nextContext = emptyContextObject;
  if (typeof contextType === &#x27;object&#x27; &amp;&amp; contextType !== null) {
    nextContext = readContext(contextType);
  } else if (!disableLegacyContext) {
    const nextUnmaskedContext = getUnmaskedContext(workInProgress, ctor, true);
    nextContext = getMaskedContext(workInProgress, nextUnmaskedContext);
  }
  
  const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
  const hasNewLifecycles =
    typeof getDerivedStateFromProps === &#x27;function&#x27; ||
    typeof instance.getSnapshotBeforeUpdate === &#x27;function&#x27;;
    
  // Note: During these life-cycles, instance.props/instance.state are what
  // ever the previously attempted to render - not the &quot;current&quot;. However,
  // during componentDidUpdate we pass the &quot;current&quot; props.
  
  // In order to support react-lifecycles-compat polyfilled components,
  // Unsafe lifecycles should not be invoked for components using the new APIs.
  if (
    !hasNewLifecycles &amp;&amp;
    (typeof instance.UNSAFE_componentWillReceiveProps === &#x27;function&#x27; ||
      typeof instance.componentWillReceiveProps === &#x27;function&#x27;)
  ) {
    if (
      unresolvedOldProps !== unresolvedNewProps ||
      oldContext !== nextContext
    ) {
      callComponentWillReceiveProps(
        workInProgress,
        instance,
        newProps,
        nextContext,
      );
    }
  }
  
  resetHasForceUpdateBeforeProcessing();
  
  const oldState = workInProgress.memoizedState;
  let newState = (instance.state = oldState);
  processUpdateQueue(workInProgress, newProps, instance, renderLanes);
  newState = workInProgress.memoizedState;
  
  if (
    unresolvedOldProps === unresolvedNewProps &amp;&amp;
    oldState === newState &amp;&amp;
    !hasContextChanged() &amp;&amp;
    !checkHasForceUpdateAfterProcessing() &amp;&amp;
    !(
      enableLazyContextPropagation &amp;&amp;
      current !== null &amp;&amp;
      current.dependencies !== null &amp;&amp;
      checkIfContextChanged(current.dependencies)
    )
  ) {
    // If an update was already in progress, we should schedule an Update
    // effect even though we&#x27;re bailing out, so that cWU/cDU are called.
    if (typeof instance.componentDidUpdate === &#x27;function&#x27;) {
      if (
        unresolvedOldProps !== current.memoizedProps ||
        oldState !== current.memoizedState
      ) {
        workInProgress.flags |= Update;
      }
    }
    if (typeof instance.getSnapshotBeforeUpdate === &#x27;function&#x27;) {
      if (
        unresolvedOldProps !== current.memoizedProps ||
        oldState !== current.memoizedState
      ) {
        workInProgress.flags |= Snapshot;
      }
    }
    return false;
  }
  
  if (typeof getDerivedStateFromProps === &#x27;function&#x27;) {
    applyDerivedStateFromProps(
      workInProgress,
      ctor,
      getDerivedStateFromProps,
      newProps,
    );
    newState = workInProgress.memoizedState;
  }
  
  const shouldUpdate =
    checkHasForceUpdateAfterProcessing() ||
    checkShouldComponentUpdate(
      workInProgress,
      ctor,
      oldProps,
      newProps,
      oldState,
      newState,
      nextContext,
    ) ||
    // TODO: In some cases, we&#x27;ll end up checking if context has changed twice,
    // both before and after &#x60;shouldComponentUpdate&#x60; has been called. Not ideal,
    // but I&#x27;m loath to refactor this function. This only happens for memoized
    // components so it&#x27;s not that common.
    (enableLazyContextPropagation &amp;&amp;
      current !== null &amp;&amp;
      current.dependencies !== null &amp;&amp;
      checkIfContextChanged(current.dependencies));
      
  if (shouldUpdate) {
    // In order to support react-lifecycles-compat polyfilled components,
    // Unsafe lifecycles should not be invoked for components using the new APIs.
    if (
      !hasNewLifecycles &amp;&amp;
      (typeof instance.UNSAFE_componentWillUpdate === &#x27;function&#x27; ||
        typeof instance.componentWillUpdate === &#x27;function&#x27;)
    ) {
      if (typeof instance.componentWillUpdate === &#x27;function&#x27;) {
        instance.componentWillUpdate(newProps, newState, nextContext);
      }
      if (typeof instance.UNSAFE_componentWillUpdate === &#x27;function&#x27;) {
        instance.UNSAFE_componentWillUpdate(newProps, newState, nextContext);
      }
    }
    if (typeof instance.componentDidUpdate === &#x27;function&#x27;) {
      workInProgress.flags |= Update;
    }
    if (typeof instance.getSnapshotBeforeUpdate === &#x27;function&#x27;) {
      workInProgress.flags |= Snapshot;
    }
  } else {
    // If an update was already in progress, we should schedule an Update
    // effect even though we&#x27;re bailing out, so that cWU/cDU are called.
    if (typeof instance.componentDidUpdate === &#x27;function&#x27;) {
      if (
        unresolvedOldProps !== current.memoizedProps ||
        oldState !== current.memoizedState
      ) {
        workInProgress.flags |= Update;
      }
    }
    if (typeof instance.getSnapshotBeforeUpdate === &#x27;function&#x27;) {
      if (
        unresolvedOldProps !== current.memoizedProps ||
        oldState !== current.memoizedState
      ) {
        workInProgress.flags |= Snapshot;
      }
    }
    
    // If shouldComponentUpdate returned false, we should still update the
    // memoized props/state to indicate that this work can be reused.
    workInProgress.memoizedProps = newProps;
    workInProgress.memoizedState = newState;
  }
  
  // Update the existing instance&#x27;s state, props, and context pointers even
  // if shouldComponentUpdate returns false.
  instance.props = newProps;
  instance.state = newState;
  instance.context = nextContext;
  
  return shouldUpdate;
}</pre>
  <p id="SPMq">Выглядит довольно громоздко, но что поделать? Именно здесь и происходит вся магия жизненного цикла классовых компонентов. Давайте разбираться по порядку.</p>
  <pre id="mxuo" data-lang="flow">const unresolvedOldProps = workInProgress.memoizedProps;
const oldProps =
  workInProgress.type === workInProgress.elementType
    ? unresolvedOldProps
    : resolveDefaultProps(workInProgress.type, unresolvedOldProps);
instance.props = oldProps;
const unresolvedNewProps = workInProgress.pendingProps;</pre>
  <p id="Zlrj">Что такое мы уже видели раньше. Суть этого кода получить две константы: <code>unresolvedOldProps</code>, который сразу записывается в <code>this.props</code> экземпляра компонента и <code>unresolvedNewProps</code>. Эти константы далее будут участвовать в методах жизненного цикла и в самой мемоизации в том числе.</p>
  <pre id="F5LZ" data-lang="flow">const oldContext = instance.context;
const contextType = ctor.contextType;
let nextContext = emptyContextObject;
if (typeof contextType === &#x27;object&#x27; &amp;&amp; contextType !== null) {
  nextContext = readContext(contextType);
} else if (!disableLegacyContext) {
  const nextUnmaskedContext = getUnmaskedContext(workInProgress, ctor, true);
  nextContext = getMaskedContext(workInProgress, nextUnmaskedContext);
} </pre>
  <p id="Phvp">Следующий блок отвечает за резолв привязанного, компоненту, контекста. Подробно эту часть здесь разбираться мы не будем, но переменные <code>oldContext</code> и <code>nextContext</code> участвует в методах жизненного цикла на ряду с <code>unresolvedOldProps</code> и <code>unresolvedNewProps</code>, поэтому обозначить их стоит. </p>
  <pre id="Bmfr" data-lang="flow">const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
const hasNewLifecycles =
  typeof getDerivedStateFromProps === &#x27;function&#x27; ||
  typeof instance.getSnapshotBeforeUpdate === &#x27;function&#x27;;
  
// Note: During these life-cycles, instance.props/instance.state are what
// ever the previously attempted to render - not the &quot;current&quot;. However,
// during componentDidUpdate we pass the &quot;current&quot; props.

// In order to support react-lifecycles-compat polyfilled components,
// Unsafe lifecycles should not be invoked for components using the new APIs.
if (
  !hasNewLifecycles &amp;&amp;
  (typeof instance.UNSAFE_componentWillReceiveProps === &#x27;function&#x27; ||
    typeof instance.componentWillReceiveProps === &#x27;function&#x27;)
) {
  if (
    unresolvedOldProps !== unresolvedNewProps ||
    oldContext !== nextContext
  ) {
    callComponentWillReceiveProps(
      workInProgress,
      instance,
      newProps,
      nextContext,
    );
  }
}</pre>
  <p id="xwcP">Еще один блок, который я так же не могу оставить без внимания. Прежде чем приступить к следующим этапа жизненного цикла, необходимо сначала выполнить метод <a href="https://react.dev/reference/react/Component#componentwillreceiveprops" target="_blank">componentWillReceiveProps(nextProps)</a>, если он был определен в компоненте. А интересно здесь то, что метод будет вызван <strong>только</strong> в том случае, если компонент не использует новое API, т.е. если в нем нет методов <a href="https://react.dev/reference/react/Component#static-getderivedstatefromprops" target="_blank">getDerivedStateFromProps(props, state)</a> и <a href="https://react.dev/reference/react/Component#getsnapshotbeforeupdate" target="_blank">getSnapshotBeforeUpdate(prevProps, prevState)</a>.</p>
  <pre id="Hk67" data-lang="flow">const oldState = workInProgress.memoizedState;
let newState = (instance.state = oldState);
processUpdateQueue(workInProgress, newProps, instance, renderLanes);
newState = workInProgress.memoizedState;</pre>
  <p id="SOq3">И, наконец четвертый блок. Переменные <code>oldState</code> и <code>newState</code>, которые так же участвуют в жизненном цикле компонента. Здесь остановимся чуть поподробнее. Если с <code>oldState</code> все очевидно, он берется из <code>Fiber.memoizedState</code>, то вот <code>newState</code> требует пояснений.</p>
  <p id="yK2Z">По умолчанию, <code>newState</code> присваивается значение текущего <code>oldState</code>. Далее исполняется цикл <code>updateQueue</code>, инициированный вызовом <code>processUpdateQueue</code>. Приводить код этой функции здесь не буду, так как он тоже довольно большой и вариативный и только отвлечет нас от темы статьи. Главно, что нам нужно тут понять, что эта функция, в конечно итоге сформирует новый State и запишет его в <code>workInProgress.memoizedState</code>.</p>
  <p id="Rr2i"><a href="https://github.com/facebook/react/blob/v18.3.1/packages/react-reconciler/src/ReactFiberClassUpdateQueue.new.js#L458" target="_blank">/packages/react-reconciler/src/ReactFiberClassUpdateQueue.new.js#L458</a></p>
  <pre id="fijR" data-lang="flow">export function processUpdateQueue&lt;State&gt;(
  ...
  // These values may change as we process the queue.
  if (firstBaseUpdate !== null) {
    ...
    workInProgress.memoizedState = newState;
  }
}</pre>
  <p id="asis"> После чего мы и получаем свою переменную <code>newState</code> нашей update-функции.</p>
  <p id="zl5C">На это сбор основной информации о компоненте заканчивается и начинается, собственно, сам процесс обновления.</p>
  <pre id="HEqM" data-lang="flow">if (
  unresolvedOldProps === unresolvedNewProps &amp;&amp;
  oldState === newState &amp;&amp;
  !hasContextChanged() &amp;&amp;
  !checkHasForceUpdateAfterProcessing() &amp;&amp;
  !(
    enableLazyContextPropagation &amp;&amp;
    current !== null &amp;&amp;
    current.dependencies !== null &amp;&amp;
    checkIfContextChanged(current.dependencies)
  )
) {
  // If an update was already in progress, we should schedule an Update
  // effect even though we&#x27;re bailing out, so that cWU/cDU are called.
  if (typeof instance.componentDidUpdate === &#x27;function&#x27;) {
    if (
      unresolvedOldProps !== current.memoizedProps ||
      oldState !== current.memoizedState
    ) {
      workInProgress.flags |= Update;
    }
  }
  if (typeof instance.getSnapshotBeforeUpdate === &#x27;function&#x27;) {
    if (
      unresolvedOldProps !== current.memoizedProps ||
      oldState !== current.memoizedState
    ) {
      workInProgress.flags |= Snapshot;
    }
  }
  return false;
}</pre>
  <p id="2WmS">Нет, это пока еще не обновление, как могло показаться на первый взгляд. Прежде чем к нему приступить, реконсиллер, в целях оптимизации исключает лишние обработки. В данном случае, если ссылки на свойства компонента и на его стейт не изменились и не был вызван <a href="https://react.dev/reference/react/Component#forceupdate" target="_blank">forceUpdate()</a>, смысла в дальнейшем процессе просто нет и вся функция завершается.</p>
  <pre id="jDOX" data-lang="flow">if (typeof getDerivedStateFromProps === &#x27;function&#x27;) {
  applyDerivedStateFromProps(
    workInProgress,
    ctor,
    getDerivedStateFromProps,
    newProps,
  );
  newState = workInProgress.memoizedState;
}</pre>
  <p id="OSz1">Вот теперь, если всё же что-то в состоянии компонента поменялось, можно переходить к его обновлению. И первым, что необходимо сделать, это вызвать <a href="https://react.dev/reference/react/Component#static-getderivedstatefromprops" target="_blank">getDerivedStateFromProps(props, state)</a>, так как этот меняет меняет State компонента и, соответственно, переменную newState в update-функции.</p>
  <pre id="nxYb" data-lang="flow">const shouldUpdate =
  checkHasForceUpdateAfterProcessing() ||
  checkShouldComponentUpdate(
    workInProgress,
    ctor,
    oldProps,
    newProps,
    oldState,
    newState,
    nextContext,
  ) ||
  // TODO: In some cases, we&#x27;ll end up checking if context has changed twice,
  // both before and after &#x60;shouldComponentUpdate&#x60; has been called. Not ideal,
  // but I&#x27;m loath to refactor this function. This only happens for memoized
  // components so it&#x27;s not that common.
  (enableLazyContextPropagation &amp;&amp;
    current !== null &amp;&amp;
    current.dependencies !== null &amp;&amp;
    checkIfContextChanged(current.dependencies));</pre>
  <p id="84Lu">Наконец, все константы и переменные собраны и теперь можно принять решение о дальнейшем продвижении по жизненному циклу, точнее, будет ли перерисован компонент. А перерисован компонент должен быть только в двух случаях, если значения свойств, стейта или контекст отличают от предыдущих или если был вызван <a href="https://react.dev/reference/react/Component#forceupdate" target="_blank">forceUpdate()</a>. Последнее определяется функцией <code>checkHasForceUpdateAfterProcessing()</code>, а вот сравнение значений происходит внутри <code>checkShouldComponentUpdate()</code>. Остановимся на этой функции поподробнее.</p>
  <p id="t5yy"><a href="https://github.com/facebook/react/blob/v18.3.1/packages/react-reconciler/src/ReactFiberClassComponent.new.js#L305" target="_blank">/packages/react-reconciler/src/ReactFiberClassComponent.new.js#L305</a></p>
  <pre id="XkoJ" data-lang="flow">function checkShouldComponentUpdate(
  workInProgress,
  ctor,
  oldProps,
  newProps,
  oldState,
  newState,
  nextContext,
) {
  const instance = workInProgress.stateNode;
  if (typeof instance.shouldComponentUpdate === &#x27;function&#x27;) {
    let shouldUpdate = instance.shouldComponentUpdate(
      newProps,
      newState,
      nextContext,
    );
    ...
    return shouldUpdate;
  }
  
  if (ctor.prototype &amp;&amp; ctor.prototype.isPureReactComponent) {
    return (
      !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
    );
  }
  
  return true;
}</pre>
  <p id="9nl5">Если убрать из функции служебный dev-mode код, она на удивление довольно простая. Функция предполагает всего три сценария:</p>
  <ol id="8qZ7">
    <li id="Q6YX">Компонент имеет метод <a href="https://react.dev/reference/react/Component#shouldcomponentupdate" target="_blank">shouldComponentUpdate()</a>. В этом случае, решение о том, должен ли быть перерисован компонент возлагается полностью на разработчика.</li>
    <li id="nQ5s">В случае <code>PureComponent</code> проводится shallow сравнение свойств и стейта (об это подробнее поговорим чуть ниже).</li>
    <li id="PbUl">Во всех остальных случаях компонент будет перерисован обязательно.</li>
  </ol>
  <pre id="8jzN" data-lang="flow">if (shouldUpdate) {
  // In order to support react-lifecycles-compat polyfilled components,
  // Unsafe lifecycles should not be invoked for components using the new APIs.
  if (
    !hasNewLifecycles &amp;&amp;
    (typeof instance.UNSAFE_componentWillUpdate === &#x27;function&#x27; ||
      typeof instance.componentWillUpdate === &#x27;function&#x27;)
  ) {
    if (typeof instance.componentWillUpdate === &#x27;function&#x27;) {
      instance.componentWillUpdate(newProps, newState, nextContext);
    }
    if (typeof instance.UNSAFE_componentWillUpdate === &#x27;function&#x27;) {
      instance.UNSAFE_componentWillUpdate(newProps, newState, nextContext);
    }
  }
  if (typeof instance.componentDidUpdate === &#x27;function&#x27;) {
    workInProgress.flags |= Update;
  }
  if (typeof instance.getSnapshotBeforeUpdate === &#x27;function&#x27;) {
    workInProgress.flags |= Snapshot;
  }
} else {
  // If an update was already in progress, we should schedule an Update
  // effect even though we&#x27;re bailing out, so that cWU/cDU are called.
  if (typeof instance.componentDidUpdate === &#x27;function&#x27;) {
    if (
      unresolvedOldProps !== current.memoizedProps ||
      oldState !== current.memoizedState
    ) {
      workInProgress.flags |= Update;
    }
  }
  if (typeof instance.getSnapshotBeforeUpdate === &#x27;function&#x27;) {
    if (
      unresolvedOldProps !== current.memoizedProps ||
      oldState !== current.memoizedState
    ) {
      workInProgress.flags |= Snapshot;
    }
  }
  
  // If shouldComponentUpdate returned false, we should still update the
  // memoized props/state to indicate that this work can be reused.
  workInProgress.memoizedProps = newProps;
  workInProgress.memoizedState = newState;
}</pre>
  <p id="H6pm">Последним, ну почти последним, этапом является выполнение оставшихся методов жизненного цикла. В случае, если компонент должен быть перерисован, будут выполнены методы:</p>
  <ol id="dg91">
    <li id="KL2a"><code>componentWillUpdate</code> (только если не используется новое API)</li>
    <li id="6kB0"><code>UNSAFE_componentWillUpdate</code> (только если не используется новое API)</li>
    <li id="sdwC"><code>componentDidUpdate</code></li>
    <li id="xNw8"><code>getSnapshotBeforeUpdate</code></li>
  </ol>
  <p id="8D4c">В противном же случае эти методы вызваны не будут, будут проставлены только нужные служебные флаги, а свойства и стейт будут записаны в memoizedProps и memoizedState соответсвенно.</p>
  <pre id="ozYd" data-lang="flow">// Update the existing instance&#x27;s state, props, and context pointers even
// if shouldComponentUpdate returns false.
instance.props = newProps;
instance.state = newState;
instance.context = nextContext;

return shouldUpdate;</pre>
  <p id="dggw">И, наконец, последний этап. Новые свойства, стейт и значение контекста присваиваются экземпляру класса, после чего функция завершается.</p>
  <h2 id="hmSV">Поверхностное (shallow) сравнение</h2>
  <p id="yTYx">Это понятие уже прозвучало чуть выше. Давайте разберем его детальнее. Мы говорили о том, что такое сравнение происходит в случае реализации PureComponent. На всякий случай вспомним, что такое PureComponent. Воспользуемся для этого примером выше и слегка его изменим.</p>
  <pre id="MxBc" data-lang="typescript">class MyComponent extends React.PureComponent&lt;{ color?: string }&gt; {
  static defaultProps = {
    color: &#x27;blue&#x27;,
  };
  
  render() {
    return (
      &lt;div&gt;
        {this.props.color}
      &lt;/div&gt;
    );
  }
}</pre>
  <p id="tMS6">Как вы видите, разница только в том, от какого родителя мы наследуем наш классовый компонент (в исходном варианты мы наследовали от <code>React.Component</code>). Именно в таком компоненте и будет применено shallow сравнение.</p>
  <p id="YnNX">Так что же такое shallow сравнение? Пожалуй, лучшим ответом на этот вопрос будет код самой функции <code>shallowEqual</code>, которую мы видели ранее.</p>
  <p id="D7gU"><a href="https://github.com/facebook/react/blob/v18.3.1/packages/shared/shallowEqual.js#L13" target="_blank">/packages/shared/shallowEqual.js#L13</a></p>
  <pre id="foZr" data-lang="flow">/**
 * Performs equality by iterating through keys on an object and returning false
 * when any key has values which are not strictly equal between the arguments.
 * Returns true when the values of all keys are strictly equal.
 */
function shallowEqual(objA: mixed, objB: mixed): boolean {
  if (is(objA, objB)) {
    return true;
  }
  
  if (
    typeof objA !== &#x27;object&#x27; ||
    objA === null ||
    typeof objB !== &#x27;object&#x27; ||
    objB === null
  ) {
    return false;
  }
  
  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);
  
  if (keysA.length !== keysB.length) {
    return false;
  }
  
  // Test for A&#x27;s keys different from B.
  for (let i = 0; i &lt; keysA.length; i++) {
    const currentKey = keysA[i];
    if (
      !hasOwnProperty.call(objB, currentKey) ||
      !is(objA[currentKey], objB[currentKey])
    ) {
      return false;
    }
  }
  
  return true;
}</pre>
  <p id="nR8I">Эта утилита условно состоит из двух частей:</p>
  <ol id="6DRv">
    <li id="KI4x">Сравнение переменных <code>objA</code> и <code>objB</code>.</li>
    <li id="ICz1">Если переменные равны или равны ссылки на объекты, которые они представляют, происходит сравнение каждого из свойств этих объектов.</li>
  </ol>
  <p id="rvos"> Само же сравнение осуществляется другой утилитой <code>is()</code>.</p>
  <p id="GTZC"><a href="https://github.com/facebook/react/blob/v18.3.1/packages/shared/objectIs.js#L10" target="_blank">/packages/shared/objectIs.js#L10</a></p>
  <pre id="ZX8m" data-lang="flow">/**
 * inlined Object.is polyfill to avoid requiring consumers ship their own
 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
 */
function is(x: any, y: any) {
  return (
    (x === y &amp;&amp; (x !== 0 || 1 / x === 1 / y)) || (x !== x &amp;&amp; y !== y) // eslint-disable-line no-self-compare
  );
}

const objectIs: (x: any, y: any) =&gt; boolean =
  typeof Object.is === &#x27;function&#x27; ? Object.is : is;</pre>
  <p id="njFl">Которая является ничем иным, как функцией Web API - <a href="https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Object/is" target="_blank">Object.is()</a>. В случае, если браузер не поддерживыет данный метод Web API, используется встроенный полифил.</p>
  <p id="m9qD">Если коротко, два значения счиются одинаковыми, если:</p>
  <ul id="Kza5">
    <li id="wIz8">оба равны <code>undefined</code></li>
    <li id="SrCR">оба равны <code>null</code></li>
    <li id="lj5u">оба равны <code>true</code>, либо оба равны <code>false</code></li>
    <li id="Y2Xi">оба являются строками с одинаковой длиной и одинаковыми символами</li>
    <li id="f2gF">оба являются одним и тем же объектом</li>
    <li id="sk1T">оба являются <code>BigInt</code> с одинаковым числовым значением</li>
    <li id="ElNy">оба являются <code>Symbol</code>, которые ссылаются на на один и тот же символ</li>
    <li id="oXSO">оба являются числами и</li>
  </ul>
  <ul id="ltwr">
    <ul id="3Bwd">
      <li id="cShq">оба равны <code>+0</code></li>
      <li id="EHMF">оба равны <code>-0</code></li>
      <li id="Tgek">оба равны <code>NaN</code></li>
      <li id="NuGA">либо оба не равны нулю или <code>NaN</code> и оба имеют одинаковое значение</li>
    </ul>
  </ul>
  <h2 id="GMPM">FunctionComponent</h2>
  <p id="SAJh">С классовыми компонентами разобрались, давайте посмотрим теперь в функциональные.</p>
  <pre id="oaHL" data-lang="flow">const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
  workInProgress.elementType === Component
    ? unresolvedProps
    : resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
  current,
  workInProgress,
  Component,
  resolvedProps,
  renderLanes,
);</pre>
  <p id="kKTg">В самом начале фазы begin инициация обновления функционального компонента ничем не отличается от обновления классового. За исключением самой update-функции.</p>
  <p id="5Hwf"><a href="https://github.com/facebook/react/blob/v18.3.1/packages/react-reconciler/src/ReactFiberBeginWork.new.js#L965" target="_blank">/packages/react-reconciler/src/ReactFiberBeginWork.new.js#L965</a></p>
  <pre id="xlY1" data-lang="flow">function updateFunctionComponent(
  current,
  workInProgress,
  Component,
  nextProps: any,
  renderLanes,
) {
  ...
  
  nextChildren = renderWithHooks(
    current,
    workInProgress,
    Component,
    nextProps,
    context,
    renderLanes,
  );
  hasId = checkDidRenderIdHook();

  ...
    
  if (current !== null &amp;&amp; !didReceiveUpdate) {
    bailoutHooks(current, workInProgress, renderLanes);
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  }
  
  if (getIsHydrating() &amp;&amp; hasId) {
    pushMaterializedTreeId(workInProgress);
  }
  
  ...
  
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}</pre>
  <p id="Zu4v">А именно, ключевым здесь является запуск отдельного flow под названием <code>renderWithHooks</code>, характерного для функциональных компонентов. Давайте посмотрим, что там происходит.</p>
  <p id="gVrr"><a href="https://github.com/facebook/react/blob/v18.3.1/packages/react-reconciler/src/ReactFiberHooks.new.js#L374" target="_blank">/packages/react-reconciler/src/ReactFiberHooks.new.js#L374</a></p>
  <pre id="KSmi" data-lang="flow">export function renderWithHooks&lt;Props, SecondArg&gt;(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) =&gt; any,
  props: Props,
  secondArg: SecondArg,
  nextRenderLanes: Lanes,
): any {
  renderLanes = nextRenderLanes;
  currentlyRenderingFiber = workInProgress;
  
  ...
  
  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;
  workInProgress.lanes = NoLanes;
  
  // The following should have already been reset
  // currentHook = null;
  // workInProgressHook = null;
  
  // didScheduleRenderPhaseUpdate = false;
  // localIdCounter = 0;
  
  // TODO Warn if no hooks are used at all during mount, then some are used during update.
  // Currently we will identify the update render as a mount because memoizedState === null.
  // This is tricky because it&#x27;s valid for certain types of components (e.g. React.lazy)
  
  // Using memoizedState to differentiate between mount/update only works if at least one stateful hook is used.
  // Non-stateful hooks (e.g. context) don&#x27;t get added to memoizedState,
  // so memoizedState would be null during updates and mounts.
  ReactCurrentDispatcher.current =
    current === null || current.memoizedState === null
      ? HooksDispatcherOnMount
      : HooksDispatcherOnUpdate;
      
  let children = Component(props, secondArg);
  
  // Check if there was a render phase update
  if (didScheduleRenderPhaseUpdateDuringThisPass) {
    // Keep rendering in a loop for as long as render phase updates continue to
    // be scheduled. Use a counter to prevent infinite loops.
    let numberOfReRenders: number = 0;
    do {
      didScheduleRenderPhaseUpdateDuringThisPass = false;
      localIdCounter = 0;
      
      if (numberOfReRenders &gt;= RE_RENDER_LIMIT) {
        throw new Error(
          &#x27;Too many re-renders. React limits the number of renders to prevent &#x27; +
            &#x27;an infinite loop.&#x27;,
        );
      }
      
      numberOfReRenders += 1;
      
      // Start over from the beginning of the list
      currentHook = null;
      workInProgressHook = null;
      
      workInProgress.updateQueue = null;
      
      ReactCurrentDispatcher.current = __DEV__
        ? HooksDispatcherOnRerenderInDEV
        : HooksDispatcherOnRerender;
        
      children = Component(props, secondArg);
    } while (didScheduleRenderPhaseUpdateDuringThisPass);
  }
  
  // We can assume the previous dispatcher is always this one, since we set it
  // at the beginning of the render phase and there&#x27;s no re-entrance.
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;
  
  // This check uses currentHook so that it works the same in DEV and prod bundles.
  // hookTypesDev could catch more cases (e.g. context) but only in DEV bundles.
  const didRenderTooFewHooks =
    currentHook !== null &amp;&amp; currentHook.next !== null;
    
  renderLanes = NoLanes;
  currentlyRenderingFiber = (null: any);
  
  currentHook = null;
  workInProgressHook = null;
  
  didScheduleRenderPhaseUpdate = false;
  // This is reset by checkDidRenderIdHook
  // localIdCounter = 0;
  
  if (didRenderTooFewHooks) {
    throw new Error(
      &#x27;Rendered fewer hooks than expected. This may be caused by an accidental &#x27; +
        &#x27;early return statement.&#x27;,
    );
  }
  
  if (enableLazyContextPropagation) {
    if (current !== null) {
      if (!checkIfWorkInProgressReceivedUpdate()) {
        // If there were no changes to props or state, we need to check if there
        // was a context change. We didn&#x27;t already do this because there&#x27;s no
        // 1:1 correspondence between dependencies and hooks. Although, because
        // there almost always is in the common case (&#x60;readContext&#x60; is an
        // internal API), we could compare in there. OTOH, we only hit this case
        // if everything else bails out, so on the whole it might be better to
        // keep the comparison out of the common path.
        const currentDependencies = current.dependencies;
        if (
          currentDependencies !== null &amp;&amp;
          checkIfContextChanged(currentDependencies)
        ) {
          markWorkInProgressReceivedUpdate();
        }
      }
    }
  }
  return children;
}</pre>
  <p id="xIg2">Функция может показаться большой и сложной, но по сути, она делает всего две вещи. Первое, определяет диспатчер хуков, который будет отвечать за дальнейшее выполнение самих этих хуков. Формально в React существует всего 4 таких диспатчера:</p>
  <ul id="LaKt">
    <li id="KATx"><code>ContextOnlyDispatcher</code></li>
    <li id="nSzt"><code>HooksDispatcherOnMount</code></li>
    <li id="5mIY"><code>HooksDispatcherOnUpdate</code></li>
    <li id="QGEw"><code>HooksDispatcherOnRender</code></li>
  </ul>
  <p id="fW4U">На самом же деле, <code>ContextOnlyDispatcher</code> не реализует никаких, он является своего рода диспатчером по умолчанию и выставляется до тех пор, пока движок не определил более релевантный диспатчер текущему компоненту.</p>
  <p id="WyWX">Из оставшихся трех, <code>HooksDispatcherOnUpdate</code> и <code>HooksDispatcherOnRender</code> на практике ничем не отличаются и оба ведут на одни и те же реализации update-хуков (<code>updateMemo</code>, <code>updateCallback</code> и т.д.). Выделение двух разных диспатчеров здесь носит исключительно логический характер, на случай, если разработчикам React когда-нибудь понадобится сделать разные версии хуков под разные фазы. К в случае в <code>HooksDispatcherOnMount</code> который ведет к отедельным реализациям mount-хуков (<code>mountMemo</code>, <code>mountCallback</code>, и т.д.).</p>
  <p id="L4m6">Про хуки и их провайдеры поговорим чуть ниже. Пока зафиксируем только тот факт, что у каждого хука имеется две версии: <strong>mount</strong> и <strong>update</strong>. И на текущем этапе важно определить, какую версию хуков движок будет исполнять. Делается следующим образом.</p>
  <pre id="tEnv" data-lang="flow">ReactCurrentDispatcher.current =
    current === null || current.memoizedState === null
      ? HooksDispatcherOnMount
      : HooksDispatcherOnUpdate;</pre>
  <p id="Mu73">Где <code>current</code>  - ссылка на <code>Fiber.alternate</code>. Если она равно <code>null</code>, значит компонент еще ни раз не был создан. Аналогично, если <code>Fiber.alternate.memoizedState</code> пустой, то хуки еще в этом компоненте еще ни разу не выполнялись. В обоих случаях будет применен диспатчер <code>HooksDispatcherOnMount</code>. В противном же случае - <code>HooksDispatcherOnUpdate</code>.</p>
  <p id="SiTi">Второе, что делает функция - создает компонент посредством <code>let children = Component(props, secondArg)</code> и повторяет этот процесс в цикле, до тех пор, пока в текущей фазе не будут выполнены все запланированные обновления. И да, именно здесь и выбрасывается то самое исключение <code>Too many re-renders. React limits the number of renders to prevent an infinite loop.</code>, если количество обновлений превысит <strong>25</strong> итераций.</p>
  <h2 id="rkIk">HooksDispatcherOnMount</h2>
  <p id="A0yM">Код диспатчера выглядит следующим образом</p>
  <p id="iTsd"><a href="https://github.com/facebook/react/blob/v18.3.1/packages/react-reconciler/src/ReactFiberHooks.new.js#L2427" target="_blank">/packages/react-reconciler/src/ReactFiberHooks.new.js#L2427</a></p>
  <pre id="dTfQ" data-lang="flow">const HooksDispatcherOnMount: Dispatcher = {
  readContext,
  
  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useInsertionEffect: mountInsertionEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  useDebugValue: mountDebugValue,
  useDeferredValue: mountDeferredValue,
  useTransition: mountTransition,
  useMutableSource: mountMutableSource,
  useSyncExternalStore: mountSyncExternalStore,
  useId: mountId,
  
  unstable_isNewReconciler: enableNewReconciler,
};
if (enableCache) {
  (HooksDispatcherOnMount: Dispatcher).getCacheSignal = getCacheSignal;
  (HooksDispatcherOnMount: Dispatcher).getCacheForType = getCacheForType;
  (HooksDispatcherOnMount: Dispatcher).useCacheRefresh = mountRefresh;
}</pre>
  <p id="2Aoq">Все mount-функции хуков мы рассматривать не будем, важнее понять суть этих функций. Её и посмотрим на примере <code>mountMemo</code>.</p>
  <p id="udI4"><a href="https://github.com/facebook/react/blob/v18.3.1/packages/react-reconciler/src/ReactFiberHooks.new.js#L1899" target="_blank">/packages/react-reconciler/src/ReactFiberHooks.new.js#L1899</a></p>
  <pre id="QSHg" data-lang="flow">function mountMemo&lt;T&gt;(
  nextCreate: () =&gt; T,
  deps: Array&lt;mixed&gt; | void | null,
): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}</pre>
  <p id="P9WS">Как можно видеть, код фука до неприличия прост. Сначала определяется ссылка на Fiber. Затем производится вычисление значения, это происходит в пользовательском колбэке <code>nextCreate</code>. Далее, вычисленное значение и массив зависимостей сохраняются в <code>Fiber.memoizedState</code>. Вот она, мемоизация!</p>
  <h2 id="C94Q">HooksDispatcherOnUpdate</h2>
  <p id="KBTw">Теперь заглянем в диспатчер <code>HooksDispatcherOnUpdate</code>.</p>
  <p id="yGHP"><a href="https://github.com/facebook/react/blob/v18.3.1/packages/react-reconciler/src/ReactFiberHooks.new.js#L2454" target="_blank">/packages/react-reconciler/src/ReactFiberHooks.new.js#L2454</a></p>
  <pre id="1P9O" data-lang="flow">const HooksDispatcherOnUpdate: Dispatcher = {
  readContext,
  
  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useInsertionEffect: updateInsertionEffect,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  useDebugValue: updateDebugValue,
  useDeferredValue: updateDeferredValue,
  useTransition: updateTransition,
  useMutableSource: updateMutableSource,
  useSyncExternalStore: updateSyncExternalStore,
  useId: updateId,
  
  unstable_isNewReconciler: enableNewReconciler,
};
if (enableCache) {
  (HooksDispatcherOnUpdate: Dispatcher).getCacheSignal = getCacheSignal;
  (HooksDispatcherOnUpdate: Dispatcher).getCacheForType = getCacheForType;
  (HooksDispatcherOnUpdate: Dispatcher).useCacheRefresh = updateRefresh;
}</pre>
  <p id="zHOO">Как я и говорил ранее, этот диспатчер отличается от предыдущего тем, что ведет на update-версии тех же самы хуков. И раз уж в предыдущем примере мы смотрели в <code>mountMemo</code>, в этот раз давайте заглянем в <code>updateMemo</code>.</p>
  <p id="PmOH"><a href="https://github.com/facebook/react/blob/v18.3.1/packages/react-reconciler/src/ReactFiberHooks.new.js#L1910" target="_blank">/packages/react-reconciler/src/ReactFiberHooks.new.js#L1910</a></p>
  <pre id="DOOG" data-lang="flow">function updateMemo&lt;T&gt;(
  nextCreate: () =&gt; T,
  deps: Array&lt;mixed&gt; | void | null,
): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    // Assume these are defined. If they&#x27;re not, areHookInputsEqual will warn.
    if (nextDeps !== null) {
      const prevDeps: Array&lt;mixed&gt; | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}</pre>
  <p id="OyyU">А вот тут уже имеет место эффект памяти, который инициировали на этапе монтирования. Теперь, прежде чем приступить к вычислениям, хук возьмет предыдущие зависимости из <code>Fiber.memoizedState[1]</code> и сравнит их с текущими посредством функции <code>areHookInputsEqual(nextDeps, prevDeps)</code>.</p>
  <p id="ywXz"><a href="https://github.com/facebook/react/blob/v18.3.1/packages/react-reconciler/src/ReactFiberHooks.new.js#L327" target="_blank">/packages/react-reconciler/src/ReactFiberHooks.new.js#L327</a></p>
  <pre id="eXX0" data-lang="flow">function areHookInputsEqual(
  nextDeps: Array&lt;mixed&gt;,
  prevDeps: Array&lt;mixed&gt; | null,
) {
  ...
  
  if (prevDeps === null) {
    if (__DEV__) {
      console.error(
        &#x27;%s received a final argument during this render, but not during &#x27; +
          &#x27;the previous render. Even though the final argument is optional, &#x27; +
          &#x27;its type cannot change between renders.&#x27;,
        currentHookNameInDev,
      );
    }
    return false;
  }
  
  ...
  
  for (let i = 0; i &lt; prevDeps.length &amp;&amp; i &lt; nextDeps.length; i++) {
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}</pre>
  <p id="iAIn">Которая в свою очередь, проводит поверхностное сравнение зависимостей с помощью всё того же <a href="https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Object/is" target="_blank">Object.is</a>.</p>
  <h2 id="yizd">MemoComponent</h2>
  <p id="iGSr">Настало время поговорить о третьем интересующим нас типе узла - <code>MemoComponent</code>. Такой узел можно создать с помощью <a href="https://react.dev/reference/react/memo" target="_blank">React.memo</a>.</p>
  <pre id="YGuM" data-lang="javascript">const MyComponent = React.memo&lt;{ color?: string }&gt;(({ color }) =&gt; {
  return &lt;div&gt;{color}&lt;/div&gt;;
});</pre>
  <p id="1uwH">Такой компонент будет перерисовываться только в том случае, если изменились переданные ему свойства. Выглядит это следующим образом:</p>
  <pre id="FEah" data-lang="flow">const type = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
// Resolve outer props first, then resolve inner props.
let resolvedProps = resolveDefaultProps(type, unresolvedProps);

resolvedProps = resolveDefaultProps(type.type, resolvedProps);
return updateMemoComponent(
  current,
  workInProgress,
  type,
  resolvedProps,
  renderLanes,
);</pre>
  <p id="jRmL">Update-функция для таких компонентов называется <code>updateMemoComponent</code>. Функция довольно длинная, приведу только ту её часть, которая нам нужна в рамках этой статьи.</p>
  <p id="qNxT"><a href="https://github.com/facebook/react/blob/v18.3.1/packages/react-reconciler/src/ReactFiberBeginWork.new.js#L452" target="_blank">/packages/react-reconciler/src/ReactFiberBeginWork.new.js#L452</a></p>
  <pre id="1ugI" data-lang="flow">function updateMemoComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  renderLanes: Lanes,
): null | Fiber {
  ...
  
  const currentChild = ((current.child: any): Fiber); // This is always exactly one child
  const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
    current,
    renderLanes,
  );
  if (!hasScheduledUpdateOrContext) {
    // This will be the props with resolved defaultProps,
    // unlike current.memoizedProps which will be the unresolved ones.
    const prevProps = currentChild.memoizedProps;
    // Default to shallow comparison
    let compare = Component.compare;
    compare = compare !== null ? compare : shallowEqual;
    if (compare(prevProps, nextProps) &amp;&amp; current.ref === workInProgress.ref) {
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    }
  }
  // React DevTools reads this flag.
  workInProgress.flags |= PerformedWork;
  const newChild = createWorkInProgress(currentChild, nextProps);
  newChild.ref = workInProgress.ref;
  newChild.return = workInProgress;
  workInProgress.child = newChild;
  return newChild;
}</pre>
  <p id="pbuH">Собственно, вся суть этого кода сводится к вызову функции сравнения <code>compare(prevProps, nextProps)</code>, где <code>compare</code> может быть как пользовательской функцией (второй аргумент <a href="https://react.dev/reference/react/memo" target="_blank">React.memo</a>) или <code>shallowEqual</code> по умолчанию, её мы уже видели выше.</p>
  <h2 id="7J1z">Заключение</h2>
  <p id="x5ok">В этой статье мы заглянули &quot;под капот&quot; движка React и посмотрели на механизмы монтирования и обновления узлов. Одним из важнейших механизмов React, участвующим в этих процессах является меомизация. Её мы разобрали на примерах трех типов Fiber: <code>ClassComponent</code>, <code>FunctionComponent</code> и <code>MemoComponent</code>.</p>
  <p id="DWgi">И покуда мы теперь лучше понимаем эти процессы, самое время сделать некоторые выводы.</p>
  <ul id="MYBn">
    <li id="xJb5">Будь то класс, функция или зависимости хука, React в качестве фукнции-сравнения использует один и тот же подход - метод <a href="https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Object/is" target="_blank">Object.is()</a> Web API или его полифил.</li>
    <li id="IfP5">Каждый хук имеет две версии реализации, <strong>mount</strong> и <strong>update</strong>. При чем update, как правило, более сложная функция, так как ей требуется произвести сравнение зависимостей. Разработчик не можем повлиять на то, какую именно версию хука будет использовать движок. Mount-версия сработает только при первом монтировании компонента.</li>
    <li id="ret8">Так как React, при обработке хука сравнивает каждую зависимость отдельно, не стоит добавлять лишние зависимости. Кроме того, сами зависимости хранятся в памяти, большое их количество может сказать потреблении ресурсов. Другими словами,</li>
  </ul>
  <pre id="nqHn" data-lang="typescript">const value = useMemo(() =&gt; {
  return a + b
}, [a, b]);</pre>
  <p id="9NgY">этот код будет чуть более производителен, чем следующий</p>
  <pre id="Hail" data-lang="typescript">const value = useMemo(() =&gt; {
  return a + b
}, [a, b, c]);</pre>
  <p id="zDOb">Аналогично, </p>
  <pre id="X43m" data-lang="typescript">const SOME_CONST = &quot;some value&quot;;

const MyComponent = ({ a, b }) =&gt; {
  // этот хук более производителен
  const value1 = useMemo(() =&gt; {
    return a + SOME_CONST
  }, [a]);

  // чем этот
  const value2 = useMemo(() =&gt; {
    return b + SOME_CONST
  }, [b, SOME_CONST]);  

  return null;
}</pre>
  <ul id="aWx0">
    <li id="WvnY">По той же причине стоит адекватно оценивать необходимость в мемоизации в целом, например,</li>
  </ul>
  <pre id="DvyM" data-lang="typescript">const isEqual = useMemo(() =&gt; {
  return a === b
}, [a, b]);</pre>
  <p id="edgm">Не даст никакого прироста в производительности. Даже наоборот, движок будет вынужден каждый раз сравнивать <code>prevDeps.a === currDeps.a</code> и <code>prevDeps.b === currDeps.b</code>. К тому же, в коде появляется дополнительная функция, которая будет потреблять свой ресурс.</p>
  <ul id="J63f">
    <li id="RODn">Предыдущий тезис касается и <a href="https://react.dev/reference/react/memo" target="_blank">React.memo</a>. Разработчик должен понимать, будет ли эффект от мемоизации выше накладных расходов на его её поддержание.</li>
  </ul>
  <p id="Krk6"></p>
  <hr />
  <p id="5jCG"></p>
  <p id="r6RJ"><strong>Мои телеграмм-каналы:</strong></p>
  <section style="background-color:hsl(hsl(55,  86%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="YnAq">EN - <a href="https://t.me/frontend_almanac" target="_blank">https://t.me/frontend_almanac</a><br />RU - <a href="https://t.me/frontend_almanac_ru" target="_blank">https://t.me/frontend_almanac_ru</a></p>
  </section>
  <p id="Ri5D"><em>English version: <a href="https://blog.frontend-almanac.com/react-memoization" target="_blank">https://blog.frontend-almanac.com/react-memoization</a></em></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.frontend-almanac.ru/js-optimisation-ic</guid><link>https://blog.frontend-almanac.ru/js-optimisation-ic?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=frontend_almanac_ru</link><comments>https://blog.frontend-almanac.ru/js-optimisation-ic?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=frontend_almanac_ru#comments</comments><dc:creator>frontend_almanac_ru</dc:creator><title>Оптимизация JavaScript. Inline Caches</title><pubDate>Tue, 23 Apr 2024 17:49:39 GMT</pubDate><media:content medium="image" url="https://img2.teletype.in/files/56/9f/569f1d89-14c1-4ac5-9af6-bd03b1fe9b35.png"></media:content><description><![CDATA[<img src="https://img3.teletype.in/files/2d/a4/2da4e55d-16a5-429a-8cff-57f83fd6d27b.png"></img>Думаю, ни для кого не секрет, что все популярные JavaScript движки имеют схожий пайплайн выполнения кода. Выглядит он примерно следующим образом. Интерпретатор быстро компилирует JS-код в байткод &quot;на лету&quot;. Полученный байткод начинает исполняться и параллельно обрабатывается оптимизатором. Оптимизатору требуется время на такую обработку, но в итоге может получиться высоко-оптимизированный код, который будет работать в разы быстрее. В движке V8 роль интерпретатора выполняет Ignition, а оптимизатором является Turbofan. В движке Chakra, который используется в Edge, вместо одного оптимизатора целых два, SimpleJIT и FullJIT. А в JSC (Safari), аж три Baseline, DFG и FTL. Конкретная реализация в разных движка может отличаться, но ...]]></description><content:encoded><![CDATA[
  <p id="l91T">Думаю, ни для кого не секрет, что все популярные JavaScript движки имеют схожий пайплайн выполнения кода. Выглядит он примерно следующим образом. Интерпретатор быстро компилирует JS-код в байткод &quot;на лету&quot;. Полученный байткод начинает исполняться и параллельно обрабатывается оптимизатором. Оптимизатору требуется время на такую обработку, но в итоге может получиться высоко-оптимизированный код, который будет работать в разы быстрее. В движке V8 роль интерпретатора выполняет Ignition, а оптимизатором является Turbofan. В движке Chakra, который используется в Edge, вместо одного оптимизатора целых два, SimpleJIT и FullJIT. А в JSC (Safari), аж три Baseline, DFG и FTL. Конкретная реализация в разных движка может отличаться, но принципиальная схема, в целом одинакова.</p>
  <p id="lZul">Сегодня мы будем говорить об одном из множества механизмов оптимизации, который называется <strong>Inline Caches</strong>.</p>
  <p id="dw6Z">Итак, давайте возьмем самую обычную функцию и попробуем понять, как она будет работать в &quot;runtime&quot;.</p>
  <pre id="0XYR" data-lang="javascript">function getX(obj) {
  return obj.x;
}</pre>
  <p id="SUjc">Наша функция довольно примитивна. Простой геттер, который в качестве аргумента принимает объект, а на выходе возвращает значение свойства <code>x</code> этого объекта. С точки зрения JS-разработчика функция выглядит абсолютно атомарно. Однако, давайте заглянем под капот движка и посмотрим, что она представляет из себя в скомпилированном виде.</p>
  <p id="ZMi0">Для экспериментов, как обычно, будем использовать самый популярный JS-движок V8 (на момент написания статьи версия <a href="https://chromium.googlesource.com/v8/v8.git/+/refs/tags/12.6.72" target="_blank">12.6.72</a>). Для полноценного дебага, кроме тела самой функции нам нужна её вызвать с реальным аргументом. Также, добавим вывод информации об объекте, он понадобится нам чуть ниже.</p>
  <pre id="Xe9T" data-lang="javascript">function getX(obj) {
  %DebugPrint(obj);
  return obj.x;
}

getX({ x: 1 });</pre>
  <p id="o9sk">Напомню, <code>%DebugPrint</code> является строенной функцией V8, чтобы использовать её в коде, нужно запустить рантайм d8 с флагом <code>--allow-natives-syntax</code>. Долнительно, выведем в консоль байткод исполняемого скрипта (флаг <code>--print-bytecode</code>).</p>
  <pre id="qQ3P" data-lang="cpp">%&gt; d8 --print-bytecode --allow-natives-syntax index.js 
...
// байткод функции getX
[generated bytecode for function: getX (0x0b75000d9c29 &lt;SharedFunctionInfo getX&gt;)]
Bytecode length: 10
Parameter count 2
Register count 0
Frame size 0
         0x28c500002194 @    0 : 65 af 01 03 01    CallRuntime [DebugPrint], a0-a0
         0x28c500002199 @    5 : 2d 03 00 00       GetNamedProperty a0, [0], [0]
         0x28c50000219d @    9 : ab                Return
Constant pool (size = 1)
0xb75000d9dcd: [FixedArray] in OldSpace
 - map: 0x0b7500000565 &lt;Map(FIXED_ARRAY_TYPE)&gt;
 - length: 1
           0: 0x0b7500002b91 &lt;String[1]: #x&gt;
Handler Table (size = 0)
Source Position Table (size = 0)
// Далее информация о переданном объекте
DebugPrint: 0xb75001c9475: [JS_OBJECT_TYPE]
 - map: 0x0b75000d9d81 &lt;Map[16](HOLEY_ELEMENTS)&gt; [FastProperties]
 - prototype: 0x0b75000c4b11 &lt;Object map = 0xb75000c414d&gt;
 - elements: 0x0b75000006cd &lt;FixedArray[0]&gt; [HOLEY_ELEMENTS]
 - properties: 0x0b75000006cd &lt;FixedArray[0]&gt;
 - All own properties (excluding elements): {
    0xb7500002b91: [String] in ReadOnlySpace: #x: 1 (const data field 0), location: in-object
 }
0xb75000d9d81: [Map] in OldSpace
 - map: 0x0b75000c3c29 &lt;MetaMap (0x0b75000c3c79 &lt;NativeContext[285]&gt;)&gt;
 - type: JS_OBJECT_TYPE
 - instance size: 16
 - inobject properties: 1
 - unused property fields: 0
 - elements kind: HOLEY_ELEMENTS
 - enum length: invalid
 - stable_map
 - back pointer: 0x0b75000d9d59 &lt;Map[16](HOLEY_ELEMENTS)&gt;
 - prototype_validity cell: 0x0b7500000a31 &lt;Cell value= 1&gt;
 - instance descriptors (own) #1: 0x0b75001c9485 &lt;DescriptorArray[1]&gt;
 - prototype: 0x0b75000c4b11 &lt;Object map = 0xb75000c414d&gt;
 - constructor: 0x0b75000c4655 &lt;JSFunction Object (sfi = 0xb7500335385)&gt;
 - dependent code: 0x0b75000006dd &lt;Other heap object (WEAK_ARRAY_LIST_TYPE)&gt;
 - construction counter: 0</pre>
  <p id="7PAZ">Итак, в сущности, скомпилированный байткод функции <code>getX</code> выглядит следующим образом.</p>
  <pre id="UC9U" data-lang="cpp">0x28c500002194 @    0 : 65 af 01 03 01    CallRuntime [DebugPrint], a0-a0
0x28c500002199 @    5 : 2d 03 00 00       GetNamedProperty a0, [0], [0]
0x28c50000219d @    9 : ab                Return</pre>
  <p id="j63Q">Первая строчка, это вызов функции <code>%DebugPrint</code>. Она носит исключительно вспомогательный характер и на исполняемый код не влияет, мы можем её спокойно опустить. Следующая инструкция <code>GetNamedProperty</code>. Её задача - получить значение указанного свойства из переданного объекта. На вход инструкция получает три параметра. Первый - ссылка на объект. В нашем случае, ссылка на объект хранится в <code>a0</code> - первом аргументе функции <code>getX</code>. Второй и третий параметры, это адрес скрытого класса и <code>offset</code> нужно свойства в массиве дескрипторов.</p>
  <h2 id="KGhU">Форма объекта</h2>
  <p id="t2nC">Что такое скрытые классы, массив дескрипторов и как вообще устроены объекты в JavaScript я подробно рассказывал в статье <a href="https://blog.frontend-almanac.ru/js-object-structure" target="_blank">Структура объекта в JavaScript движках</a>. Не буду пересказывать всю статью, обозначу только нужные нам факты. Каждый объект имеет один или несколько скрытых классов (Hidden Classes). Скрытые классы являются исключительно внутренним механизмом движка и JS-разработчику не доступны, ну, по крайней мере, без применения специальных встроенных методов движка. Скрытый класс представляет, так называемую, форму объекта и всю необходимую информацию о свойствах объекта. Сам же объект хранит только значения свойств и ссылку на скрытый класс. Если два объекта имеют одинаковую форму, у них будет ссылка на один и тот же скрытый класс.</p>
  <pre id="ub6y" data-lang="javascript">const obj1 = { x: 1 }
const obj2 = { x: 2 }

//          {}     &lt;- empty map
//          |
//        { x }    &lt;- common map
//       /    \
// &lt;obj1&gt;      &lt;obj2&gt;</pre>
  <p id="N0FJ">По мере добавления свойств объекту, его дерево скрытых классов увеличивается.</p>
  <pre id="2U3U" data-lang="javascript">const obj1 = {}
obj1.x = 1;
obj1.y = 2;
obj1.z = 3;

// {} -&gt; { x } -&gt; { x, y } -&gt; { x, y, z } -&gt; &lt;obj1&gt;</pre>
  <p id="nNzq">Давайте вернемся к функции, с которой мы начали. Предположим, мы передали ей объект следующего вида.</p>
  <pre id="243b" data-lang="javascript">function getX(obj) {
  return obj.x;
}

getX({ y: 1, x: 2, z: 3 });</pre>
  <p id="DcPD">Что получить значение свойства <code>x</code>, интерпретатор должен знать: а) адрес последнего скрытого класса объекта; б) offset этого свойства в массиве десктрипторов.</p>
  <pre id="5lh1" data-lang="javascript">d8&gt; const obj = { y: 1, x: 2, z: 3};
d8&gt;
d8&gt; %DebugPrint(obj);
DebugPrint: 0x2034001c9435: [JS_OBJECT_TYPE]
 - map: 0x2034000d9cf9 &lt;Map[24](HOLEY_ELEMENTS)&gt; [FastProperties]
 - prototype: 0x2034000c4b11 &lt;Object map = 0x2034000c414d&gt;
 - elements: 0x2034000006cd &lt;FixedArray[0]&gt; [HOLEY_ELEMENTS]
 - properties: 0x2034000006cd &lt;FixedArray[0]&gt;
 - All own properties (excluding elements): {
    0x203400002ba1: [String] in ReadOnlySpace: #y: 1 (const data field 0), location: in-object
    0x203400002b91: [String] in ReadOnlySpace: #x: 2 (const data field 1), location: in-object
    0x203400002bb1: [String] in ReadOnlySpace: #z: 3 (const data field 2), location: in-object
 }
0x2034000d9cf9: [Map] in OldSpace
 - map: 0x2034000c3c29 &lt;MetaMap (0x2034000c3c79 &lt;NativeContext[285]&gt;)&gt;
 - type: JS_OBJECT_TYPE
 - instance size: 24
 - inobject properties: 3
 - unused property fields: 0
 - elements kind: HOLEY_ELEMENTS
 - enum length: invalid
 - stable_map
 - back pointer: 0x2034000d9cd1 &lt;Map[24](HOLEY_ELEMENTS)&gt;
 - prototype_validity cell: 0x203400000a31 &lt;Cell value= 1&gt;
 - instance descriptors (own) #3: 0x2034001c9491 &lt;DescriptorArray[3]&gt;
 - prototype: 0x2034000c4b11 &lt;Object map = 0x2034000c414d&gt;
 - constructor: 0x2034000c4655 &lt;JSFunction Object (sfi = 0x203400335385)&gt;
 - dependent code: 0x2034000006dd &lt;Other heap object (WEAK_ARRAY_LIST_TYPE)&gt;
 - construction counter: 0</pre>
  <p id="LqTi">В примере выше скрытый класс расположен по адресу <code>0x2034000d9cd1</code> (back pointer).</p>
  <pre id="KZ3y" data-lang="javascript">d8&gt; %DebugPrintPtr(0x2034000d9cd1)
DebugPrint: 0x2034000d9cd1: [Map] in OldSpace
 - map: 0x2034000c3c29 &lt;MetaMap (0x2034000c3c79 &lt;NativeContext[285]&gt;)&gt;
 - type: JS_OBJECT_TYPE
 - instance size: 24
 - inobject properties: 3
 - unused property fields: 1
 - elements kind: HOLEY_ELEMENTS
 - enum length: invalid
 - back pointer: 0x2034000d9c89 &lt;Map[24](HOLEY_ELEMENTS)&gt;
 - prototype_validity cell: 0x203400000a31 &lt;Cell value= 1&gt;
 - instance descriptors #2: 0x2034001c9491 &lt;DescriptorArray[3]&gt;
 - transitions #1: 0x2034000d9cf9 &lt;Map[24](HOLEY_ELEMENTS)&gt;
     0x203400002bb1: [String] in ReadOnlySpace: #z: (transition to (const data field, attrs: [WEC]) @ Any) -&gt; 0x2034000d9cf9 &lt;Map[24](HOLEY_ELEMENTS)&gt;
 - prototype: 0x2034000c4b11 &lt;Object map = 0x2034000c414d&gt;
 - constructor: 0x2034000c4655 &lt;JSFunction Object (sfi = 0x203400335385)&gt;
 - dependent code: 0x2034000006dd &lt;Other heap object (WEAK_ARRAY_LIST_TYPE)&gt;
 - construction counter: 0
0x2034000c3c29: [MetaMap] in OldSpace
 - map: 0x2034000c3c29 &lt;MetaMap (0x2034000c3c79 &lt;NativeContext[285]&gt;)&gt;
 - type: MAP_TYPE
 - instance size: 40
 - native_context: 0x2034000c3c79 &lt;NativeContext[285]&gt;</pre>
  <p id="QiAs">Из которого мы можем получить массив дескрипторов по адресу <code>0x2034001c9491</code> (instance descriptors).</p>
  <pre id="7vEp" data-lang="javascript">d8&gt; %DebugPrintPtr(0x2034001c9491)
DebugPrint: 0x2034001c9491: [DescriptorArray]
 - map: 0x20340000062d &lt;Map(DESCRIPTOR_ARRAY_TYPE)&gt;
 - enum_cache: 3
   - keys: 0x2034000daaa9 &lt;FixedArray[3]&gt;
   - indices: 0x2034000daabd &lt;FixedArray[3]&gt;
 - nof slack descriptors: 0
 - nof descriptors: 3
 - raw gc state: mc epoch 0, marked 0, delta 0
  [0]: 0x203400002ba1: [String] in ReadOnlySpace: #y (const data field 0:s, p: 2, attrs: [WEC]) @ Any
  [1]: 0x203400002b91: [String] in ReadOnlySpace: #x (const data field 1:s, p: 1, attrs: [WEC]) @ Any
  [2]: 0x203400002bb1: [String] in ReadOnlySpace: #z (const data field 2:s, p: 0, attrs: [WEC]) @ Any
0x20340000062d: [Map] in ReadOnlySpace
 - map: 0x2034000004c5 &lt;MetaMap (0x20340000007d &lt;null&gt;)&gt;
 - type: DESCRIPTOR_ARRAY_TYPE
 - instance size: variable
 - elements kind: HOLEY_ELEMENTS
 - enum length: invalid
 - stable_map
 - non-extensible
 - back pointer: 0x203400000061 &lt;undefined&gt;
 - prototype_validity cell: 0
 - instance descriptors (own) #0: 0x203400000701 &lt;DescriptorArray[0]&gt;
 - prototype: 0x20340000007d &lt;null&gt;
 - constructor: 0x20340000007d &lt;null&gt;
 - dependent code: 0x2034000006dd &lt;Other heap object (WEAK_ARRAY_LIST_TYPE)&gt;
 - construction counter: 0</pre>
  <p id="Wzb4">Где интерпретатор может найти нужное свойство по его имени и получить <code>offset</code>, в нашем случае равный <code>1</code>.</p>
  <p id="lL2y">Таким образом, путь интерпретатора будет выглядеть примерно следующим образом.</p>
  <figure id="fGX6" class="m_column">
    <img src="https://img3.teletype.in/files/2d/a4/2da4e55d-16a5-429a-8cff-57f83fd6d27b.png" width="1520" />
  </figure>
  <p id="BNJF">Наш случай довольно простой. Здесь значения свойств хранятся внутри самого объекта (in-object), что делает доступ к ним довольно быстрым. Но, тем не менее, определение позиции свойства в массиве дескрипторов все равно будет пропорционально количеству самих свойств. Более того, в статье <a href="https://blog.frontend-almanac.ru/js-object-structure" target="_blank">Структура объекта в JavaScript движках</a> я говорил, что значения не всегда хранятся таким &quot;быстрым&quot; образом. В ряде случаев тип хранилища может быть изменен на медленный &quot;словарь&quot;.</p>
  <h2 id="kqds">Inline Cache</h2>
  <p id="Kf2U">Конечно, в единичных случаях для JS-движка это все не вызывает больших трудностей, а время доступа к значению свойств вряд ли можно заметить невооруженным глазом. Но что, если наш случай не единичный? Допустим, нам требуется выполнить функцию сотни или тысячи раз в цикле? Суммарное время работы тут приобретает критическую важность. При том, что функция, фактически выполняет одни и те же операции, разработчики JavaScript предложили оптимизировать процесс поиска свойства и не выполнять его каждый раз. Все, что для этого требуется, сохранить адрес скрытого класса объекта и <code>offset</code> нужного свойства.</p>
  <p id="GQa9">Вернемся к самому началу статьи и к инструкции <code>GetNamedProperty</code>, которая имеет три параметра. Мы уже выяснили, что первый параметр - это ссылка на объект. Второй параметр - адрес скрытого класса. Третий параметр - найденный <code>offset</code> свойства. Определив эти параметры единожды, функция может их запомнить и при следующем запуске не выполнять процедуру поиска значения снова. Такое кэширование и называется <strong>Inline Cache (IC)</strong>.</p>
  <h2 id="cMEw">TurboFan</h2>
  <p id="RMNg">Однако, стоит учитывать, что мемоизация параметров тоже требует памяти и некоторого процессорного времени. Это делает механизм эффективным только при интенсивном выполнении функции (так называемая, <strong>hot</strong>-функция). Насколько интенсивно выполняется функция зависит от количества и частоты её вызовов. Оценкой интенсивности занимается оптимизатор. В случае с V8 в роли оптимизатора выступает TurboFan. Прежде всего, оптимизатор собирает граф из AST и байткода и отправляет его на фазу &quot;inlining&quot;, где собираются метрики вызовов, загрузок и сохранений в кэш. Далее, если функция пригодна для inline-кэширования, она встает в очередь на оптимизацию. После того как очередь заполнена оптимизатор должен определить самую &quot;горячую&quot; из них и произвести кэширование. Потом взять следующую и так далее. В отличие, кстати, от его предшественника Crankshaft, который брал функции по очереди начиная с первой. Вообще, данная тема довольно большая и заслуживает отдельной статьи, сейчас рассматривать все подробности и особенности эвристики TurboFan смысла не имеет . Перейдем лучше к примерам.</p>
  <h2 id="Tn1Y">Типы IC</h2>
  <p id="CQ2q">Чтобы проанализировать работу IC, нужно включить логирование в runtime-среде. В случае с d8 это можно сделать, указав флаг <code>--log-ic</code>.</p>
  <pre id="eZ36" data-lang="javascript">%&gt; d8 --log-ic

function getX(obj) {
  return obj.x;
}

for (let i = 0; i &lt; 10; i++) {
  getX({ x: i });
}</pre>
  <p id="csYj">Как я уже говорил, TurboFan начинает кэшировать свойства объектов только в hot-функциях. Чтобы сделать нашу функцию таковой, нам потребуется запустить её в цикле несколько раз подряд. В моем простом скрипте в среде d8 понадобилось минимум 10 итераций. На практике, в других условиях и при наличии других функций, это число может варьироваться и, скорее всего, будет больше. Полученный лог теперь можно прогнать через <strong>System Analyzer</strong>.</p>
  <pre id="jiaZ" data-lang="cpp">Grouped by type: 1#
&gt;100.00%	1	LoadIC
Grouped by category: 1#
&gt;100.00%	1	Load
Grouped by functionName: 1#
&gt;100.00%	1	~getX
Grouped by script: 1#
&gt;100.00%	1	Script(3): index.js
Grouped by sourcePosition: 1#
&gt;100.00%	1	index.js:3:14
Grouped by code: 1#
&gt;100.00%	1	Code(JS~)
Grouped by state: 1#
&gt;100.00%	1	0 → 1
Grouped by key: 1#
&gt;100.00%	1	x
Grouped by map: 1#
&gt;100.00%	1	Map(0x3cd2000d9e61)
Grouped by reason: 1#
&gt;100.00%	1	</pre>
  <p id="dbl5">В списке IC List мы видим нотацию типа <code>LoadIC</code>, которая говорит о получении доступа к свойству объекта из кэша. Здесь же указана сама функция <code>functionName: ~getX</code>, адрес скрытого класса <code>Map(0x3cd2000d9e61)</code> и название свойства <code>key: x</code>. Но наибольший интерес представляет <code>state: 0 -&gt; 1</code>.</p>
  <p id="MpZs">Существует несколько типов IC. Полный список выглядит следующим образом:</p>
  <p id="Qi78"><a href="https://chromium.googlesource.com/v8/v8.git/+/refs/tags/12.6.72/src/common/globals.h#1481" target="_blank">/src/common/globals.h#1481</a></p>
  <pre id="3AH2" data-lang="cpp">// State for inline cache call sites. Aliased as IC::State.
enum class InlineCacheState {
  // No feedback will be collected.
  NO_FEEDBACK,
  // Has never been executed.
  UNINITIALIZED,
  // Has been executed and only one receiver type has been seen.
  MONOMORPHIC,
  // Check failed due to prototype (or map deprecation).
  RECOMPUTE_HANDLER,
  // Multiple receiver types have been seen.
  POLYMORPHIC,
  // Many DOM receiver types have been seen for the same accessor.
  MEGADOM,
  // Many receiver types have been seen.
  MEGAMORPHIC,
  // A generic handler is installed and no extra typefeedback is recorded.
  GENERIC,
};</pre>
  <p id="ac4S">В системном анализаторе эти типы обозначаются символами</p>
  <pre id="iJ03">0: UNINITIALIZED
X: NO_FEEDBACK
1: MONOMORPHIC
^: RECOMPUTE_HANDLER
P: POLYMORPHIC
N: MEGAMORPHIC
D: MEGADOM
G: GENERIC</pre>
  <p id="jdQG">Например, тип <code>X (NO_FEEDBACK)</code> говорит о тои, что оптимизатор пока не собрал достаточной статистики, чтобы поставить функцию на оптимизацию. В нашем же случае мы видим <code>state: 0 -&gt; 1</code> означает, что функция перешла в состояние &quot;мономорфного IC&quot;.</p>
  <h3 id="ceDI">Monomorphic</h3>
  <p id="F114">Я уже говорил, что на практике, одна и та же функция может принимать объект в качестве аргумента. Форма этого объекта на конкретном вызове функции может отличаться от предыдущего. Что несколько усложняет жизнь оптимизатору. Меньше всего проблем возникает в случае, когда мы передаем в функцию только одинаковый по форме объекты, как в нашем последнем примере.</p>
  <pre id="Bx6k" data-lang="javascript">function getX(obj) {
  return obj.x; // Monomorphic IC
}

for (let i = 0; i &lt; 10; i++) {
  getX({ x: i }); // все объекты имеют одинаковую форму
}</pre>
  <p id="bTx7">В этом случае, оптимизатору надо просто запомнить адрес скрытого класса и <code>offset</code> свойства. Такой тип IC называется <strong>мономорфным</strong>.</p>
  <h3 id="DCVd">Polymorphic</h3>
  <p id="X9ub">Давайте теперь попробуем добавить вызов функции с объектами разной формы.</p>
  <pre id="fbGk" data-lang="javascript">function getX(obj) {
  return obj.x; // Polymorphic IC
}

for (let i = 0; i &lt; 10; i++) {
  getX({ x: i, y: 0 });
  getX({ y: 0, x: i });
}</pre>
  <p id="Ts5f">В &quot;IC List&quot; теперь мы видим:</p>
  <pre id="y8vc">50.00%        1    0 → 1
50.00%        1    1 → P</pre>
  <p id="l8Ot">Функция в рантайме получила несколько разных форм объекта. Доступ к свойству <code>x</code> у каждой формы будет отличаться. У каждой свой адрес скрытого класса и свой <code>offset</code> этого свойства. В этом,  случае оптимизатор выделяет по два слота для каждой формы объекта, куда и сохраняет нужные параметры. Условно представить такую структура можно в виде массива наборов параметров.</p>
  <pre id="Tl2I" data-lang="javascript">[
  [Map1, offset1],
  [Map2, offset2],
  ...
]</pre>
  <p id="Thlk">Такой тип IC называется <strong>полиморфным</strong>. У полиморфного IC есть ограничение на количество допустимых форм.</p>
  <p id="V4Hb"><a href="https://chromium.googlesource.com/v8/v8.git/+/refs/tags/12.6.72/src/wasm/wasm-constants.h#192" target="_blank">/src/wasm/wasm-constants.h#192</a></p>
  <pre id="SGjo" data-lang="cpp">// Maximum number of call targets tracked per call.
constexpr int kMaxPolymorphism = 4;</pre>
  <p id="wqoc">По умолчанию, полиморфный тип может от 2 до 4 форм на функцию. Но этот параметр можно регулировать флагом <code>--max-valid-polymorphic-map-count</code>.</p>
  <h3 id="it95">Megamorphic</h3>
  <p id="FP9J">Если же форм объекта больше, чем может переварит Polymorphic, тип меняется на <strong>мегаморфный</strong>.</p>
  <pre id="DV8p" data-lang="javascript">function getX(obj) {
  return obj.x; // Megamorphic IC
}

for (let i = 0; i &lt; 10; i++) {
  getX({ x: i });
  getX({ prop1: 0, x: i });
  getX({ prop1: 0, prop2: 1, x: i });
  getX({ prop1: 0, prop2: 1, prop3: 2, x: i });
  getX({ prop1: 0, prop2: 1, prop3: 2, prop4: 3, x: i });
}</pre>
  <p id="g3oM">Результат IC List:</p>
  <pre id="CwJl">40.00%        2    P → P
20.00%        1    0 → 1
20.00%        1    1 → P
20.00%        1    P → N</pre>
  <p id="eQCc">Поиск нужного набора параметров в таком случае нивелирует экономию процессорного времени и, следовательно, не имеет никакого смысла. Поэтому оптимизатор просто сохраняет в кэш символ <code>MegamorphicSymbol</code>. Для следующих вызовов фукнции это будет означать, что закэшированных параметров тут нет, их придется брать обычным способом. Равно как и нет смысла дальше ставить функцию на оптимизацию и собирать её метрики.</p>
  <h2 id="O04T">Заключение</h2>
  <p id="wVRb">Вы наверняка заметили, что в списке типов IC присутствует еще <code>MEGADOM</code>. Этот тип используется для кэширования нод DOM-дерева. Дело в том, что механизм инлайн-кэширования не ограничивается только функциями и объектами. Он активно применяется и во многих других местах, в том числе и за пределами V8.Ввесь объем информации о кэшировании за один раз мы физически покрыть не сможем. А раз уж мы сегодня говорим об объектах JavaScript, то и тип MegaDom рассматривать в этой статье не будем.</p>
  <p id="lQZE">Давайте проведем пару тестов и посмотрим, на сколько эффективно работает оптимизация Turbofan в V8. Эксперимент будем ставить в среде <strong>Node.js</strong> (последняя стабильная версия на момент написания статьи <code>v20.12.2</code>).</p>
  <p id="eiTF">Экспериментальный код:</p>
  <pre id="CG7S" data-lang="javascript">const N = 1000;

//===

function getXMonomorphic(obj) {
  let sum = 0;

  for (let i = 0; i &lt; N; i++) {
    sum += obj.x;
  }

  return sum;
}

console.time(&#x27;Monomorphic&#x27;);

for (let i = 0; i &lt; N; i++) {
  getXMonomorphic({ x: i });
  getXMonomorphic({ x: i });
  getXMonomorphic({ x: i });
  getXMonomorphic({ x: i });
  getXMonomorphic({ x: i });
}

console.timeLog(&#x27;Monomorphic&#x27;);

//===

function getXPolymorphic(obj) {
  let sum = 0;

  for (let i = 0; i &lt; N; i++) {
    sum += obj.x;
  }

  return sum;
}

console.time(&#x27;Polymorphic&#x27;);

for (let i = 0; i &lt; N; i++) {
  getXPolymorphic({ x: i, y: 0 });
  getXPolymorphic({ y: 0, x: i });
  getXPolymorphic({ x: i, y: 0 });
  getXPolymorphic({ y: 0, x: i });
  getXPolymorphic({ x: i, y: 0 });
}

console.timeEnd(&#x27;Polymorphic&#x27;);

//===

function getXMegamorphic(obj) {
  let sum = 0;

  for (let i = 0; i &lt; N; i++) {
    sum += obj.x;
  }

  return sum;
}

//===

console.time(&#x27;Megamorphic&#x27;);

for (let i = 0; i &lt; N; i++) {
  getXMegamorphic({ x: i });
  getXMegamorphic({ prop1: 0, x: i });
  getXMegamorphic({ prop1: 0, prop2: 1, x: i });
  getXMegamorphic({ prop1: 0, prop2: 1, prop3: 2, x: i });
  getXMegamorphic({ prop1: 0, prop2: 1, prop3: 2, prop4: 3, x: i });
}

console.timeLog(&#x27;Megamorphic&#x27;);</pre>
  <p id="TqGa">Для начала запустим скрипт с отключенной оптимизацией и посмотрим &quot;чистую&quot; скорость работы функций.</p>
  <pre id="ZFkw" data-lang="cpp">%&gt; node --no-opt test.js
Monomorphic: 68.55ms
Polymorphic: 69.939ms
Megamorphic: 85.045ms</pre>
  <p id="ZGa5">Для чистоты эксперимента я повторил тесты несколько раз. Скорость работы мономорфных и полиморфных объектов примерно одинакова. Полиморфные иногда могу оказываться даже быстрее. Связано это уже не столько с работой самого V8, сколько с системными ресурсами. А вот скорость мегаморфных объектов несколько ниже в силу того, что на этом тапе образуется дерево скрытых классов и доступ к свойствам этих объектов априори сложнее, чем в первых двух случаях. </p>
  <p id="RK5h">Теперь запустим тот же скрипт с включенной оптимизацией:</p>
  <pre id="JZNT" data-lang="cpp">%&gt; node test.js
Monomorphic: 9.313ms
Polymorphic: 9.673ms
Megamorphic: 29.104ms</pre>
  <p id="qHTX">Скорость работы мономорфных и полиморфных функций увеличилась примерно в 7 раз. Как и в предыдущем случае, разница между этими двумя типами незначительна и при повторных испытаниях полиморфные иногда бывают даже быстрее. А вот скорость мегаморфной функции увеличилась всего в 3 раза. Вообще, исходя из теории, описанной выше, мегаморфные функции не должны были показать прирост скорости на оптимизации. Однако, не все так просто. Во-первых, у них все же имеется побочный эффект от оптимизации, поскольку такие функции исключаются из процесса дальнейшего сбора метрик. Это хоть и не большое, но все же преимущество перед остальными типами. Во-вторых, оптимизация работы JS не ограничена инлайн-кэшированием доступа к свойствам объекта. Существует еще ряд других видов оптимизаций, которые в этой статье мы не рассматривали.</p>
  <p id="p7ZP">Более того, в 2023-м году Google выпустил релиз &quot;Chromium M117&quot;, в который был включен новый оптимизатор <strong>Maglev</strong>. Он был встроен как промежуточное звено между Ignition (интерпретатором) и Turbofan (оптимизатором). Теперь процесс оптимизации приобрел трех-ступенчатую архитектуру и выглядит как <code>Ignition -&gt; Maglev -&gt; Turbofan</code>. Maglev вносит свой вклад в оптимизацию функций, в частности, он работает с аргументами функции. Подробнее об этом поговорим в другой раз. А пока можем сделать вывод, что мегаморфные функции примерно в два раза медленнее мономорфных и полиморфных.</p>
  <p id="b5FS"></p>
  <hr />
  <p id="IhYu"></p>
  <p id="7pWR"><strong>Мои телеграмм-каналы:</strong></p>
  <section style="background-color:hsl(hsl(55,  86%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="YnAq">EN - <a href="https://t.me/frontend_almanac" target="_blank">https://t.me/frontend_almanac</a><br />RU - <a href="https://t.me/frontend_almanac_ru" target="_blank">https://t.me/frontend_almanac_ru</a></p>
  </section>
  <p id="Ri5D"><em>English version: <a href="https://blog.frontend-almanac.com/js-optimisation-ic" target="_blank">https://blog.frontend-almanac.com/js-optimisation-ic</a></em></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.frontend-almanac.ru/js-object-structure</guid><link>https://blog.frontend-almanac.ru/js-object-structure?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=frontend_almanac_ru</link><comments>https://blog.frontend-almanac.ru/js-object-structure?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=frontend_almanac_ru#comments</comments><dc:creator>frontend_almanac_ru</dc:creator><title>Структура объекта в JavaScript движках</title><pubDate>Sun, 31 Mar 2024 20:01:51 GMT</pubDate><media:content medium="image" url="https://img4.teletype.in/files/fe/bb/febbf129-152f-49f0-9296-31bd9fb29dc1.png"></media:content><description><![CDATA[<img src="https://img3.teletype.in/files/23/86/23864a98-b722-4c0c-9e46-b91d39e0d667.png"></img>С точки зрения разработчика, объекты в JavaScript довольно гибкие и понятные. Мы можем добавлять, удалять и изменять свойства объекта по своему усмотрению. Однако мало кто задумывается о том, как объекты хранятся в памяти и обрабатываются JS-движками. Могут ли действия разработчика, прямо или косвенно, оказать влияние на производительность и потребление памяти? Попробуем разобраться во всем этом в этой статье.]]></description><content:encoded><![CDATA[
  <p id="SHRk">С точки зрения разработчика, объекты в JavaScript довольно гибкие и понятные. Мы можем добавлять, удалять и изменять свойства объекта по своему усмотрению. Однако мало кто задумывается о том, как объекты хранятся в памяти и обрабатываются JS-движками. Могут ли действия разработчика, прямо или косвенно, оказать влияние на производительность и потребление памяти? Попробуем разобраться во всем этом в этой статье.</p>
  <h2 id="5BMc">Объект и его свойства</h2>
  <p id="DYa8">Прежде чем погрузиться во внутренние структуры объекта, давайте быстро пройдемся по мат. части и вспомним, что вообще представляет собой объект. Спецификация ECMA-262 в разделе <a href="https://262.ecma-international.org/#sec-object-type" target="_blank">6.1.7 The Object Type</a> определяет объект довольно примитивно, просто как набор свойств. Свойства объекта представлены как структура &quot;ключ-значение&quot;, где ключ (key) является названием свойства, а значение (value) - набором атрибутов. Все свойства объекта можно условно разделить на два типа: <strong>data properties</strong> и <strong>accessor properties</strong>.</p>
  <h3 id="lZqy">Data properties</h3>
  <p id="nXUx">Свойства, имеющие следующие атрибуты:</p>
  <ul id="0Hrq">
    <li id="yffg"><code>[[Value]]</code> - значение свойства</li>
    <li id="5NWY"><code>[[Writable]]</code> - <em>boolean</em>, по умолчанию false - если false, значение [[Value]] не может быть изменено</li>
    <li id="Geho"><code>[[Enumerable]]</code> - <em>boolean</em>, по умолчанию false - если true, свойство может участвовать в итерировании посредством &quot;for-in&quot;</li>
    <li id="ePqV"><code>[[Configurable]]</code> - <em>boolean</em>, по умолчанию false -  если false, свойство не может быть удалено, нельзя изменить его тип с Data property на Accessor property (и наоборот), нельзя изменить никакие атрибуты, кроме <code>[[Value]]</code> и выставления <code>[[Writable]]</code> в false</li>
  </ul>
  <h3 id="Aywz">Accessor properties</h3>
  <p id="7eob">Свойства, имеющие следующие атрибуты:</p>
  <ul id="Ie9z">
    <li id="3Yqy"><code>[[Get]]</code> - функция, возвращающая значение объекта</li>
    <li id="zynz"><code>[[Set]]</code> - функция, вызываемая при попытке присвоить значение свойству</li>
    <li id="kMSK"><code>[[Enumerable]]</code> - идентичен Data property</li>
    <li id="qDOT"><code>[[Configurable]]</code> - идентичен Data property</li>
  </ul>
  <h2 id="XUQI">Скрытые классы</h2>
  <p id="iA3u">Таким образом, согласно спецификации, помимо самих значений, каждое свойство объекта должно хранить информацию о своих атрибутах.</p>
  <pre id="PCxa" data-lang="javascript">const obj1 = { a: 1, b: 2 };</pre>
  <p id="BPU6">Приведенный выше простой объект, в представлении движка JavaScript, должен выглядеть примерно следующим образом.</p>
  <pre id="ZFvM" data-lang="javascript">{
  a: {
    [[Value]]: 1,
    [[Writable]]: true,
    [[Enumerable]]: true,
    [[Configurable]]: true,
  },
  b: {
    [[Value]]: 2,
    [[Writable]]: true,
    [[Enumerable]]: true,
    [[Configurable]]: true,
  }
}</pre>
  <p id="ZR2a">Давайте теперь представим, что у нас есть два схожих по структуре объекта.</p>
  <pre id="makz" data-lang="javascript">const obj1 = { a: 1, b: 2 };
const obj2 = { a: 3, b: 4 };</pre>
  <p id="LMVF">Согласно вышесказанному, нам нужно хранить информацию о каждом из четырех приведенных свойств этих двух объектов. Звучит несколько расточительно с точки зрения потребления памяти. Кроме того, очевидно, что конфигурация этих свойств одинакова, за исключением названия свойства и его <code>[[Value]]</code>.</p>
  <p id="SmQj">Данную проблему все популярные JS-движки решают с помощью так называемых <strong>скрытых классов</strong> (hidden classes). Это понятие часто можно встретить в разного рода публикациях и документации. Однако оно немного пересекается с понятием JavaScript-классов, поэтому разработчики движков приняли свои собственные определения. Так, в V8 скрытые классы обозначаются термином <strong>Maps</strong> (что также пересекается с понятием JavaScript Maps). В движке Chakra, используемом в браузере Internet Explorer, применяется термин <strong>Types</strong>. Разработчики Safari, в своем движке JavaScriptCore, используют понятие <strong>Structures</strong>. А в движке SpiderMonkey для Mozilla скрытые классы называются <strong>Shapes</strong>. Последнее, кстати, тоже довольно популярно и не редко встречается в публикациях, так как это понятие уникально и его трудно перепутать с чем-либо другим в JavaScript.</p>
  <p id="m7NZ">Вообще, про скрытые классы в сети есть много интересных публикаций. В частности, рекомендую заглянуть в <a href="https://mathiasbynens.be/notes/shapes-ics" target="_blank">пост Матиаса Биненса</a>, одного из разработчиков V8 и Chrome DevTools.</p>
  <p id="4vlm">Итак, суть скрытых классов заключается в том, чтобы выделить метаинформацию и свойства объекта в отдельные, переиспользуемые объекты и привязывать такой класс к реальному объекту по ссылке.</p>
  <p id="b3Mg">В данной концепции пример выше можно представить следующим образом. Позже мы посмотрим, как выглядят реальные Maps в движке V8, а пока проиллюстрирую в условном виде.</p>
  <pre id="g9PP" data-lang="javascript">Map1 {
  a: {
    [[Writable]]: true,
    [[Enumerable]]: true,
    [[Configurable]]: true,
  },
  b: {
    [[Writable]]: true,
    [[Enumerable]]: true,
    [[Configurable]]: true,
  }
}

ob1 {
  map: Map1,
  values: { a: 1, a: 2 }
}

ob2 {
  map: Map1,
  values: { a: 3, a: 4 }
}</pre>
  <h2 id="wtZP">Наследование скрытого класса</h2>
  <p id="7VVC">Концепция скрытых классов выглядит неплохо в случае объектов с одинаковой формой. Однако, что делать, если второй объект имеет другую структуру? В следующем примере два объекта не идентичны друг другу по структуре, но имеют пересечение.</p>
  <pre id="IKVT" data-lang="javascript">const obj1 = { a: 1, b: 2 };
const obj2 = { a: 3, b: 4, c: 5 };</pre>
  <p id="usJi">По описанной выше логике в памяти должно появиться два класса с разной формой. Однако тогда проблема дублирования атрибутов возвращается. Дабы избежать этого, скрытые классы принято наследовать друг от друга.</p>
  <pre id="Sask" data-lang="javascript">Map1 {
  a: {
    [[Writable]]: true,
    [[Enumerable]]: true,
    [[Configurable]]: true,
  },
  b: {
    [[Writable]]: true,
    [[Enumerable]]: true,
    [[Configurable]]: true,
  }
}

Map2 {
  back_pointer: Map1,
  с: {
    [[Writable]]: true,
    [[Enumerable]]: true,
    [[Configurable]]: true,
  }
}

ob1 {
  map: Map1,
  values: { a: 1, b: 2 }
}

ob2 {
  map: Map2,
  values: { a: 3, b: 4, c: 5 }
}</pre>
  <p id="YYrh">Здесь мы видим, что класс <code>Map2</code> описывает только одно свойство и ссылку на объект с более &quot;узкой&quot; формой.</p>
  <p id="i2pE">Стоит также сказать, что на форму скрытого класса влияет не только набор свойств, но и их порядок. Другими словами, следующие объекты будут иметь разные формы скрытых классов.</p>
  <pre id="MTeT" data-lang="javascript">Map1 {
  a: {
    [[Writable]]: true,
    [[Enumerable]]: true,
    [[Configurable]]: true,
  },
  b: {
    [[Writable]]: true,
    [[Enumerable]]: true,
    [[Configurable]]: true,
  }
}

Map2 {
  b: {
    [[Writable]]: true,
    [[Enumerable]]: true,
    [[Configurable]]: true,
  },
  a: {
    [[Writable]]: true,
    [[Enumerable]]: true,
    [[Configurable]]: true,
  }
}

ob1 {
  map: Map1,
  values: { a: 1, b: 2 }
}

ob2 {
  map: Map2,
  values: { b: 3, a: 4 }
}</pre>
  <p id="PMKd">Если мы меняем форму объекта уже после инициализации, это также приводит к созданию нового скрытого класса-наследника.</p>
  <pre id="NRJ3" data-lang="javascript">const ob1 = { a: 1, b: 2 };
obj1.c = 3;

const obj2 = { a: 4, b: 5, c: 6 };</pre>
  <p id="EWRm">Данный пример приводит к следующей структуре скрытых классов.</p>
  <pre id="tUuf" data-lang="javascript">Map1 {
  a: {
    [[Writable]]: true,
    [[Enumerable]]: true,
    [[Configurable]]: true,
  },
  b: {
    [[Writable]]: true,
    [[Enumerable]]: true,
    [[Configurable]]: true,
  }
}

Map2 {
  back_pointer: Map1,
  с: {
    [[Writable]]: true,
    [[Enumerable]]: true,
    [[Configurable]]: true,
  }
}

Map3 {
  back_pointer: Map1,
  с: {
    [[Writable]]: true,
    [[Enumerable]]: true,
    [[Configurable]]: true,
  }
}

ob1 {
  map: Map2,
  values: { a: 1, b: 2, c: 3 }
}

ob2 {
  map: Map3,
  values: { a: 4, b: 5, c: 6 }
}</pre>
  <h2 id="TAZ8">Скрытые классы на практике</h2>
  <p id="DGAB">Чуть выше я ссылался на <a href="https://mathiasbynens.be/notes/shapes-ics" target="_blank">пост Матиаса Биненса</a> о формах объекта. Однако с тех пор прошло много лет. Для чистоты эксперимента я решил проверить, как обстоят дела на практике в реальном движке V8.</p>
  <p id="giYG">Проведем эксперимент на примере, приведенном в статье Матиаса.</p>
  <figure id="5WRc" class="m_column">
    <img src="https://img4.teletype.in/files/fe/89/fe898845-11e5-48cb-b38d-1530a90d65d2.png" width="3542" />
  </figure>
  <p id="YfIT">Для этого нам понадобится встроенный внутренний методом V8 - <code>%DebugPrint</code>. Напомню, чтобы иметь возможность использовать встроенные методы движка, его нужно запустить с флагом <code>--allow-natives-syntax</code>. Чтобы видеть подробную информацию об объектах JS, движок должен быть скомпилирован в режиме <code>debug</code>.</p>
  <pre id="B2NN" data-lang="javascript">d8&gt; const a = {};
d8&gt; a.x = 6;
d8&gt; const b = { x: 6 };
d8&gt;
d8&gt; %DebugPrint(a);
DebugPrint: 0x1d47001c9425: [JS_OBJECT_TYPE]
 - map: 0x1d47000da9a9 &lt;Map[28](HOLEY_ELEMENTS)&gt; [FastProperties]
 - prototype: 0x1d47000c4b11 &lt;Object map = 0x1d47000c414d&gt;
 - elements: 0x1d47000006cd &lt;FixedArray[0]&gt; [HOLEY_ELEMENTS]
 - properties: 0x1d47000006cd &lt;FixedArray[0]&gt;
 - All own properties (excluding elements): {
    0x1d4700002b91: [String] in ReadOnlySpace: #x: 6 (const data field 0), location: in-object
 }
0x1d47000da9a9: [Map] in OldSpace
 - map: 0x1d47000c3c29 &lt;MetaMap (0x1d47000c3c79 &lt;NativeContext[285]&gt;)&gt;
 - type: JS_OBJECT_TYPE
 - instance size: 28
 - inobject properties: 4
 - unused property fields: 3
 - elements kind: HOLEY_ELEMENTS
 - enum length: invalid
 - stable_map
 - back pointer: 0x1d47000c4945 &lt;Map[28](HOLEY_ELEMENTS)&gt;
 - prototype_validity cell: 0x1d47000da9f1 &lt;Cell value= 0&gt;
 - instance descriptors (own) #1: 0x1d47001cb111 &lt;DescriptorArray[1]&gt;
 - prototype: 0x1d47000c4b11 &lt;Object map = 0x1d47000c414d&gt;
 - constructor: 0x1d47000c4655 &lt;JSFunction Object (sfi = 0x1d4700335385)&gt;
 - dependent code: 0x1d47000006dd &lt;Other heap object (WEAK_ARRAY_LIST_TYPE)&gt;
 - construction counter: 0</pre>
  <p id="v33f">Мы видим объект <code>a</code>, размещенный по адресу <code>0x1d47001c9425</code>. К объекту привязан скрытый класс с адресом <code>0x1d47000da9a9</code>. Внутри самого объекта хранится значение <code>#x: 6</code>. Атрибуты свойства расположены в привязанном скрытом классе в поле <code>instance descriptors</code>. На всякий случай, давайте посмотрим на массив дескрипторов по указанному адресу.</p>
  <pre id="5WPW" data-lang="javascript">d8&gt; %DebugPrintPtr(0x1d47001cb111)
DebugPrint: 0x1d47001cb111: [DescriptorArray]
 - map: 0x1d470000062d &lt;Map(DESCRIPTOR_ARRAY_TYPE)&gt;
 - enum_cache: 1
   - keys: 0x1d47000dacad &lt;FixedArray[1]&gt;
   - indices: 0x1d47000dacb9 &lt;FixedArray[1]&gt;
 - nof slack descriptors: 0
 - nof descriptors: 1
 - raw gc state: mc epoch 0, marked 0, delta 0
  [0]: 0x1d4700002b91: [String] in ReadOnlySpace: #x (const data field 0:s, p: 0, attrs: [WEC]) @ Any
0x1d470000062d: [Map] in ReadOnlySpace
 - map: 0x1d47000004c5 &lt;MetaMap (0x1d470000007d &lt;null&gt;)&gt;
 - type: DESCRIPTOR_ARRAY_TYPE
 - instance size: variable
 - elements kind: HOLEY_ELEMENTS
 - enum length: invalid
 - stable_map
 - non-extensible
 - back pointer: 0x1d4700000061 &lt;undefined&gt;
 - prototype_validity cell: 0
 - instance descriptors (own) #0: 0x1d4700000701 &lt;DescriptorArray[0]&gt;
 - prototype: 0x1d470000007d &lt;null&gt;
 - constructor: 0x1d470000007d &lt;null&gt;
 - dependent code: 0x1d47000006dd &lt;Other heap object (WEAK_ARRAY_LIST_TYPE)&gt;
 - construction counter: 0

32190781763857</pre>
  <p id="oUvY">В массиве дескрипторов имеется элемент <code>#x</code>, который хранит всю необходимую информацию о свойстве объекта.</p>
  <p id="yJ8i">Теперь давайте посмотрим на ссылку <code>back pointer</code> с адресом <code>0x1d47000c4945</code>.</p>
  <pre id="uqs2" data-lang="javascript">d8&gt; %DebugPrintPtr(0x1d47000c4945)
DebugPrint: 0x1d47000c4945: [Map] in OldSpace
 - map: 0x1d47000c3c29 &lt;MetaMap (0x1d47000c3c79 &lt;NativeContext[285]&gt;)&gt;
 - type: JS_OBJECT_TYPE
 - instance size: 28
 - inobject properties: 4
 - unused property fields: 4
 - elements kind: HOLEY_ELEMENTS
 - enum length: invalid
 - back pointer: 0x1d4700000061 &lt;undefined&gt;
 - prototype_validity cell: 0x1d4700000a31 &lt;Cell value= 1&gt;
 - instance descriptors (own) #0: 0x1d4700000701 &lt;DescriptorArray[0]&gt;
 - transitions #1: 0x1d47000da9d1 &lt;TransitionArray[6]&gt;Transition array #1:
     0x1d4700002b91: [String] in ReadOnlySpace: #x: (transition to (const data field, attrs: [WEC]) @ Any) -&gt; 0x1d47000da9a9 &lt;Map[28](HOLEY_ELEMENTS)&gt;

 - prototype: 0x1d47000c4b11 &lt;Object map = 0x1d47000c414d&gt;
 - constructor: 0x1d47000c4655 &lt;JSFunction Object (sfi = 0x1d4700335385)&gt;
 - dependent code: 0x1d47000006dd &lt;Other heap object (WEAK_ARRAY_LIST_TYPE)&gt;
 - construction counter: 0
0x1d47000c3c29: [MetaMap] in OldSpace
 - map: 0x1d47000c3c29 &lt;MetaMap (0x1d47000c3c79 &lt;NativeContext[285]&gt;)&gt;
 - type: MAP_TYPE
 - instance size: 40
 - native_context: 0x1d47000c3c79 &lt;NativeContext[285]&gt;

32190780688709</pre>
  <p id="dlKd">Этот скрытый класс является представлением пустого объекта. Массив дескрипторов у него пустой, а ссылка <code>back pointer</code> не определена.</p>
  <p id="7qSM">Теперь давайте посмотрим на объект <code>b</code>.</p>
  <pre id="0cay" data-lang="javascript">d8&gt; %DebugPrint(b)    
DebugPrint: 0x1d47001cb169: [JS_OBJECT_TYPE]
 - map: 0x1d47000dab39 &lt;Map[16](HOLEY_ELEMENTS)&gt; [FastProperties]
 - prototype: 0x1d47000c4b11 &lt;Object map = 0x1d47000c414d&gt;
 - elements: 0x1d47000006cd &lt;FixedArray[0]&gt; [HOLEY_ELEMENTS]
 - properties: 0x1d47000006cd &lt;FixedArray[0]&gt;
 - All own properties (excluding elements): {
    0x1d4700002b91: [String] in ReadOnlySpace: #x: 6 (const data field 0), location: in-object
 }
0x1d47000dab39: [Map] in OldSpace
 - map: 0x1d47000c3c29 &lt;MetaMap (0x1d47000c3c79 &lt;NativeContext[285]&gt;)&gt;
 - type: JS_OBJECT_TYPE
 - instance size: 16
 - inobject properties: 1
 - unused property fields: 0
 - elements kind: HOLEY_ELEMENTS
 - enum length: 1
 - stable_map
 - back pointer: 0x1d47000dab11 &lt;Map[16](HOLEY_ELEMENTS)&gt;
 - prototype_validity cell: 0x1d4700000a31 &lt;Cell value= 1&gt;
 - instance descriptors (own) #1: 0x1d47001cb179 &lt;DescriptorArray[1]&gt;
 - prototype: 0x1d47000c4b11 &lt;Object map = 0x1d47000c414d&gt;
 - constructor: 0x1d47000c4655 &lt;JSFunction Object (sfi = 0x1d4700335385)&gt;
 - dependent code: 0x1d47000006dd &lt;Other heap object (WEAK_ARRAY_LIST_TYPE)&gt;
 - construction counter: 0

{x: 6}</pre>
  <p id="j8ya">Здесь также значение свойства хранится в самом объекте, а атрибуты свойства — в массиве дескрипторов скрытого класса. Однако обращу внимание, что ссылка <code>back pointer</code> здесь тоже не пустая, хотя на приведенной схеме класса-родителя тут быть не должно. Давайте посмотрим на класс по этой ссылке.</p>
  <pre id="2uUY" data-lang="javascript">d8&gt; %DebugPrintPtr(0x1d47000dab11)
DebugPrint: 0x1d47000dab11: [Map] in OldSpace
 - map: 0x1d47000c3c29 &lt;MetaMap (0x1d47000c3c79 &lt;NativeContext[285]&gt;)&gt;
 - type: JS_OBJECT_TYPE
 - instance size: 16
 - inobject properties: 1
 - unused property fields: 1
 - elements kind: HOLEY_ELEMENTS
 - enum length: invalid
 - back pointer: 0x1d4700000061 &lt;undefined&gt;
 - prototype_validity cell: 0x1d4700000a31 &lt;Cell value= 1&gt;
 - instance descriptors (own) #0: 0x1d4700000701 &lt;DescriptorArray[0]&gt;
 - transitions #1: 0x1d47000dab39 &lt;Map[16](HOLEY_ELEMENTS)&gt;
     0x1d4700002b91: [String] in ReadOnlySpace: #x: (transition to (const data field, attrs: [WEC]) @ Any) -&gt; 0x1d47000dab39 &lt;Map[16](HOLEY_ELEMENTS)&gt;
 - prototype: 0x1d47000c4b11 &lt;Object map = 0x1d47000c414d&gt;
 - constructor: 0x1d47000c4655 &lt;JSFunction Object (sfi = 0x1d4700335385)&gt;
 - dependent code: 0x1d47000006dd &lt;Other heap object (WEAK_ARRAY_LIST_TYPE)&gt;
 - construction counter: 0
0x1d47000c3c29: [MetaMap] in OldSpace
 - map: 0x1d47000c3c29 &lt;MetaMap (0x1d47000c3c79 &lt;NativeContext[285]&gt;)&gt;
 - type: MAP_TYPE
 - instance size: 40
 - native_context: 0x1d47000c3c79 &lt;NativeContext[285]&gt;

32190780779281</pre>
  <p id="KpcH">Класс выглядит точно так же, как скрытый класс пустого объекта выше, но с другим адресом. Это означает, что фактически это дубликат предыдущего класса. Таким образом, реальная структура данного примера выглядит следующим образом.</p>
  <figure id="AJjb" class="m_column">
    <img src="https://img4.teletype.in/files/f8/02/f802be5a-7123-4ebd-9732-22e1179bcfff.png" width="3564" />
  </figure>
  <p id="17ad">Это первое отклонение от теории. Чтобы понять, зачем нужен еще один скрытый класс для пустого объекта, нам потребуется объект с несколькими свойствами. Предположим, что исходный объект изначально имеет несколько свойств. Исследовать такой объект через командную строку будет не очень удобно, поэтому воспользуемся Chrome DevTools. Для удобства, замкнем объект внутри контекста функции.</p>
  <pre id="Y6qG" data-lang="javascript">function V8Snapshot() {
  this.obj1 = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 };
}

const v8Snapshot1 = new V8Snapshot();</pre>
  <figure id="4Efl" class="m_column">
    <img src="https://img1.teletype.in/files/c4/01/c4013aee-927c-4f07-8715-3498c1c058f6.png" width="2122" />
  </figure>
  <p id="RliO">Слепок памяти показывает 6 наследуемых классов для этого объекта, что равно количеству свойств объекта. И это второе отклонение от теории, согласно которой предполагалось, что объект изначально имеет один скрытый класс, форма которого содержит те свойства, с которыми он был инициализирован. Причина этому кроется в том, что на практике мы оперируем не одним единственным объектом, а несколькими, может быть даже десятками, сотнями или тысячами. В таких реалиях поиск и перестройка деревьев классов могут оказаться весьма дорогими. Итак, мы подошли к еще одному понятию JS движков.</p>
  <h2 id="gVIL">Переходы</h2>
  <p id="ZKXQ">Давайте к примеру выше добавим еще один объект со схожей формой.</p>
  <pre id="d6vO" data-lang="javascript">function V8Snapshot() {
  this.obj1 = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 };
  this.obj2 = { a: 1, b: 2, d: 3, c: 4, e: 5, f: 6 };
}

const v8Snapshot1 = new V8Snapshot();</pre>
  <p id="aSuf">На первый взгляд, форма второго объекта очень похожа, но свойства <code>c</code> и <code>d</code> имеют другой порядок следования.</p>
  <figure id="22PY" class="m_column">
    <img src="https://img3.teletype.in/files/e4/79/e47932db-ae93-4b9f-bbc8-dec81a0fe24b.png" width="2124" />
  </figure>
  <p id="XoKe">В массивах дескрипторов эти свойства будут иметь разные индексы. Класс с адресом <code>@101187</code> имеет двух наследников.</p>
  <figure id="gsJ0" class="m_column">
    <img src="https://img1.teletype.in/files/08/bf/08bf461b-dd6c-491d-9066-ea5b36ef3880.png" width="2126" />
  </figure>
  <p id="sRHZ">Для большей наглядности прогоним лог скрипта через V8 System Analyzer.</p>
  <figure id="k61B" class="m_column">
    <img src="https://img4.teletype.in/files/38/29/38299252-eed7-430d-a940-dd4cad02d959.png" width="1730" />
  </figure>
  <p id="4az2">Здесь хорошо видно, что изначальная форма <code>{ a, b, c, d, e, f }</code> имеет расширение в точке <code>c</code>. Однако интерпретатор не узнает об этом, пока не начнет инициализацию второго объекта. Чтобы составить новое дерево классов, движку пришлось бы найти в куче подходящий по форме класс, разбить его на части, сформировать новые классы и переназначить их всем созданным объектам. Чтобы избежать этого, разработчики V8 решили разбивать класс на набор минимальных форм сразу, еще при первой инициализации объекта, начиная с пустого класса.</p>
  <pre id="30k0" data-lang="javascript">{}
{ a }
{ a, b }
{ a, b, c }
{ a, b, c, d }
{ a, b, c, d, e }
{ a, b, c, d, e, f }</pre>
  <p id="fHfK">Создание нового скрытого класса с добавлением или изменением какого-либо свойства называется <strong>переходом</strong> (transition). В нашем случае, у первого объекта изначально будет 6 переходов (+a, +b, +c и т.д.).</p>
  <p id="0nTk">Такой подход позволяет: а) легко найти подходящую стартовую форму для нового объекта, б) нет необходимости ничего перестраивать, достаточно создать новый класс с ссылкой на подходящую минимальную форму.</p>
  <pre id="utRQ" data-lang="javascript">              {}
              { a }
              { a, b }

{ a, b, c }            { a, b, d }
{ a, b, c, d }         { a, b, d, c }
{ a, b, c, d, e }      { a, b, d, c, e }
{ a, b, c, d, e, f }   { a, b, d, c, e, f }</pre>
  <h2 id="nHd6">Внутренние и внешние свойства объекта</h2>
  <p id="g5KL">Рассмотрим следующий пример:</p>
  <pre id="3j9o" data-lang="javascript">d8&gt; const obj1 = { a: 1 };
d8&gt; obj1.b = 2;
d8&gt;
d8&gt; %DebugPrint(obj1);
DebugPrint: 0x2387001c942d: [JS_OBJECT_TYPE]
 - map: 0x2387000dabb1 &lt;Map[16](HOLEY_ELEMENTS)&gt; [FastProperties]
 - prototype: 0x2387000c4b11 &lt;Object map = 0x2387000c414d&gt;
 - elements: 0x2387000006cd &lt;FixedArray[0]&gt; [HOLEY_ELEMENTS]
 - properties: 0x2387001cb521 &lt;PropertyArray[3]&gt;
 - All own properties (excluding elements): {
    0x238700002a21: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object
    0x238700002a31: [String] in ReadOnlySpace: #b: 2 (const data field 1), location: properties[0]
 }
0x2387000dabb1: [Map] in OldSpace
 - map: 0x2387000c3c29 &lt;MetaMap (0x2387000c3c79 &lt;NativeContext[285]&gt;)&gt;
 - type: JS_OBJECT_TYPE
 - instance size: 16
 - inobject properties: 1
 - unused property fields: 2
 - elements kind: HOLEY_ELEMENTS
 - enum length: invalid
 - stable_map
 - back pointer: 0x2387000d9ca1 &lt;Map[16](HOLEY_ELEMENTS)&gt;
 - prototype_validity cell: 0x2387000dabd9 &lt;Cell value= 0&gt;
 - instance descriptors (own) #2: 0x2387001cb4f9 &lt;DescriptorArray[2]&gt;
 - prototype: 0x2387000c4b11 &lt;Object map = 0x2387000c414d&gt;
 - constructor: 0x2387000c4655 &lt;JSFunction Object (sfi = 0x238700335385)&gt;
 - dependent code: 0x2387000006dd &lt;Other heap object (WEAK_ARRAY_LIST_TYPE)&gt;
 - construction counter: 0
 
 {a: 1, b: 2}</pre>
  <p id="Ee7E">Если внимательно посмотреть на набор значений этого объекта, мы увидим, что свойство <code>a</code> помечено как <code>in-object</code>, а свойство <code>b</code> - как элемент массива <code>properties</code>.</p>
  <pre id="YEeK" data-lang="javascript">- All own properties (excluding elements): {
    ... #a: 1 (const data field 0), location: in-object
    ... #b: 2 (const data field 1), location: properties[0]
 }</pre>
  <p id="Vksd">Этот пример показывает, что часть свойств хранится непосредственно внутри самого объекта (&quot;in-object&quot;), а часть свойств - во внешнем хранилище свойств. Связано это с тем, что согласно спецификации <a href="https://262.ecma-international.org/#sec-object-type" target="_blank">ECMA-262</a>, объекты JavaScript не имеют фиксированного размера. Добавляя или удаляя свойства в объекте, меняется его размер. Из-за этого возникает вопрос: какую область памяти выделить под объект? Более того, как расширить уже аллоцированную память объекта? Разработчики V8 решили эти вопросы следующим образом.</p>
  <h3 id="P3Hb">Внутренние свойства</h3>
  <p id="5N3j">В момент первичной инициализации литерал объекта уже распарсен, и AST-дерево содержит информацию о свойствах, указанных в момент инициализации. Набор таких свойств помещается непосредственно внутрь объекта, что позволяет обращаться к ним максимально быстро и с минимальными затратами. Эти свойства называются <strong>in-object</strong>.</p>
  <p id="qjmZ">Давайте еще раз взглянем на класс пустого объекта.</p>
  <pre id="hg3c" data-lang="javascript">d8&gt; const obj1 = {}
d8&gt;
d8&gt; %DebugPrint(obj1);
DebugPrint: 0x2d56001c9ed1: [JS_OBJECT_TYPE]
 - map: 0x2d56000c4945 &lt;Map[28](HOLEY_ELEMENTS)&gt; [FastProperties]
 - prototype: 0x2d56000c4b11 &lt;Object map = 0x2d56000c414d&gt;
 - elements: 0x2d56000006cd &lt;FixedArray[0]&gt; [HOLEY_ELEMENTS]
 - properties: 0x2d56000006cd &lt;FixedArray[0]&gt;
 - All own properties (excluding elements): {}
0x2d56000c4945: [Map] in OldSpace
 - map: 0x2d56000c3c29 &lt;MetaMap (0x2d56000c3c79 &lt;NativeContext[285]&gt;)&gt;
 - type: JS_OBJECT_TYPE
 - instance size: 28
 - inobject properties: 4
 - unused property fields: 4
 - elements kind: HOLEY_ELEMENTS
 - enum length: invalid
 - back pointer: 0x2d5600000061 &lt;undefined&gt;
 - prototype_validity cell: 0x2d5600000a31 &lt;Cell value= 1&gt;
 - instance descriptors (own) #0: 0x2d5600000701 &lt;DescriptorArray[0]&gt;
 - prototype: 0x2d56000c4b11 &lt;Object map = 0x2d56000c414d&gt;
 - constructor: 0x2d56000c4655 &lt;JSFunction Object (sfi = 0x2d5600335385)&gt;
 - dependent code: 0x2d56000006dd &lt;Other heap object (WEAK_ARRAY_LIST_TYPE)&gt;
 - construction counter: 0</pre>
  <p id="hjAA">Хочу обратить внимание на параметр <code>inobject properties</code>. Здесь он равен 4, хотя в объекте еще нет ни одного свойства. Дело в том, что у пустых объектов, по умолчанию есть несколько слотов для <code>in-object</code> свойств. В V8 количество таких слотов равно 4.</p>
  <pre id="gM1n" data-lang="javascript">d8&gt; obj1.a = 1;
d8&gt; obj1.b = 2;
d8&gt; obj1.c = 3;
d8&gt; obj1.d = 4;
d8&gt; obj1.e = 5;
d8&gt; obj1.f = 6;
d8&gt;
d8&gt; %DebugPrint(obj1);
DebugPrint: 0x2d56001c9ed1: [JS_OBJECT_TYPE]
 - map: 0x2d56000db291 &lt;Map[28](HOLEY_ELEMENTS)&gt; [FastProperties]
 - prototype: 0x2d56000c4b11 &lt;Object map = 0x2d56000c414d&gt;
 - elements: 0x2d56000006cd &lt;FixedArray[0]&gt; [HOLEY_ELEMENTS]
 - properties: 0x2d56001cc1a9 &lt;PropertyArray[3]&gt;
 - All own properties (excluding elements): {
    0x2d5600002a21: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object
    0x2d5600002a31: [String] in ReadOnlySpace: #b: 2 (const data field 1), location: in-object
    0x2d5600002a41: [String] in ReadOnlySpace: #c: 3 (const data field 2), location: in-object
    0x2d5600002a51: [String] in ReadOnlySpace: #d: 4 (const data field 3), location: in-object
    0x2d5600002a61: [String] in ReadOnlySpace: #e: 5 (const data field 4), location: properties[0]
    0x2d5600002a71: [String] in ReadOnlySpace: #f: 6 (const data field 5), location: properties[1]
 }
0x2d56000db291: [Map] in OldSpace
 - map: 0x2d56000c3c29 &lt;MetaMap (0x2d56000c3c79 &lt;NativeContext[285]&gt;)&gt;
 - type: JS_OBJECT_TYPE
 - instance size: 28
 - inobject properties: 4
 - unused property fields: 1
 - elements kind: HOLEY_ELEMENTS
 - enum length: invalid
 - stable_map
 - back pointer: 0x2d56000db169 &lt;Map[28](HOLEY_ELEMENTS)&gt;
 - prototype_validity cell: 0x2d56000dace9 &lt;Cell value= 0&gt;
 - instance descriptors (own) #6: 0x2d56001cc1f5 &lt;DescriptorArray[6]&gt;
 - prototype: 0x2d56000c4b11 &lt;Object map = 0x2d56000c414d&gt;
 - constructor: 0x2d56000c4655 &lt;JSFunction Object (sfi = 0x2d5600335385)&gt;
 - dependent code: 0x2d56000006dd &lt;Other heap object (WEAK_ARRAY_LIST_TYPE)&gt;
 - construction counter: 0</pre>
  <p id="ijUZ">Это значит, что первые 4 свойства, добавленные в пустой объект будут размещены в эти слоты как <code>in-object</code>.</p>
  <h3 id="yhT4">Внешние свойства</h3>
  <p id="Hhfp">Свойства, которые были добавлены после инициализации, уже не могут быть размещены внутри объекта, так как память под объект уже выделена. Чтобы не тратить ресурсы на переаллоцирование всего объекта, движок помещает такие свойства во внешнее хранилище, в данном случае, во внешний массив свойств, ссылка на который уже имеется внутри объекта. Такие свойства называются <strong>внешними</strong> или <strong>нормальными</strong> (именно такой термин можно нередко встретить в материалах разработчиков V8). Доступ к таким свойствам чуть менее быстрый, так как требуется резолв ссылки на хранилище и получение свойства по индексу. Но это намного эффективнее, чем переаллоцирование всего объекта.</p>
  <h3 id="hy6t">Быстрые и медленные свойства</h3>
  <p id="oLAV">Внешнее свойство из примера выше, как мы только что рассмотрели, хранится во внешнем массиве свойств, связанном непосредственно с нашим объектом. Формат данных в этом массиве идентичен формату внутренних свойств. Другими словами, там хранятся только значения свойств, а метаинформация о них размещена в массиве дескрипторов, где также содержится информация и о внутренних свойствах. По сути, внешние свойства отличаются от внутренних только местом их хранения. И те, и другие условно можно считать быстрыми свойствами. Однако напомню, что JavaScript - это живой и гибкий язык программирования. Разработчик имеет возможность добавлять, удалять и изменять свойства объекта по своему усмотрению. Активное изменение набора свойств может привести к существенным затратам процессорного времени. Для оптимизации этого процесса V8 поддерживает так называемые &quot;медленные&quot; свойства. Суть медленных свойств заключается в использовании другого типа внешнего хранилища. Вместо массива значений свойства размещаются в отдельном объекте-словаре вместе со всеми их атрибутами. Доступ как к значениям, так и к атрибутам таких свойств осуществляется по их названию, которое служит ключом словаря.</p>
  <pre id="228w" data-lang="javascript">d8&gt; delete obj1.a;
d8&gt;
d8&gt; %DebugPrint(obj1)
DebugPrint: 0x2387001c942d: [JS_OBJECT_TYPE]
 - map: 0x2387000d6071 &lt;Map[12](HOLEY_ELEMENTS)&gt; [DictionaryProperties]
 - prototype: 0x2387000c4b11 &lt;Object map = 0x2387000c414d&gt;
 - elements: 0x2387000006cd &lt;FixedArray[0]&gt; [HOLEY_ELEMENTS]
 - properties: 0x2387001cc1d9 &lt;NameDictionary[30]&gt;
 - All own properties (excluding elements): {
   b: 2 (data, dict_index: 2, attrs: [WEC])
 }
0x2387000d6071: [Map] in OldSpace
 - map: 0x2387000c3c29 &lt;MetaMap (0x2387000c3c79 &lt;NativeContext[285]&gt;)&gt;
 - type: JS_OBJECT_TYPE
 - instance size: 12
 - inobject properties: 0
 - unused property fields: 0
 - elements kind: HOLEY_ELEMENTS
 - enum length: invalid
 - dictionary_map
 - may_have_interesting_properties
 - back pointer: 0x238700000061 &lt;undefined&gt;
 - prototype_validity cell: 0x238700000a31 &lt;Cell value= 1&gt;
 - instance descriptors (own) #0: 0x238700000701 &lt;DescriptorArray[0]&gt;
 - prototype: 0x2387000c4b11 &lt;Object map = 0x2387000c414d&gt;
 - constructor: 0x2387000c4655 &lt;JSFunction Object (sfi = 0x238700335385)&gt;
 - dependent code: 0x2387000006dd &lt;Other heap object (WEAK_ARRAY_LIST_TYPE)&gt;
 - construction counter: 0

{b: 2}</pre>
  <p id="GG69">Мы удалили свойство <code>obj1.a</code>. Несмотря на то, что свойство было внутренним, мы полностью изменили форму скрытого класса. Если быть точным, мы его сократили, что отличается от типичного расширения формы. Это означает, что дерево форм стало короче, следовательно, дескрипторы и массив значений также должны быть перестроены. Все эти операции требуют определенных временных ресурсов. Чтобы избежать этого, движок изменяет способ хранения свойств объекта на медленный с использованием объекта-словаря. В данном примере словарь (<code>NameDictionary</code>) расположен по адресу <code>0x2387001cc1d9</code>.</p>
  <pre id="OrxX" data-lang="javascript">d8&gt; %DebugPrintPtr(0x2387001cc1d9)
DebugPrint: 0x2387001cc1d9: [NameDictionary]
 - FixedArray length: 30
 - elements: 1
 - deleted: 1
 - capacity: 8
 - elements: {
              7: b -&gt; 2 (data, dict_index: 2, attrs: [WEC])
 }
0x238700000ba1: [Map] in ReadOnlySpace
 - map: 0x2387000004c5 &lt;MetaMap (0x23870000007d &lt;null&gt;)&gt;
 - type: NAME_DICTIONARY_TYPE
 - instance size: variable
 - elements kind: HOLEY_ELEMENTS
 - enum length: invalid
 - stable_map
 - back pointer: 0x238700000061 &lt;undefined&gt;
 - prototype_validity cell: 0
 - instance descriptors (own) #0: 0x238700000701 &lt;DescriptorArray[0]&gt;
 - prototype: 0x23870000007d &lt;null&gt;
 - constructor: 0x23870000007d &lt;null&gt;
 - dependent code: 0x2387000006dd &lt;Other heap object (WEAK_ARRAY_LIST_TYPE)&gt;
 - construction counter: 0

39062729441753</pre>
  <h2 id="wz6e">Массивы</h2>
  <p id="kM1l">Согласно разделу <a href="https://262.ecma-international.org/#sec-array-objects" target="_blank">23.1 Array Objects</a> спецификации, массив - это объект, ключи которого являются целыми числами от <code>0</code> до <code>2**32 - 2</code>. С одной стороны, кажется, что с точки зрения скрытых классов массив ничем не отличается от обычного объекта. Однако на практике массивы бывают довольно большими. Что если в массиве тысячи элементов? Неужели на каждый элемент будет создан отдельный скрытый класс? Давайте посмотрим, как на самом деле выглядит скрытый класс массива.</p>
  <pre id="YLIn" data-lang="javascript">d8&gt; arr = [];
d8&gt; arr[0] = 1;
d8&gt; arr[1] = 2;
d8&gt;
d8&gt; %DebugPrint(arr); 
DebugPrint: 0x24001c9421: [JSArray]
 - map: 0x0024000ce6b1 &lt;Map[16](PACKED_SMI_ELEMENTS)&gt; [FastProperties]
 - prototype: 0x0024000ce925 &lt;JSArray[0]&gt;
 - elements: 0x0024001cb125 &lt;FixedArray[17]&gt; [PACKED_SMI_ELEMENTS]
 - length: 2
 - properties: 0x0024000006cd &lt;FixedArray[0]&gt;
 - All own properties (excluding elements): {
    0x2400000d41: [String] in ReadOnlySpace: #length: 0x00240030f6f9 &lt;AccessorInfo name= 0x002400000d41 &lt;String[6]: #length&gt;, data= 0x002400000061 &lt;undefined&gt;&gt; (const accessor descriptor), location: descriptor
 }
 - elements: 0x0024001cb125 &lt;FixedArray[17]&gt; {
           0: 1
           1: 2
        2-16: 0x0024000006e9 &lt;the_hole_value&gt;
 }
0x24000ce6b1: [Map] in OldSpace
 - map: 0x0024000c3c29 &lt;MetaMap (0x0024000c3c79 &lt;NativeContext[285]&gt;)&gt;
 - type: JS_ARRAY_TYPE
 - instance size: 16
 - inobject properties: 0
 - unused property fields: 0
 - elements kind: PACKED_SMI_ELEMENTS
 - enum length: invalid
 - back pointer: 0x002400000061 &lt;undefined&gt;
 - prototype_validity cell: 0x002400000a31 &lt;Cell value= 1&gt;
 - instance descriptors #1: 0x0024000cef3d &lt;DescriptorArray[1]&gt;
 - transitions #1: 0x0024000cef59 &lt;TransitionArray[4]&gt;Transition array #1:
     0x002400000e05 &lt;Symbol: (elements_transition_symbol)&gt;: (transition to HOLEY_SMI_ELEMENTS) -&gt; 0x0024000cef71 &lt;Map[16](HOLEY_SMI_ELEMENTS)&gt;

 - prototype: 0x0024000ce925 &lt;JSArray[0]&gt;
 - constructor: 0x0024000ce61d &lt;JSFunction Array (sfi = 0x2400335da5)&gt;
 - dependent code: 0x0024000006dd &lt;Other heap object (WEAK_ARRAY_LIST_TYPE)&gt;
 - construction counter: 0

[1, 2]</pre>
  <p id="k4fu">Как мы видим, у скрытого класса этого объекта ссылка <code>back pointer</code> пустая, что означает отсутствие родительского класса, хотя мы добавили два элемента. Дело в том, что скрытый класс любого массива всегда имеет единую форму <code>JS_ARRAY_TYPE</code>. Это особенный скрытый класс, у которого в дескрипторах есть лишь одно свойство - <code>length</code>. Элементы массива же располагаются внутри объекта в структуре <code>FixedArray</code>. В действительности скрытые классы массивов все же могут наследоваться, поскольку сами элементы могут иметь различные типы данных, а ключи, в зависимости от числа, могут храниться разными способами для оптимизации доступа к ним. В этой статье я не буду подробно рассматривать все возможные переходы внутри массивов, так как это тема для отдельной статьи. Однако стоит иметь в виду, что разнообразные нестандартные манипуляции с ключами массивов могут привести к созданию древа классов для всех или части элементов.</p>
  <pre id="zeCZ" data-lang="javascript">d8&gt; const arr = [];
d8&gt; arr[-1] = 1;
d8&gt; arr[2**32 - 1] = 2;
d8&gt;
d8&gt; %DebugPrint(arr)
DebugPrint: 0xe0b001c98c9: [JSArray]
 - map: 0x0e0b000dacc1 &lt;Map[16](PACKED_SMI_ELEMENTS)&gt; [FastProperties]
 - prototype: 0x0e0b000ce925 &lt;JSArray[0]&gt;
 - elements: 0x0e0b000006cd &lt;FixedArray[0]&gt; [PACKED_SMI_ELEMENTS]
 - length: 0
 - properties: 0x0e0b001cb5f1 &lt;PropertyArray[3]&gt;
 - All own properties (excluding elements): {
    0xe0b00000d41: [String] in ReadOnlySpace: #length: 0x0e0b0030f6f9 &lt;AccessorInfo name= 0x0e0b00000d41 &lt;String[6]: #length&gt;, data= 0x0e0b00000061 &lt;undefined&gt;&gt; (const accessor descriptor), location: descriptor
    0xe0b000dab35: [String] in OldSpace: #-1: 1 (const data field 0), location: properties[0]
    0xe0b000daca9: [String] in OldSpace: #4294967295: 2 (const data field 1), location: properties[1]
 }
0xe0b000dacc1: [Map] in OldSpace
 - map: 0x0e0b000c3c29 &lt;MetaMap (0x0e0b000c3c79 &lt;NativeContext[285]&gt;)&gt;
 - type: JS_ARRAY_TYPE
 - instance size: 16
 - inobject properties: 0
 - unused property fields: 1
 - elements kind: PACKED_SMI_ELEMENTS
 - enum length: invalid
 - stable_map
 - back pointer: 0x0e0b000dab45 &lt;Map[16](PACKED_SMI_ELEMENTS)&gt;
 - prototype_validity cell: 0x0e0b000dab95 &lt;Cell value= 0&gt;
 - instance descriptors (own) #3: 0x0e0b001cb651 &lt;DescriptorArray[3]&gt;
 - prototype: 0x0e0b000ce925 &lt;JSArray[0]&gt;
 - constructor: 0x0e0b000ce61d &lt;JSFunction Array (sfi = 0xe0b00335da5)&gt;
 - dependent code: 0x0e0b000006dd &lt;Other heap object (WEAK_ARRAY_LIST_TYPE)&gt;
 - construction counter: 0

[]</pre>
  <p id="dteD">В примере выше оба элемента <code>-1</code> и <code>2**32 - 1</code> не входят в диапазон возможных индексов массива <code>[0 .. 2**32 - 2]</code> и были объявлены как обычные свойства объекта с соответствующими формами и порождением дерева скрытых классов.</p>
  <p id="67Oj">Еще одна нештатная ситуация возможна в случае попытки изменить атрибуты индекса. Чтобы элементы хранились в быстром хранилище, все индексы должны иметь одинаковую конфигурацию. Попытка изменить атрибуты любого из индексов не приведет к созданию отдельного свойства, но приведет к изменению типа хранилища на медленное, в котором будут храниться не только значения, но и атрибуты каждого индекса. По сути, здесь применяется то же правило, что и в случае с медленными свойствами объекта.</p>
  <pre id="0sRc" data-lang="javascript">d8&gt; const arr = [1];
d8&gt; Object.defineProperty(arr, &#x27;0&#x27;, { value: 2, writable:  false });      
d8&gt; arr.push(3);
d8&gt;
d8&gt; %DebugPrint(arr);
DebugPrint: 0x29ee001c9425: [JSArray]
 - map: 0x29ee000dad05 &lt;Map[16](DICTIONARY_ELEMENTS)&gt; [FastProperties]
 - prototype: 0x29ee000ce925 &lt;JSArray[0]&gt;
 - elements: 0x29ee001cb391 &lt;NumberDictionary[16]&gt; [DICTIONARY_ELEMENTS]
 - length: 2
 - properties: 0x29ee000006cd &lt;FixedArray[0]&gt;
 - All own properties (excluding elements): {
    0x29ee00000d41: [String] in ReadOnlySpace: #length: 0x29ee0030f6f9 &lt;AccessorInfo name= 0x29ee00000d41 &lt;String[6]: #length&gt;, data= 0x29ee00000061 &lt;undefined&gt;&gt; (const accessor descriptor), location: descriptor
 }
 - elements: 0x29ee001cb391 &lt;NumberDictionary[16]&gt; {
   - requires_slow_elements
   0: 2 (data, dict_index: 0, attrs: [_EC])
   1: 3 (data, dict_index: 0, attrs: [WEC])
 }
0x29ee000dad05: [Map] in OldSpace
 - map: 0x29ee000c3c29 &lt;MetaMap (0x29ee000c3c79 &lt;NativeContext[285]&gt;)&gt;
 - type: JS_ARRAY_TYPE
 - instance size: 16
 - inobject properties: 0
 - unused property fields: 0
 - elements kind: DICTIONARY_ELEMENTS
 - enum length: invalid
 - stable_map
 - back pointer: 0x29ee000cf071 &lt;Map[16](HOLEY_ELEMENTS)&gt;
 - prototype_validity cell: 0x29ee00000a31 &lt;Cell value= 1&gt;
 - instance descriptors (own) #1: 0x29ee000cef3d &lt;DescriptorArray[1]&gt;
 - prototype: 0x29ee000ce925 &lt;JSArray[0]&gt;
 - constructor: 0x29ee000ce61d &lt;JSFunction Array (sfi = 0x29ee00335da5)&gt;
 - dependent code: 0x29ee000006dd &lt;Other heap object (WEAK_ARRAY_LIST_TYPE)&gt;
 - construction counter: 0

[2, 3]</pre>
  <h2 id="PoZs">Итог</h2>
  <p id="SgnO">В этой статье мы познакомились ближе с методами хранения свойств объектов, понятиями скрытого класса, формами объекта, дескрипторов объекта, внутренними и внешними свойствами, а также быстрым и медленным способами их хранения. Давайте теперь коротко вспомним основные тезисы и выводы.</p>
  <ul id="ybhx">
    <li id="QotV">Каждый объект в JavaScript имеет свой основной внутренний класс и скрытый класс, описывающий его форму.</li>
    <li id="sVFa">Скрытые классы наследуют друг друга и выстраивются в деревья классов. Форма объекта <code>{ a: 1 }</code> будет родительской для формы объекта <code>{ a: 1, b: 2 }</code>.</li>
    <li id="GQg3">Порядок свойств имеет значение. Объекты<code> { a: 1, b: 2 }</code> и <code>{ b: 2, a: 1 }</code> будут иметь две разные формы.</li>
    <li id="F9hk">Класс-наследник хранит ссылку на класс-родитель и информацию о том, что было изменено (<strong>переход</strong>).</li>
    <li id="0IRo">В дереве классов каждого объекта количество уровней не менее количества свойств в объекте.</li>
    <li id="x0iM">Самыми быстрыми свойствами объекта будут те, которые объявлены при инициализации. В следующем примере доступ к свойству <code>obj1.a</code> будет быстрее, чем к <code>obj2.a</code>.</li>
  </ul>
  <pre id="gKYP" data-lang="javascript">const obj1 = { a: undefined };
obj1.a = 1; // &lt;- &quot;a&quot; - in-object свойство

const obj2 = {};
obj2.a = 1; // &lt;- &quot;a&quot; - внешнее свойство</pre>
  <ul id="HAQq">
    <li id="pL1y">Нетипичные изменения формы объекта, такие как удаление свойства, могут привести к изменению типа хранения свойств на медленный. В следующем примере <code>obj1</code> изменит свой тип на <code>NamedDictionary</code>, и доступ к его свойствам будет значительно медленнее, чем к свойствам <code>obj2</code>. </li>
  </ul>
  <pre id="T1oj" data-lang="javascript">const obj1 = { a: 1, b: 2 };
delete obj1.a; // изменяет тип хранения на NameDictionary 

const obj2 = { a: 1, b: 2 };
obj2.a = undefined; // не меняет тип хранения свойств</pre>
  <ul id="r1uV">
    <li id="JzuS">Если в объекте есть внешние свойства, а внутренних меньше 4-х, такой объект можно немного оптимизировать, так как пустой объект, по умолчанию имеет несколько слотов для <code>in-object</code> свойств.</li>
  </ul>
  <pre id="W3ar" data-lang="javascript">const obj1 = { a: 1 };
obj1.b = 2;
obj1.c = 3;
obj1.d = 4;
obj1.e = 5;
obj1.f = 6;

%DebugPrint(obj1);
...
- All own properties (excluding elements): {
    ...#a: 1 (const data field 0), location: in-object
    ...#b: 2 (const data field 1), location: properties[0]
    ...#c: 3 (const data field 2), location: properties[1]
    ...#d: 4 (const data field 3), location: properties[2]
    ...#e: 5 (const data field 4), location: properties[3]
    ...#f: 6 (const data field 5), location: properties[4]
 }

const obj2 = Object.fromEntries(Object.entries(obj1));

%DebugPrint(obj2);
...
 - All own properties (excluding elements): {
    ...#a: 1 (const data field 0), location: in-object
    ...#b: 2 (const data field 1), location: in-object
    ...#c: 3 (const data field 2), location: in-object
    ...#d: 4 (const data field 3), location: in-object
    ...#e: 5 (const data field 4), location: properties[0]
    ...#f: 6 (const data field 5), location: properties[1]
 }</pre>
  <ul id="B76Z">
    <li id="0IGy">Массив является обычным классом, форма которого имеет вид <code>{ length: [W__] }</code>. Элементы массива хранятся в специальных структурах, ссылки на которые размещены внутри объекта. Добавление и удаление элементов массива не приводят к увеличению дерева классов.</li>
  </ul>
  <pre id="u5Kh" data-lang="javascript">const arr = [];
arr[0] = 1; // новый элемент массива не увеличивает дерево классов

const obj = {};
obj1[0] = 1; // каждое новое свойство объекта увеличивает дерево классов</pre>
  <ul id="sSku">
    <li id="x6Dq">Использование нетипичных ключей в массиве, например не числовых или вне диапазона <code>[0 .. 2**32 - 2]</code>), приводит к созданию новых форм в дереве классов.</li>
  </ul>
  <pre id="RGCs" data-lang="javascript">const arr = [];
arr[-1] = 1;
arr[2**32 - 1] = 2;
// Приведет к образованию дерева форм
// { length } =&gt; { length, [-1] } =&gt; { length, [-1], [2**32 - 1] }</pre>
  <ul id="AkfU">
    <li id="ioLk">Попытка изменить атрибут элемента массива приведет к смене типа хранилища на медленный.</li>
  </ul>
  <pre id="uqRG" data-lang="javascript">const arr = [1, 2, 3];
// { elements: {
//   #0: 1,
//   #1: 2,
//   #2: 3
// }}

Object.defineProperty(arr, &#x27;0&#x27;, { writable: false };
// { elements: {
//   #0: { value: 1, attrs: [_EC] }, 
//   #1: { value: 2, attrs: [WEC] },
//   #2: { value: 3, attrs: [WEC] }
// }}</pre>
  <p id="mA9K"></p>
  <hr />
  <p id="SzI0"></p>
  <p id="7pWR"><strong>Мои телеграмм-каналы:</strong></p>
  <section style="background-color:hsl(hsl(55,  86%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="YnAq">EN - <a href="https://t.me/frontend_almanac" target="_blank">https://t.me/frontend_almanac</a><br />RU - <a href="https://t.me/frontend_almanac_ru" target="_blank">https://t.me/frontend_almanac_ru</a></p>
  </section>
  <p id="Ri5D"><em>English version: <a href="https://blog.frontend-almanac.com/js-object-structure" target="_blank">https://blog.frontend-almanac.com/js-object-structure</a></em></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.frontend-almanac.ru/react-state-management</guid><link>https://blog.frontend-almanac.ru/react-state-management?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=frontend_almanac_ru</link><comments>https://blog.frontend-almanac.ru/react-state-management?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=frontend_almanac_ru#comments</comments><dc:creator>frontend_almanac_ru</dc:creator><title>Управление состоянием React приложения</title><pubDate>Tue, 19 Mar 2024 19:09:14 GMT</pubDate><description><![CDATA[Вопрос управления состоянием в React-приложениях всегда был очень актуален. Сам React, в момент первого выхода в свет, не предлагал никаких комплексных подходов к управлению состоянием и оставлял эту задачу на совести разработчиков. Из технических средств были доступны классические свойства компонентов и внутренний стейт компонентов. Стандартная базовая концепция была крайне примитивной и предполагала хранение данных в стейте компонента. Так, глобальные данные могли храниться в стейте самого верхнего компонента, а более специфичные - в стейтах нижних компонентов. Единственным штатным способом обмена данными между компонентами было использование свойств компонентов.]]></description><content:encoded><![CDATA[
  <p id="Vzeo">Вопрос управления состоянием в React-приложениях всегда был очень актуален. Сам React, в момент первого выхода в свет, не предлагал никаких комплексных подходов к управлению состоянием и оставлял эту задачу на совести разработчиков. Из технических средств были доступны классические свойства компонентов и внутренний стейт компонентов. Стандартная базовая концепция была крайне примитивной и предполагала хранение данных в стейте компонента. Так, глобальные данные могли храниться в стейте самого верхнего компонента, а более специфичные - в стейтах нижних компонентов. Единственным штатным способом обмена данными между компонентами было использование свойств компонентов.</p>
  <h2 id="bhWm">Prop-drilling</h2>
  <p id="qZtD">Типичный подход выглядел примерно следующим образом.</p>
  <pre id="f9kQ" data-lang="javascript">class App extends React.Component {
  constructor(props) {
    super(props);

    // инциализация стейта значениями по умолчанию
    this.state = {
      a: &#x27;&#x27;,
      b: 0,
    };
  }

  render() {
    // проброс данных из стейта в дочерний компонент
    return &lt;Child a={this.state.a} b={this.state.b} /&gt;;
  }
}

class Child extends React.Component&lt;any, any&gt; {
  // дочерний компонент получает данные через ссылку this.props
  render() {
    return (
      &lt;div&gt;
        &lt;div&gt;{this.props.a}&lt;/div&gt;
        &lt;div&gt;{this.props.b}&lt;/div&gt;
      &lt;/div&gt;
    );
  }
}

ReactDOM.render(&lt;App /&gt;, document.getElementById(&#x27;root&#x27;));</pre>
  <p id="apuo">В примере выше данные хранятся в стейте верхнего компонента и передаются в дочерний компонент через свойства. Дочерний компонент также может иметь свой внутренний стейт.</p>
  <pre id="WgcH" data-lang="javascript">class Child extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      c: 2,
    };
  }

  render() {
    return (
      &lt;div&gt;
        &lt;div&gt;{this.props.a}&lt;/div&gt;
        &lt;div&gt;{this.props.b}&lt;/div&gt;
        &lt;Child2 c={this.state.c} /&gt;
      &lt;/div&gt;
    );
  }
}

class Child2 extends React.Component {
  render() {
    return (
      &lt;div&gt;
        &lt;div&gt;{this.props.c}&lt;/div&gt;
      &lt;/div&gt;
    );
  }
}</pre>
  <p id="J0Q1">Изменение состояния компонента в таком случае возможно только посредством вызова метода <code>this.setState</code> внутри компонента, то есть, дочерний компонент может обновить верхнеуровневые данные только при наличии соответствующего метода, переданного ему через те же свойства.</p>
  <pre id="WUwC" data-lang="javascript">class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      a: &#x27;&#x27;,
      b: 0,
    };
  }

  render() {
    return (
      &lt;Child
        a={this.state.a}
        b={this.state.b}
        setB={(b) =&gt; {
          this.setState({ b });
        }}
      /&gt;
    );
  }
}

class Child extends React.Component&lt;any, any&gt; {
  constructor(props) {
    super(props);

    this.state = {
      c: 2,
    };
  }

  render() {
    return (
      &lt;div&gt;
        &lt;div&gt;{this.props.a}&lt;/div&gt;
        &lt;div&gt;{this.props.b}&lt;/div&gt;
        &lt;Child2
          b={this.state.b}
          c={this.state.c}
          setB={this.props.setB}
        /&gt;
      &lt;/div&gt;
    );
  }
}

class Child2 extends React.Component {
  render() {
    return (
      &lt;div&gt;
        &lt;div&gt;{this.props.c}&lt;/div&gt;

        &lt;button
          onClick={() =&gt; {
            this.props.setB(this.props.b + 1);
          }}
        &gt;
          Increment
        &lt;/button&gt;
      &lt;/div&gt;
    );
  }
}</pre>
  <p id="xInu">Уже на этом этапе проблема данного подхода становится очевидной. В реальных приложениях с большим набором компонентов эта схема приводит к так называемому <strong>prop-drilling</strong>, то есть массовому пробросу ссылок вниз по дереву компонентов и обратно наверх. Это неизбежно приводит:</p>
  <p id="vV6n">а) к гонке обновления данных, то есть ситуация, когда два компонента, не зная друг о друге, пытаются обновить одну и ту же ссылку в стейте;</p>
  <p id="eACy">б) к тому, что компоненты должны выполнять транзитную роль для данных, которыми, фактически, не оперируют;</p>
  <p id="N7j0">в) к сложности замены компонента в середине цепочки;</p>
  <p id="4iQ7">г) к длинному и запутанному графу ссылок на состояние.</p>
  <p id="qxCL">Дополнительно, задачу осложняло еще и то, что простые функциональные компоненты в первых версиях React не имели внутреннего стейта (хуков тогда еще не было).</p>
  <pre id="n70Y" data-lang="javascript">const Child3 = ({ c }) =&gt; {
  // У функционального компонента нет this.state
  // хук useState() появился только в 2019-м году с версии 16.8.0
  return &lt;div&gt;{c}&lt;/div&gt;;
};</pre>
  <h2 id="5iKR">Flux</h2>
  <p id="yoSv">Надо ли говорить, что показанный выше подход вел к большому количеству ошибок и экспоненциально нарастающей сложности кода? Не могли избежать этого и сами создатели React. В былые годы на сайте &quot;Facebook&quot; то и дело появлялись ошибки, связанные с различными асинхронными процессами, такими как исчезающие или не отображающиеся уведомления и другие. В связи с этим разработчики принялись за проработку нового подхода к организации управления состоянием данных в приложении. Так появился паттерн <a href="https://ru.wikipedia.org/wiki/Flux-%D0%B0%D1%80%D1%85%D0%B8%D1%82%D0%B5%D0%BA%D1%82%D1%83%D1%80%D0%B0" target="_blank">Flux</a>. Прародителем этого паттерна стал классический и очень популярный в те времена паттерн <a href="https://ru.wikipedia.org/wiki/Model-View-Controller" target="_blank">MVC</a>. Суть Flux заключается в том, что данные всегда должны двигаться в одном направлении и храниться в отдельном крупном хранилище. Обновление данных осуществляется с помощью специальных методов-экшенов, не привязанных к какому-либо конкретному компоненту.</p>
  <p id="VhC2">На основе паттерна Flux появилось много библиотек для управления состоянием. Такие как <strong>Fluxxor</strong>, <strong>Flummox</strong>, <strong>Baobab</strong> и еще много других. Самой популярной из них, на протяжении многих лет и по сей день, является <strong>Redux</strong>.</p>
  <h2 id="ap6o">Redux</h2>
  <p id="hiu3">Библиотека управления состоянием приложения <strong>Redux</strong> появилась в 2015 году и с тех пор приобрела огромную популярность. Redux доказал эффективность подхода Flux и до недавнего времени фактически являлся стандартом в мире React.</p>
  <p id="ac9B">Архитектура типичного приложения обрела следующий вид.</p>
  <pre id="yx9K" data-lang="javascript">import { connect, Provider } from &#x27;react-redux&#x27;;

const defaultState1 = {
  a: &quot;&quot;,
  b: 0
};

const reducer1 = function(state = defaultState1, action) {
  switch(action.type){
    case &quot;A_TYPE&quot;:
      return { ...state, a: action.payload };
    case &quot;B_TYPE&quot;:
      return { ...state, b: action.payload };
  }

  return state;
};

const defaultState2 = {
  c: false,
  d: []
};

const reducer2 = function(state = defaultState2, action) {
  switch(action.type){
    case &quot;C_TYPE&quot;:
      return { ...state, c: action.payload };
    case &quot;D_TYPE&quot;:
      return { ...state, c: action.payload };
  }

  return state;
};

const store = createStore(
  combineReducers({
    reducer1,
    reducer2
  })
);

const App = connect(
  (state) =&gt; ({ b: state.reducer1.b }),
  (dispatch) =&gt; {
    setB: (value) =&gt; dispatch({ type: &quot;B_TYPE&quot;, payload: value }) 
  }
)(({ b }) =&gt; {
  return (
    &lt;Provider store={store}&gt;
      &lt;button
        onClick={() =&gt; {
          setB(b + 1)
        }}
      &gt;
        Increase {b}
      &lt;/button&gt;
    &lt;/Provider&gt;
  );
});</pre>
  <p id="h34l">Теперь все данные в приложении хранятся в отдельном большом дереве за пределами какого-либо компонента. К дереву можно «подключиться», т.е. слушать все изменения нужной ветки и реагировать на эти изменения. Обновить значение в дереве можно, послав так называемый экшен - событие, содержащее объект с полем <code>type</code>. По этому type редьюсер поймет, что это за экшен и выполнит соответствующую мутацию дерева.</p>
  <p id="PYZQ">В целом, вопрос, казалось бы, решен. Данные живут отдельно, компоненты отдельно. Никаких гонок и проп-дриллинга. Цена этому, правда, громоздкие конструкции по обеспечению жизнедеятельности самого хранилища. На каждое свойство дерева нужен редьюсер и экшен. А компонент необходимо подписывать на изменение всего дерева, чтобы получить данные. Кроме того, в более сложных приложениях, по мере роста дерева, начинают возникать специфические кейсы. Например, в глубоко вложенных объектах становится сложнее следить за иммутабельностью, что приводит к несрабатывающим реакциям на изменения стейта. На помощь пришли дополнительные библиотеки, такие как <strong>immutable-js</strong> и <strong>reselect</strong>.</p>
  <p id="JB2X">Потом встал вопрос работы с запросами, ведь именно запросы в большинстве случаев являются источником данных в приложении. А значит, появилась необходимость добавить асинхронность в редюсеры. Так появились <strong>redux-thunk</strong> и <strong>redux-saga</strong>. Однако и этого было недостаточно.</p>
  <p id="4MzV">Запросы, помимо данных, имеют еще и свое собственное состояние. Среднестатистическому приложению, как минимум, требуется знать, находится ли запрос еще в процессе или ответ уже получен. Традиционно, флаг выполнения запроса (обычно его называют &quot;isFetching&quot; или &quot;isLoading&quot;) лежал тут же в дереве, что требовало на каждый запрос держать один и тот же механизм выставления этого флага. Вместе с флагом загрузки в такой же ситуации находилась и обработка ошибок запроса. Текст ошибки также хранился в дереве рядом с данными и флагом загрузки. Если копнуть еще глубже, продвинутые приложения в целях оптимизации захотели кэшировать ответы на запросы, чтобы не перезапрашивать данные по сети каждый раз, когда, к примеру, монтируется компонент. Все эти рутиные операции и оптимизации еще больше раздували и без того не маленький код, обслуживающий стейт-дерево. Поэтому появление таких библиотек, как, например, <strong>redux-toolkit</strong> (RTK) выглядит вполне логично. Библиотека RTK позволяет организовать работу со стейт-деревом в одном общем паттерне. А в сочетании с дополнительным <strong>rtk-query</strong> можно получить кэширование запросов и информацию об их состоянии из коробки. Правда, от экшенов и редюсеров избавиться таким образом все равно не получится.</p>
  <h2 id="qVXH">useReducer</h2>
  <p id="7FFy">Redux и прочие Flux-подобные библиотеки являются сторонними разработками. Конечно же, команда React не осталась в стороне и добавила в API возможность работать в стиле Flux. Фактически, это выразилось в появлении хука <strong>useReducer</strong> в версии React <a href="https://github.com/facebook/react/releases/tag/v16.8.0" target="_blank">16.8.0</a>.</p>
  <p id="85MW">Тот же пример, что был приведен выше, теперь можно оформить следующим образом без подключения Redux.</p>
  <pre id="oP4T" data-lang="javascript">const reducer = (state, action) =&gt; {
  switch (action.type) {
    case &#x27;A_TYPE&#x27;:
      return { ...state, a: action.payload };
    case &#x27;B_TYPE&#x27;:
      return { ...state, b: action.payload };

    default:
      return state;
  }
};

export const App = () =&gt; {
  const [state, dispatch] = useReducer(reducer, { a: &quot;&quot;, b: 0 });

  return (
    &lt;button
      onClick={() =&gt; {
        dispatch({ type: &quot;A_TYPE&quot;, payload: b + 1 })
      }}
    &gt;
      Increase {state.b}
    &lt;/button&gt;
  );
};</pre>
  <p id="S27k">Однако все перечисленные ранее вопросы, связанные с работой Redux, справедливы и для useReducer. А поскольку Redux оброс массой дополнительных библиотек и инструментов, хук не получил большой популярности среди разработчиков.</p>
  <h2 id="tVWp">React контекст</h2>
  <p id="N5JV">Контекст в React существовал с самых первых версий. Однако официально он был включен в API только начиная с версии <a href="https://github.com/facebook/react/releases/tag/v16.3.0" target="_blank">16.3.0</a> в 2018 году. Суть подхода заключается в следующем: данные, которые требуется сделать доступными для нескольких компонентов, помещаются в отдельное независимое место посредством метода API <code>createContext()</code>.</p>
  <pre id="olfJ" data-lang="javascript">const MyContext = createContext({ a: &quot;&quot;, b: 0 });</pre>
  <p id="G6BX">Результатом метода <code>createContext</code> является объект, содержащий ссылки на провайдер и консьюмер созданного контекста.</p>
  <p id="i9yo">Провайдер должен оборачивать компонент или дерево компонентов, которые будут иметь доступ к данным внутри этого контекста.</p>
  <pre id="jh8R" data-lang="javascript">class App extends React.Component&lt;any, any&gt; {
  render() {
    return (
      &lt;MyContext.Provider value={{ a: &#x27;&#x27;, b: 0 }}&gt;
        &lt;Child /&gt;
      &lt;/MyContext.Provider&gt;
    );
  }
}

class Child extends React.Component&lt;any, any&gt; {
  contextType = MyContext;

  render() {
    return (
      &lt;div&gt;
        &lt;div&gt;{this.context.a}&lt;/div&gt;
        &lt;Child2 /&gt;
      &lt;/div&gt;
    );
  }
}

const Child2 = () =&gt; {
  return (
    &lt;MyContext.Consumer&gt;
      {(context) =&gt; &lt;div&gt;{context.b}&lt;/div&gt;}
    &lt;/MyContext.Consumer&gt;
  );
};</pre>
  <p id="fx7O">Доступ к контексту осуществляется через ссылку-консьюмер. В классовом компоненте имеется возможность привязать контекст к компоненту, определив свойство <code>contextType</code>. В функциональных компонентах можно воспользоваться прямой ссылкой на консьюмер <code>MyContext.Consumer</code>.</p>
  <p id="UDoi">С появлением официальной поддержки API контекста для функциональных компонентов стал доступен хук <code>useContext</code>.</p>
  <pre id="szFb" data-lang="javascript">const Child2 = () =&gt; {
  const { b } = useContext(MyContext);

  return (
    &lt;div&gt;{b}&lt;/div&gt;
  );
};</pre>
  <p id="42Wr">За обновлением данных в контексте отвечает компонент, создавший его.</p>
  <pre id="LdfT" data-lang="javascript">class App extends React.Component&lt;any, any&gt; {
  const [a, setA] = useState(&quot;&quot;);
  const [b, setB] = useState(0);

  render() {
    return (
      &lt;MyContext.Provider value={{ a, b }}&gt;
        &lt;Child /&gt;
        &lt;button onClick={() =&gt; setB(b =&gt; b + 1)}&gt;
          Increase {b}
        &lt;/button&gt;
      &lt;/MyContext.Provider&gt;
    );
  }
}</pre>
  <p id="jYp9">Если требуется делегировать возможность обновления данных нижестоящим компонентам, ссылку на экшен можно положить туда же, в сам контекст.</p>
  <pre id="dtip" data-lang="javascript">class App extends React.Component&lt;any, any&gt; {
  const [a, setA] = useState(&quot;&quot;);
  const [b, setB] = useState(0);

  render() {
    return (
      &lt;MyContext.Provider value={{ a, b, setB }}&gt;
        &lt;Child /&gt;
      &lt;/MyContext.Provider&gt;
    );
  }
}

const Child = () =&gt; {
  const { b, setB } = useContext(MyContext);

  return (
    &lt;button onClick={() =&gt; setB(b =&gt; b + 1)}&gt;
      Increase {b}
    &lt;/button&gt;
  );
};</pre>
  <p id="87xF">Так можно контролировать процесс мутации контекста и давать возможность изменять только те его части, которые нужно. Более того, можно добавить промежуточные действия, подобные &quot;Redux middleware&quot;, и иметь еще больший контроль над мутациями.</p>
  <pre id="Ik63" data-lang="javascript">class App extends React.Component&lt;any, any&gt; {
  const [a, setA] = useState(&quot;&quot;);
  const [b, setB] = useState(0);
  
  const increaseB = () =&gt; {
    setB((b) =&gt; {
      return b &lt; 10 ? b + 1 : b;
    });
  };

  render() {
    return (
      &lt;MyContext.Provider value={{ a, b, increaseB }}&gt;
        &lt;Child /&gt;
      &lt;/MyContext.Provider&gt;
    );
  }
}

const Child = () =&gt; {
  const { b, increaseB } = useContext(MyContext);

  return (
    &lt;button onClick={increaseB}&gt;
      Increase {b}
    &lt;/button&gt;
  );
};</pre>
  <p id="nXq8">К преимуществам контекста можно отнести</p>
  <h3 id="x8hs">Гибкость</h3>
  <p id="vhCU">Хранилище контекста максимально примитивно, а API состоит из простых методов-обёрток. Следовательно, разработчик имеет максимальный контроль на всех этапах проектирования контекста.</p>
  <h3 id="s0TQ">Селективность</h3>
  <p id="diui">В отличие от Redux с его глобальным деревом, каждый конкретный контекст имеет возможность обернуть только нужные компоненты. И наоборот, компонент может быть обернут сразу в несколько контекстов. Это позволяет контролировать, какие данные и где будут доступны.</p>
  <h3 id="eFAx">Уникальность</h3>
  <p id="gdlS">В ряде случаев может возникнуть необходимость продублировать модель контекста. Такое может понадобиться, например, при работе с массивами.</p>
  <pre id="279K" data-lang="javascript">import { connect, Provider } from &#x27;react-redux&#x27;;

const defaultState = {
  items: [
    { id: 1, name: &quot;item 1&quot; },
    { id: 2, name: &quot;item 2&quot; },
    { id: 3, name: &quot;item 3&quot; },
  ],
  selectedId: undefined,
};

const reducer = function(state = defaultState, action) {
  switch(action.type){
    case &quot;SET_SELECTED_ID_TYPE&quot;:
      return {
        ...state,
        selectedId: action.id,
      };
    default:
      return state;
  }

  return state;
};

const store = createStore(reducer);

const SelectedItem = connect(
  (state) =&gt; ({ item: state.reducer.items.find(item =&gt; item.id === state.selectedId) }),
)(({ item }) =&gt; {
  return item ? null : (
    &lt;div&gt;
      Seleceted item: {item?.title}
    &lt;/div&gt;
  );
});

const ItemsList = connect(
  (state) =&gt; ({
    items: state.reducer.items,
  }),
  (dispatch = {
    setItem: (id) =&gt; dispatch({ type: &#x27;SET_SELECTED_ID_TYPE&#x27;, id }),
  })
)(({ items, setItem }) =&gt; {
  return (
    &lt;div&gt;
      {items.map((item) =&gt; (
        &lt;button key={item.id} onClick={() =&gt; setItem(item.id)}&gt;
          {item.title}
        &lt;/button&gt;
      ))}

      &lt;SelectedItem /&gt;
    &lt;/div&gt;
  );
});</pre>
  <p id="fTJb">В данном примере все, кажется, хорошо, пока компонент <code>ItemsList</code> смонтирован. Однако, если мы его размонтируем, например, изменим маршрут, а затем вернемся к нему снова, в дереве Redux останется старое значение <code>selectedId</code>. В случае, если мы ожидаем увидеть здесь значение по умолчанию, а не старое, придется заботиться об этом отдельно и дописывать соответствующую логику.</p>
  <p id="fFq4">Данный пример довольно простой. На практике встречаются гораздо более сложные случаи, в которых уже не обойтись простым ресет-экшеном.</p>
  <p id="TDDi">В случае с контекстом архитектура могла бы выглядеть, например, так.</p>
  <pre id="DuYX" data-lang="javascript">const ItemContext = createContext();

const SelectedItem: FC = ({ title }) =&gt; {
  const { title } = useContext(ItemContext);

  return &lt;div&gt;Selected item: {title}&lt;/div&gt;;
};

const ItemsList: FC = () =&gt; {
  const [items] = useState([
    { id: 1, name: &#x27;item 1&#x27; },
    { id: 2, name: &#x27;item 2&#x27; },
    { id: 3, name: &#x27;item 3&#x27; },
  ]);

  const [selectedItemId, setSelectedItemId] = useState();

  const item = useMemo(
    () =&gt; items.find((item) =&gt; item.id === selectedItemId),
    [items, selectedItemId]
  );

  return (
    &lt;div&gt;
      {items.map((item) =&gt; (
        &lt;button key={item.id} onClick={() =&gt; setSelectedItemId(item.id)}&gt;
          {item.title}
        &lt;/button&gt;
      ))}

      {item &amp;&amp; (
        &lt;ItemContext.Provider value={item}&gt;
          &lt;SelectedItem /&gt;
        &lt;/ItemContext.Provider&gt;
      )}
    &lt;/div&gt;
  );
};</pre>
  <p id="XGYn">Поскольку данные живут внутри компонента <code>ItemsList</code>, при его перемонтировании стейт всегда будет иметь исходный вид, и нам не надо дополнительно беспокоиться о его сбросе.</p>
  <p id="fQEv">В то же время, компонент <code>SelectedItem</code> оперирует контекстом <code>ItemContext</code> и вообще не знает, откуда и как в него попали данные.</p>
  <h2 id="z56W">Универсальный паттерн применения React контекста</h2>
  <p id="fqnV">Ниже я представлю универсальный шаблон типичного использования контекста в реальном приложении. Как и Redux, контексты хорошо дружат с TypeScript, поэтому сразу приведу пример с типизацией.</p>
  <p id="fz0m">Для начала создадим независимый контекст и отдельный компонент-провайдер для него.</p>
  <pre id="N4oH" data-lang="typescript">// MyContext.tsx
import React, {
  createContext,
  Dispatch,
  FC,
  PropsWithChildren,
  SetStateAction,
  useCallback,
  useContext,
  useMemo,
  useState,
} from &#x27;react&#x27;;

export interface MyContextProps {
  myVar: string;
  setMyVar: Dispatch&lt;SetStateAction&lt;MyContextProps[&#x27;myVar&#x27;]&gt;&gt;;

  someAction: (a: number, b?: boolean) =&gt; void;
}

export const MyContext = createContext&lt;MyContextProps&gt;({
  myVar: &#x27;&#x27;,
  setMyVar: () =&gt; {},

  someAction: () =&gt; {},
});

// Вместо WithMyContext можно использовать более традиционное
// название MyContextProvder
export const WithMyContext: FC&lt;PropsWithChildren&gt; = ({ children }) =&gt; {
  const [myVar, setMyVar] = useState&lt;MyContextProps[&#x27;myVar&#x27;]&gt;(&#x27;&#x27;);

  const someAction = useCallback&lt;MyContextProps[&#x27;someAction&#x27;]&gt;(
    (a, b) =&gt; {},
    []
  );

  const value = useMemo&lt;MyContextProps&gt;(
    () =&gt; ({
      myVar,
      setMyVar,

      someAction,
    }),
    [myVar, someAction]
  );

  return &lt;MyContext.Provider value={value}&gt;{children}&lt;/MyContext.Provider&gt;;
};

// Создадим, также отдельный хук для удобства
export const useMyContext = () =&gt; {
  return useContext(MyContext);
};</pre>
  <p id="f3XI">Контекст готов, осталось обернуть в него нужный компонент.</p>
  <pre id="fXql" data-lang="javascript">&lt;WithMyContext&gt;
  &lt;MyComponent /&gt;
&lt;/WithMyContext&gt;</pre>
  <p id="4Mdj">Или, например, конкретный роут &quot;react-router&quot;</p>
  <pre id="zrpl" data-lang="javascript">&lt;Route element={(
  &lt;WithMyContext&gt;
    &lt;Outlet /&gt;
  &lt;/WithMyContext&gt;
)}&gt;
  &lt;Route …&gt;
&lt;/Route&gt;</pre>
  <p id="tEGV">Теперь контекст доступен для использования внутри вложенных компонентов.</p>
  <pre id="lrLy" data-lang="typescript">import { FC } from &#x27;react&#x27;;

import { useMyContext } from ‘./MyContext&#x27;;

export const MyComponent: FC = () =&gt; {
  const { myVar } = useMyContext();

  return &lt;div&gt;My component {myVar}&lt;/div&gt;;
};</pre>
  <p id="FLeK"></p>
  <hr />
  <p id="5Lqd"></p>
  <p id="7pWR"><strong>Мои телеграмм-каналы:</strong></p>
  <section style="background-color:hsl(hsl(55,  86%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="YnAq">EN - <a href="https://t.me/frontend_almanac" target="_blank">https://t.me/frontend_almanac</a><br />RU - <a href="https://t.me/frontend_almanac_ru" target="_blank">https://t.me/frontend_almanac_ru</a></p>
  </section>
  <p id="Ri5D"><em>English version: <a href="https://blog.frontend-almanac.com/react-state-management" target="_blank">https://blog.frontend-almanac.com/react-state-management</a></em></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.frontend-almanac.ru/v8-garbage-collection</guid><link>https://blog.frontend-almanac.ru/v8-garbage-collection?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=frontend_almanac_ru</link><comments>https://blog.frontend-almanac.ru/v8-garbage-collection?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=frontend_almanac_ru#comments</comments><dc:creator>frontend_almanac_ru</dc:creator><title>Сборка мусора в V8</title><pubDate>Thu, 07 Mar 2024 17:36:29 GMT</pubDate><media:content medium="image" url="https://img2.teletype.in/files/9c/ad/9cad7d4a-930c-43e3-a98a-0203fd62a3f1.png"></media:content><description><![CDATA[<img src="https://img2.teletype.in/files/97/89/97896713-401b-4757-a4e9-c53cea7570c1.png"></img>В этой статье мы детально разберем процесс сборки мусора движком V8. Познакомимся с понятиями поколений, Minor и Major Garbage Collection, посмотрим, как аллоцируются, трассируются и маркируются объекты в памяти. Что происходит с пустыми областями после очистки, и как выполняется сборка мусора в фоновом режиме.]]></description><content:encoded><![CDATA[
  <p id="uI1W">В этой статье мы детально разберем процесс сборки мусора движком V8. Познакомимся с понятиями поколений, Minor и Major Garbage Collection, посмотрим, как аллоцируются, трассируются и маркируются объекты в памяти. Что происходит с пустыми областями после очистки и как выполняется сборка мусора в фоновом режиме. </p>
  <h2 id="wQlK">Что собирается сборщиком мусора</h2>
  <p id="1hvg">Прежде чем погрузиться в процесс самой сборки мусора, давайте сначала разберемся, что именно V8 собирает и как трассирует объекты.</p>
  <p id="7plF">Во время работы V8 в памяти может находиться множество разных структур данных. Часть этих структур является внутренними представлениями самого V8, такими как скрытые классы (<strong>hidden classes</strong>), списки, очереди и многое другое. Другая часть - это объекты JavaScript. Практически все типы данных внутри V8, по факту являются объектами, за исключением так называемых SMI - маленьких чисел не более <code>2^30 - 1</code>, они хранятся в памяти прямым бинарным значением, а все переменные, имеющее такое значение, хранят ссылку на область памяти с этим конкретным SMI-значением. Подробнее про это я писал в статье <a href="https://blog.frontend-almanac.ru/5688-ygxnVD" target="_blank">Глубокий JS. В память о типах и данных</a>. Все остальные типы представлены в памяти в виде объектов и имитируют свою мутабельность/иммутабельность в соответствии со спецификацией <a href="http://262.ecma-international.org" target="_blank">ECMA-262</a>, выдавая, как мы привыкли говорить, &quot;примитивные&quot; или &quot;ссылочные&quot; типы.</p>
  <p id="eAMf">Вообще, процесс сборки мусора не ограничивается объектами JavaScript. Движок Chromium предусматривает сборку всего, что может вообще быть собрано, будь то удаленные элементы HTML, не используемые таблицы стилей CSS и ресурсы, завершенная анимация и многое другое. Так как Chromium написан на языке C++, его внутренние объекты движка являются, очевидно, классами C++. Однако, объекты JavaScript, хоть и созданы средствами того же языка, являются более абстрактными понятиями и должны реализовывать требования спецификации <a href="http://ECMA-262" target="_blank">ECMA-262</a>. В частности, объекты JavaScript не имеют фиксированного размера и могу меняться динамически. А куча JavaScript, по своей структуре отличается от кучи C++. Как такие объекты размещаются в памяти посмотрим чуть ниже. Сейчас важно понять, что процесс сборки мертвых объектов JavaScript будет несколько расходиться со сборкой объектов C++.</p>
  <p id="iDSl">Несколько лет назад, приблизительно в 2018-м году команда Chromium начала создавать систему сборки мертвых объектов C++. Система получила название Oilpan и стала частью внутреннего движка рендеринга Blink. Позже, в 2020-м, разработчики решили перенести систему внутрь движка V8, так как, с одной стороны, V8 использует те же механизмы сборки мусора, что и Blink, с другой, перенос в V8 делает систему доступной для сторонних разработчиков, которые интегрируют движок V8, но не используют Blink, например, Node.js. В результате, в API V8 появилась директория <a href="https://chromium.googlesource.com/v8/v8/+/refs/heads/main/include/cppgc/README.md" target="_blank">cppgc</a>.</p>
  <h2 id="jFJo">Трассировка объектов</h2>
  <p id="cqlY">Как я уже говорил, суть сборки мусора заключается в освобождении памяти от &quot;мертвых&quot; объектов. Мертвым считается объект, на который нет больше ни одной ссылки, а значит, к нему невозможно получить доступ. Следовательно, такой объект больше не может быть использован и его следует очистить. И наоборот, &quot;живой&quot; объект - это объект, до которого можно добраться по ссылкам от корня.</p>
  <figure id="B3A5" class="m_column">
    <img src="https://img2.teletype.in/files/97/89/97896713-401b-4757-a4e9-c53cea7570c1.png" width="1400" />
  </figure>
  <p id="wJOY">На рисунке выше изображена упрощенная схема трассировки. Ссылки на все созданные объекты хранятся в отдельной глобальной таблице <strong>GlobalGCInfoTable</strong>. Во время работы сборщика мусора, происходит итеративный проход объектов от корня (корней может быть несколько, проход осуществляется по всем активным корням). Объекты, до которых смог дотянуться сборщик мусора, будут считаться живыми, они обозначены черным цветом. Все остальные объекты являются &quot;мертвыми&quot; (обозначены белым цветом).</p>
  <h3 id="1Hre">Трассировка объектов C++</h3>
  <p id="szKQ">Чуть выше я говорил, то объекты C++ отличаются от объектов JavaScript. Дело в том, что объекты JavaScript стандартизированы и описаны в разделе <a href="https://262.ecma-international.org/#sec-object-type" target="_blank">6.1.7 The Object Type</a> спецификации. Что делает их предсказуемыми для сборщика мусора. Объекты же C++ не стандартизированы и их структуры могут сильно отличаться. Более того, не каждый объект C++ должен быть потенциально очищаемый. В связи с этим, для объектов C++ в Oilpan был предусмотрен специальный API.</p>
  <p id="SpaO">В блоге движка V8 этому был посвящен отдельный пост <a href="https://v8.dev/blog/high-performance-cpp-gc" target="_blank">High-performance garbage collection for C++</a>. Не буду пересказывать весь пост, обозначу только основную концепцию. Объекты C++, которые хотят быть очищаемыми сборщиком мусоры, должны реализовывать интерфейсы <strong>GarbageCollected</strong> или <strong>GarbageCollectedMixin</strong>. Фактически, это означает, что объект должен иметь метод <strong>Trace()</strong>, в котором, сам же этот объект должен вызвать трассировку других объектов, на которые ссылается.</p>
  <p id="0H5T">Например, вот так выглядит трассировка класса <strong>RunningAnimation </strong>(на момент написания статьи, версия Chromium <a href="https://chromium.googlesource.com/chromium/src/+/refs/tags/124.0.6339.1" target="_blank">124.0.6339.1</a>):</p>
  <p id="v1vw"><a href="https://chromium.googlesource.com/chromium/src/+/refs/tags/124.0.6339.1/third_party/blink/renderer/core/animation/css/css_animations.h#155" target="_blank">src/third_party/blink/renderer/core/animation/css/css_animations.h</a></p>
  <pre id="F1ol" data-lang="cpp">class RunningAnimation final : public GarbageCollected&lt;RunningAnimation&gt; {
 public:
  RunningAnimation(Animation* animation, NewCSSAnimation new_animation)
      : animation(animation),
        name(new_animation.name),
        name_index(new_animation.name_index),
        specified_timing(new_animation.timing),
        style_rule(new_animation.style_rule),
        style_rule_version(new_animation.style_rule_version),
        play_state_list(new_animation.play_state_list) {}

  AnimationTimeline* Timeline() const {
    return animation-&gt;TimelineInternal();
  }
  const std::optional&lt;TimelineOffset&gt;&amp; RangeStart() const {
    return animation-&gt;GetRangeStartInternal();
  }
  const std::optional&lt;TimelineOffset&gt;&amp; RangeEnd() const {
    return animation-&gt;GetRangeEndInternal();
  }

  void Update(UpdatedCSSAnimation update) {
    DCHECK_EQ(update.animation, animation);
    style_rule = update.style_rule;
    style_rule_version = update.style_rule_version;
    play_state_list = update.play_state_list;
    specified_timing = update.specified_timing;
  }

  void Trace(Visitor* visitor) const {
    visitor-&gt;Trace(animation);
    visitor-&gt;Trace(style_rule);
  }

  Member&lt;Animation&gt; animation;
  AtomicString name;
  size_t name_index;
  Timing specified_timing;
  Member&lt;StyleRuleKeyframes&gt; style_rule;
  unsigned style_rule_version;
  Vector&lt;EAnimPlayState&gt; play_state_list;
};</pre>
  <p id="l0R9">Здесь мы видим, что класс реализует интерфейс <strong>GarbageCollected</strong>, а его метод <strong>Trace</strong> запускает трассировку внутренних объектов <code>animation</code> и <code>style_rule</code>.</p>
  <h3 id="u4PQ">Трассировка объектов JavaScript.</h3>
  <p id="AUss">В отличие от объектов C++ и системы Oilpan, про объекты JavaScript официальных публикаций нет. Поэтому, не редко, трассировку объектов JavaScript ошибочно ставят в один ряд с объектами C++. На самом же деле, объекты JavaScript полностью контролируются движком V8 и живут в специальной куче V8. А значит, нет никакой необходимости в реализации дополнительного, управляемого из вне, API. Каждый объект JavaScript является наследником класса <strong>HeadObject</strong>, который имеет <strong>Iterate</strong>, <strong>IterateFast</strong>, <strong>IterateBody</strong> и <strong>IterateBodyFast</strong>. А сама куча имеет метод <strong>Visit</strong>, который запускает трассировку по объектам в этой куче (на момент написания статьи, версия V8 <a href="https://chromium.googlesource.com/v8/v8/+/refs/tags/12.4.147" target="_blank">12.4.147</a>)</p>
  <p id="GC5R"><a href="https://chromium.googlesource.com/v8/v8/+/refs/tags/12.4.147/src/objects/heap-object.h#204" target="_blank">src/objects/heap-object.h#204</a></p>
  <pre id="C5Nc" data-lang="cpp">// HeapObject is the superclass for all classes describing heap allocated
// objects.
class HeapObject : public TaggedImpl&lt;HeapObjectReferenceType::STRONG, Address&gt; {
 public:
  
  ...
  
  // Iterates over pointers contained in the object (including the Map).
  // If it&#x27;s not performance critical iteration use the non-templatized
  // version.
  void Iterate(PtrComprCageBase cage_base, ObjectVisitor* v);

  template &lt;typename ObjectVisitor&gt;
  inline void IterateFast(PtrComprCageBase cage_base, ObjectVisitor* v);

  template &lt;typename ObjectVisitor&gt;
  inline void IterateFast(Tagged&lt;Map&gt; map, ObjectVisitor* v);

  template &lt;typename ObjectVisitor&gt;
  inline void IterateFast(Tagged&lt;Map&gt; map, int object_size, ObjectVisitor* v);

  // Iterates over all pointers contained in the object except the
  // first map pointer.  The object type is given in the first
  // parameter. This function does not access the map pointer in the
  // object, and so is safe to call while the map pointer is modified.
  // If it&#x27;s not performance critical iteration use the non-templatized
  // version.
  void IterateBody(PtrComprCageBase cage_base, ObjectVisitor* v);
  void IterateBody(Tagged&lt;Map&gt; map, int object_size, ObjectVisitor* v);

  template &lt;typename ObjectVisitor&gt;
  inline void IterateBodyFast(PtrComprCageBase cage_base, ObjectVisitor* v);

  template &lt;typename ObjectVisitor&gt;
  inline void IterateBodyFast(Tagged&lt;Map&gt; map, int object_size,
                            ObjectVisitor* v);
  
  ...
} </pre>
  <h2 id="8XvU">Поколения сборки мусора</h2>
  <p id="haGn">Процедура нахождения мертвых объектов и их очистка, сама по себе, может быть весьма небыстрой. В современных приложениях в памяти могут находиться тысячи объектов, а их размер может превышать <code>1 Gb</code>. Если речь идет о сборке объектов C++, работу можно переложить в отдельные потоки и запускать по требованию. Однако, если мы говорим о JavaScript, будучи однопоточным и не имеющим механизма синхронизации между потоками, при выполнении полной трассировки объектов на регулярной основе, разного рода задержки и снижение производительности, практически гарантированы. С этого момента и далее будем говорить о реализации сборки мусора именно JavaScript-объектов.</p>
  <p id="92RP">Чтобы не нагружать систему и проводить процесс сборки мусора незаметно для пользователя, команда V8 решила разбить процесс на части и работать над каждой из них отдельно.</p>
  <p id="EIAp">Существует гипотеза, под названием <a href="https://www.memorymanagement.org/glossary/g.html#term-generational-hypothesis" target="_blank">infant mortality или generational hypothesis</a>, согласно которой, в большинстве случаев, молодые объекты умрут раньше с большей вероятности, чем старые.</p>
  <p id="qanb">Так, в V8 появилось понятие &quot;поколение&quot; (<strong>generation</strong>). Существуют &quot;молодое поколение&quot; (<strong>young generation</strong>) и &quot;старое поколение&quot; (<strong>old generation</strong>).</p>
  <p id="oaRs">Куча, условно разбивается на маленькие young generations (до <code>16 Mb</code>), куда попадают все только что аллоцированные объекты. <strong>Old generation</strong> предназначена для старых объектов и может быть размером до <code>1.4 Gb</code>.</p>
  <p id="HMi3">Дополнительно, оба поколения организованы в, так называемые страницы (<strong>pages</strong>) по <code>1 Mb</code>. Объекты больше <code>600 Кb</code> размещаются в отдельных страницах и считаются частью <strong>old generation</strong>.</p>
  <p id="pHSN"><strong>Old generation</strong>, так же, включает в себя <strong>code space</strong> со всеми исполняемыми объектами кода и <strong>map space</strong> с вовлеченными скрытыми классами (<strong>hidden classes</strong>).</p>
  <h2 id="DOWs">Полупространственное размещение объектов в young generation</h2>
  <p id="9Dn3">Чтобы двинуться дальше и начать разбираться в удалении объектов из памяти, давай, для начала поймем, как эти объекты в памяти хранятся.</p>
  <p id="xmmZ">Я уже упоминал, что JavaScript работает в одном потоке. Он не требует синхронизации, а каждый JavaScript-контекст получает свою персональную кучу. Весь последующий алгоритм будет строиться исходя из этих фактов и необходимости уместить все процессы в одном потоке.</p>
  <p id="PQ8c">Итак, young generation делится на два полупространства (<strong>semi-space</strong>), активное и неактивное. Все новые объекты изначально размещаются в текущем активном полупространстве используя метод <strong>bump-pointer</strong>.</p>
  <figure id="LkEF" class="m_custom">
    <img src="https://img4.teletype.in/files/74/f0/74f0e4eb-89b7-4dfd-a90d-ed64d0be11ed.png" width="250.31034482758622" />
  </figure>
  <p id="jQhg">Метод заключается в том, что в куче всегда есть указатель на текущую свободную область. При создании нового объекта, этот указатель будет являться началом объекта. Конец объекта определяется простым подсчетом его размера. Определив размер и, соответственно конец объекты, указатель смещается на следующий свободный адрес.</p>
  <p id="truk">Как только полупространство полностью заполнилось, в работу вступает механизм <strong>Scavanger</strong>. Его задача - пройтись по живым объектам и переместить их в новое, неактивное полупространство. Эта операция называется <strong>Minor Garbage Collection</strong> (второстепенная сборка мусора).</p>
  <p id="2hOF">После этого два полупространства меняются местами. Текущее активное полупространство становится неактивным, а неактивное очищается и становится активным. Таким образом, уже со второй итерации в неактивном полупространстве могу оставаться ссылки на живые объекты. При следующем выполнении <strong>Minor Garbage Collection</strong>, если объекты уже были один раз помещены во второе полупространство - они считаются старыми и перемещаются в <strong>old generation</strong>, а мертвые - удаляются.</p>
  <figure id="Lk5b" class="m_column">
    <img src="https://img3.teletype.in/files/29/42/29421987-096f-4a1d-b978-f025f7e98552.png" width="1400" />
  </figure>
  <p id="7cdr">Продолжительность <strong>Minor Garbage Collection</strong> зависит от количества живых объектов в young generation. Если все объекты являются мертвыми, процесс займет меньше <code>1 ms</code>. Однако, если все или большинство объектов живые - значительно дольше. </p>
  <h2 id="ZCaU">Old generation</h2>
  <p id="mAtG"><strong>Old generation</strong> тоже использует метод <strong>bump-pointer</strong> для размещения объектов, а указатели хранятся в отдельных сводных таблицах, по аналогии с той, что я приводил в начале статьи.</p>
  <p id="00Nf">Это поколение очищается другим процессом, под названием <strong>Major Garbage Collection</strong> (основная сборка мусора). Запускается он, когда размер живых объектов в old generation превышает эвристически рассчитанный предел.</p>
  <p id="2wgb">Для уменьшения задержки и потребления памяти, <strong>old generation</strong> использует метод <strong>mark-sweep-comparator</strong>. Задержка во время маркировки зависит от количества живых объектов, которые должны быть промаркированы. Маркировка всей кучи может занимать до <code>100 ms</code> для больших Web-страниц. Чтобы избежать этого, V8 маркирует объекты маленькими порциями, такими, чтобы каждый шаг не превышал <code>5 ms</code>.</p>
  <p id="XFUq">Сама схема называется &quot;трехцветная маркировка&quot; (<strong>tricolor marking scheme</strong>). Более подробно про процесс маркировку можно почитать в после <a href="https://v8.dev/blog/concurrent-marking" target="_blank">Concurrent marking in V8</a> блога V8. Опять же, пересказывать пост полностью смысла не имеет. Обозначу общая суть процесса. Каждый объект находится в одном из трех статусов, условно обозначенных цветами. На самом деле, статус объекта - это 2-х битное поле, где</p>
  <ul id="QC4f">
    <li id="8TXG"><code>00</code> - <strong>белый</strong> - исходный статус всех новых объектов. Он означает, что объект еще не был обнаружен сборщиком.</li>
    <li id="galY"><code>01</code> - <strong>серый</strong> - в этот статус объект переходит сразу, как только сборщик до него добрался. Такой объект встает в список на дальнейшую трассировку.</li>
    <li id="FrcT"><code>11</code> - <strong>черный</strong> - конечный статус, означает, что сборщик посетил все дочерние узлы объекта и его можно убрать из списка трассировки.</li>
  </ul>
  <p id="NEO7">Такая схема позволяет ставить задачи по маркировке в очередь, частями, не блокирую, надолго, главный поток. Процесс будет завершен, когда список объект на трассировку опустеет.</p>
  <p id="TrVX">Однако, здесь имеется одна проблема. Между шагами маркировки выполняется JavaScript код, который может добавить новые объекты или удалить старые. Что делает статус уже промаркированных объектов неактуальным. Чтобы решить эту проблему, движок должен сообщать сборщику обо всех изменения в дереве объектов. Делается это посредством, так называемого <strong>write-barrier</strong>. В блоге V8 приводится пример реализации такого барьера.</p>
  <pre id="hedF" data-lang="cpp">// Called after &#x60;object.field = value&#x60;.
write_barrier(object, field_offset, value) {
  if (color(object) == black &amp;&amp; color(value) == white) {
    set_color(value, grey);
    marking_worklist.push(value);
  }
}</pre>
  <p id="JNZ8">Каждый раз при добавлении нового объекта в уже существующий срабатывает функция-барьер, которая проверят, был ли родитель уже промаркирован и, в случае, если дочерний объект еще ни к чему не привязан, родительский будет возвращен в список на трассировку, а сам этот объект сразу перейдет в статус серых.</p>
  <p id="FFZR">Данный код, конечно же, сейчас уже не актуален. На сегодняшний день барьеры гораздо сложнее и выглядят несколько иначе, но суть осталась прежней. Есть разные варианты реализации для разных случаев. Для полноты картины приведу часть кода реального барьера V8.</p>
  <p id="G3sz"><a href="https://chromium.googlesource.com/v8/v8/+/refs/tags/12.4.147/src/heap/marking-barrier-inl.h#64" target="_blank">src/heap/marking-barrier-inl.h#64</a></p>
  <pre id="B17D" data-lang="cpp">void MarkingBarrier::MarkValueShared(Tagged&lt;HeapObject&gt; value) {
  // Value is either in read-only space or shared heap.
  DCHECK(InAnySharedSpace(value));
  // We should only reach this on client isolates (= worker isolates).
  DCHECK(!is_shared_space_isolate_);
  DCHECK(shared_heap_worklist_.has_value());
  // Mark shared object and push it onto shared heap worklist.
  if (marking_state_.TryMark(value)) {
    shared_heap_worklist_-&gt;Push(value);
  }
}

void MarkingBarrier::MarkValueLocal(Tagged&lt;HeapObject&gt; value) {
  DCHECK(!InReadOnlySpace(value));
  if (is_minor()) {
    // We do not need to insert into RememberedSet&lt;OLD_TO_NEW&gt; here because the
    // C++ marking barrier already does this for us.
    // TODO(v8:13012): Consider updating C++ barriers to respect
    // POINTERS_TO_HERE_ARE_INTERESTING and POINTERS_FROM_HERE_ARE_INTERESTING
    // page flags and make the following branch a DCHECK.
    if (Heap::InYoungGeneration(value)) {
      WhiteToGreyAndPush(value);  // NEW-&gt;NEW
    }
  } else {
    if (WhiteToGreyAndPush(value)) {
      if (V8_UNLIKELY(v8_flags.track_retaining_path)) {
        heap_-&gt;AddRetainingRoot(Root::kWriteBarrier, value);
      }
    }
  }
}

...

bool MarkingBarrier::WhiteToGreyAndPush(Tagged&lt;HeapObject&gt; obj) {
  if (marking_state_.TryMark(obj)) {
    current_worklist_-&gt;Push(obj);
    return true;
  }
  return false;
}</pre>
  <p id="2qX2">После того как весь граф объектов был промаркирован, те объекты, до которых дотянулся сборщик, оказываются помеченными черным статусом. Все остальные остаются белыми. Как и в случае с Minor Garbage Collection, живые объекты перемещаются в новое полупространство или в новую страницу <strong>old generation</strong>.</p>
  <p id="oWLm">Продолжительность <strong>Major Garbage Collection</strong> линейна и зависит от количества живых объектов в old generation. Используя пошаговый алгоритм сборки мусора, V8 старается держать время работы <strong>Major Garbage Collection</strong> в пределах <code>6 ms</code>.</p>
  <h2 id="BiUq">Фрагментация страницы</h2>
  <p id="yEEo">После маркировки сборщик может оценить степень фрагментации данной страницы. Между живыми объектами, после очистки образуются пустые области. В зависимости от того скольки и какие объекты будут очищены, итоговая страница может получиться разной степени фрагментации.</p>
  <figure id="dcEg" class="m_column">
    <img src="https://img2.teletype.in/files/57/4f/574fb93b-1e09-44f2-a631-13003158ece3.png" width="1400" />
  </figure>
  <p id="dcWa">На рисунке выше изображены примеры высоко фрагментированной (слева) и низко фрагментированной (справа) страниц. Движок V8 проводит оценку степени фрагментации и принимает решение о том, что делать дальше со страницей.</p>
  <h3 id="u1M9">Compaction</h3>
  <p id="43zQ">В случае высокой фрагментации, сборщик копирует живые объекты в другую станицу которая еще не была скомпонована, параллельно сдвигая их, избавляясь от пустых областей. Этот процесс называется компоновкой (<strong>compaction</strong>). В результате получается дефрагментированная область памяти с живыми объектами. Мертвые же объекты очищаются вместе со старым полупространством. Однако, в памяти, зачастую бывает много долгоживущих объектов. Проводить компоновку каждой страницы может оказать слишком накладно. Поэтому, в случае низкой фрагментации сборщик идет по другому пути.</p>
  <h3 id="8LqV"><strong>Sweeping</strong></h3>
  <p id="qztS">При низкой фрагментации, между живыми объектами остаются сравнительно большие области. Вместо того чтобы копировать и компоновать объекты, планировщик просто помещает адреса объединенных мертвых областей в специальный список <strong>free-list</strong>. Позже, когда движку понадобится аллоцировать новый объект в памяти, он, в первую очередь заглянет в <strong>free-list</strong>, и если там есть подходящее, место разместит новый объект в нем. Этот процесс и называется <strong>Sweeping</strong>.</p>
  <h2 id="pp7I">Планирование Idle-задач</h2>
  <p id="kfxL">В статье <a href="https://blog.frontend-almanac.ru/chromium-rendering" target="_blank">Chromium. Отрисовка страницы с помощью Blink, CC и планировщика</a> я подробно описывал процесс рендеринга Web-страницы и планировщик задач. Давайте коротко вспомним, о чем шла речь.</p>
  <p id="PB3I">Движок Blink запускает процесс CC в отдельном потоке <strong>compositor thread</strong>. CC получает сигналы от разных систем Chromium и решает когда и что будет выводиться на экран. Если, например, инициировано начало анимации или скролл, СС посылает в главный поток сигнал о начале нового фрейма. Вместе с этим сигналом передается и дедлайн - время, до которого фрейм должен быть сформирован. Так как Chromium старается держать фреймрейт в районе 60 FPS, на один кадр приходится <code>1000/60 = ~16.6 ms</code>. Ровно столько времени есть у Blink, чтобы поочередно выполнить задачи по обработке пользовательского ввода, части JavaScript и других приоритетных задач и обновить RenderTree. Если Blink не уложиться в заданное время, кадр будет задержан, что может сказать на визуальном пользовательском опыте (могут появиться скачки анимации, дрожание и т.д.). Однако, зачастую, движку требуется гораздо меньше времени. При маленьком количестве задач Chromium может справить с кадром за время <code>&lt;1 ms</code>.</p>
  <p id="b6Os">Оставшееся неиспользованное время называется временем простоя (<strong>Idle time</strong>). Однако, <strong>idle time</strong>, на самом деле, не совсем простой. Это тоже задача, такая же как и остальные задачи в очереди, только с низким приоритетом. Длительность этой задачи равно времени, оставшемуся до начала формирования следующего кадра. Если следующего кадра не предвидится, <strong>idle time</strong> выставляется в <code>50 ms</code>. Число выбрано в связи с исследованиями, которые показывают, что ответ на пользовательский ввод в течение <code>100 ms</code> воспринимается пользователем как мгновенный. Если по окончании <code>50 ms</code> новых кадров не появилось, в очередь ставится новый <strong>idle time</strong> и так далее. Однако, если поступит сигнал о начале нового кадра, <strong>idle time</strong> будет прерван, не дожидаясь окончания времени.</p>
  <p id="yyBk">Когда стартует <strong>idle time</strong>, могут начать выполняться задачи из специальной низкоприоритетной очереди. Такие задачи называются <strong>idle tasks</strong>. Чтобы не превысить время дедлайна, в задачу передается время окончания <strong>idle time</strong>. Ответственность самой задачи - быть завершенной до указанного срока. Задача может выполнить часть работы, которая уложиться в отведенное время, а оставшуюся часть - поставить в очередь новой задачей. Аналогично, если задача не может выполнить сколько-нибудь значимую работу за указанное время, она должна переставить себя в очередь новой задачей.</p>
  <p id="pj9K"><strong>Idle tasks</strong> - прерогатива не только движка V8. Web-разработчику тоже доступен этот механизм. Браузерами предусмотрен метод Web API - <a href="https://w3c.github.io/requestidlecallback/#the-requestidlecallback-method" target="_blank">requestIdleCallback</a>, который ставит задачу в очередь idle tasks. Правда, на момент написания статьи, метод еще находится в процессе проработки и не поддерживается браузером Safari.</p>
  <h2 id="fA7d">Планирование Garbage Collection для времени простоя</h2>
  <p id="eo1g">Выше мы говорили, что процесс сборки мусора может быть довольно нагрузочным. Чтобы не блокировать систему этой утилитарной операцией, V8 разбивает весь процесс на мелкие шаги, на каждый шаг, все равно может занимать до <code>5 ms</code> процессорного времени. Чтобы сделать процесс максимально незаметным для всей остальной системы, V8 ставит задачи по сборке мусора именно в очередь <strong>idle tasks</strong>.</p>
  <p id="wJdN"><strong>Minor Garbage Collection</strong> не может быть разбито на части о должна выполниться целиком за один раз.</p>
  <p id="c1ZK"><strong>Major Garbage Collection</strong> состоит из трех частей:</p>
  <ul id="DnOd">
    <li id="CdNk">начало пошаговой маркировки (<strong>incremental marking</strong>)</li>
    <li id="5YjH">несколько шагов маркировки</li>
    <li id="Ys0C">завершение (<strong>finalization</strong>)</li>
  </ul>
  <p id="Wd0l">Каждый из этапов имеет свои показатели времени задержки и влияния на память. Хотя стадия начала маркировки, сама по себе является быстрой операцией, она ведет к нескольким шагам последующей маркировки и завершению, что может спровоцировать длинные паузы. Таким образом, старт маркировки может привести к ухудшению показателя времени задержки, зато оказывает большое влияние на показатель потребления памяти.</p>
  <p id="NFCE">Запуск <strong>Minor Garbage Collection</strong> сокращает время задержки, но имеет сравнительно небольшое влияние на потребляемую память. Тем не менее регулярное выполнение <strong>Minor Garbage Collection</strong> позволяет предотвратить попадание объектов в стадию консервативной сборки мусора, которая выполняется вне времени простоя. Консервативная сборка мусора может быть запущена в критических ситуациях, например, при обнаружении нехватки выделенной памяти.</p>
  <p id="31rK">Таким образом, планирование должно планировщик должен балансировать между стартом слишком рано (и, как следствие, продвижению объектов в <strong>old generation</strong> слишком рано) и слишком поздно, что может привести к разрастанию размера <strong>young generation</strong>.</p>
  <h3 id="zdRt">Планирование Minor Garbage Collection для времени простоя</h3>
  <p id="YRY2">Для осуществления Minor Garbage Collection требуется:</p>
  <ol id="9aFs">
    <li id="NsKQ">предикат, который скажет, можно ли выполнить Minor Garbage Collection в установленное время</li>
    <li id="FH5A">механизм постановки <strong>idle task</strong> в Blink-планировщик для запуска в нужное время</li>
  </ol>
  <p id="9v32">V8 выполняет <strong>idle-time Minor Garbage Collection</strong>, тольк если её ожидаемое время выполнения укладывается в дедлайн пекущего idle-пероида и, при этом, есть достаточное количество объектов в <strong>young generation</strong>.</p>
  <p id="UMKS"></p>
  <p id="TLtg">Пусть <code>H</code> - общий размер объектов в байтах в <strong>young generation</strong>.</p>
  <p id="Mj1x"><code>S&#x27;</code> - средняя скорость обработки предыдущих Minor Garbage Collections в байтах в секунду</p>
  <p id="ye4r"><code>T</code> - дедлайн текущего idle-периода в секундах</p>
  <p id="O7Eu"><code>T&#x27;</code> - средний дедлайн idle task в секундах</p>
  <p id="ARGd"></p>
  <p id="mEBu">V8 выполнит Minor Garbage Collection, если:</p>
  <pre id="8hEI" data-lang="haskell">max(T&#x27; * S&#x27; - N, Hmin) &lt; H &lt;= S&#x27; * T</pre>
  <p id="oLGA">где <code>N</code> - ожидаемое количество байт, которые будут аллоцированы перед следующей <strong>idle task</strong></p>
  <p id="qaNM"><code>Hmin</code> - минимальный размер young generation, гарантирующий сборку мусора</p>
  <p id="EatY"></p>
  <p id="ICKe"><code>(T&#x27; * S&#x27; - N) &lt; H</code> можно переписать, как <code>(T&#x27; * S&#x27;) &lt; (H + N)</code>, что оценивает, будет ли следующий Minor Garbage Collection больше среднего дедлайна idle-задачи. Если больше - Minor Garbage Collection должна быть выполнена в текущем idle-периоде. Если меньше - в <strong>young generation</strong> пока еще недостаточно объектов для эффективной работы.</p>
  <p id="rtF8">В целях запроса <strong>idle time</strong> для <strong>Minor Garbage Collection</strong> от Blink-планировщика, V8 размещает аварийные маркеры каждые <code>N</code> байт в новом <strong>generation</strong>. Когда очередная аллокация пересекает аварийный маркер, она вызывает runtime-функцию V8, которая публикует <strong>idle task</strong> в Blink-планировщик.</p>
  <p id="cU7h">Экспериментальным путем установлено, что <code>N = 512 Kb</code> достаточно, чтобы не снижать пропускную способность, при этом достаточно мало, чтобы V8 имел несколько возможностей опубликовать <strong>idle task</strong>.</p>
  <h3 id="KKEg">Планирование Major Garbage Collection для времени простоя</h3>
  <p id="aLDu">Процесс запуска пошаговой маркировки мы рассмотрим чуть ниже. Пока представим, что она уже запущена. Сразу, как только произошел запуск, V8 ставит <strong>idle task</strong> в Blink-планировщик. Задача выполняет, непосредственно, сам процесс маркировки.</p>
  <p id="iA3I">Взяв за основу среднюю скорость маркировки, <strong>idle task</strong> пытается достигнуть максимального объема работы, возможного в текущем idle-периоде. </p>
  <p id="nRA9"></p>
  <p id="4Cay">Пусть <code>Tidle</code> - дедлайн idle-задачи в секундах,</p>
  <p id="jUnZ"><code>M</code> - скорость маркировки в байтах в секунду.</p>
  <p id="Ovy8">Тогда,</p>
  <p id="W6WM"><code>Tidle * M</code> - количество байт, которые будут промаркированы в этой <strong>idle task</strong>.</p>
  <p id="Ozd5"></p>
  <p id="mbAr"><strong>Idle task</strong> итеративно переставляет себя в очередь до тех пор вся <strong>Heap</strong> не будет промаркирована. После этого V8 ставит задачу на завершение <strong>Major Garbage Collection</strong>. Процесс завершения будет выполнен, если оценочное время этой задачи укладывается в дедлайн. V8 определяет время процесса завершения на основе скорости предыдущих задач на завершение.</p>
  <h2 id="T9jn">Memory Reducer</h2>
  <p id="cwAK"><strong>Memory reducer</strong> - это контроллер, который пытается высвободить память в неактивных Web-страницах.</p>
  <p id="46ge"><strong>Major Garbage Collector</strong> включается в работу, когда куча достигает определенного заданного размера. Этот размер рассчитывается и выставляется в конце предыдущей <strong>Major Garbage Collection</strong>, на основе коэффициента увеличения кучи <code>f</code> и общего размера живых объектов в <strong>old generation</strong></p>
  <pre id="SHNU">limit&#x27; = f * size</pre>
  <p id="ko2o">Следующая <strong>Major Garbage Collection</strong> ставится в очередь, когда количество байт, аллоцированных с момента предыдущей сборки, превысит этот предельный размер <code>limit&#x27; - size</code>.</p>
  <p id="DImj">Все хорошо, пока Web-страница стабильно выделяет память. Однако, если Web-страница ушла в простой, память выделяться перестает, а значит, заданный предел не будет достигнут и <strong>Major Garbage Collection</strong> не запустится.</p>
  <p id="U2jO">Зачастую, многие Web-страницу демонстрируют высокую интенсивность выделения памяти в момент загрузки, посколько они инициализируют свои внутренние структуры данных. Через несколько секунд или минут после загрузки, Web-страница часто становится неактивной, при этом в памяти находится большое количество объектов, которые уже не нужны. Что может приводить к снижению скорости выделения памяти и, как следствие, к снижению скорости выполнения JavaScript кода.</p>
  <figure id="B4qt" class="m_column">
    <img src="https://img4.teletype.in/files/70/81/7081977d-a82c-4d17-8d4b-5514222a46de.png" width="2048" />
  </figure>
  <p id="3Bwh">На диаграмме выше приведен пример планирования <strong>Major Garbage Collection</strong>. Первая сборка случается в момент времени <code>t1</code>, так как был достигнут предельный размер кучи. V8 устанавливает новое значение предела на основе коэффициента увеличения кучи и размера кучи. Следующий вызов <strong>Major Garbage Collection</strong> произойдет в момент <code>t2</code>, когда будет достигнут новый предел, потом, в момент <code>t3</code> и так далее. Пунктирной линией показан размер кучи, каким бы он был без <strong>memory reducer</strong>.</p>
  <p id="1kFH">Таким образом, memory reducer может запустить <strong>Major Garbage Collection</strong>, не дожидаясь лимита аллоцирования. Однако, бесконтрольное снижение предела может привести к ухудшению показателя задержки. Поэтом команда V8 разработала эвристический метод, который полагается не только на idle time, предоставленным планировщиком Blink между кадрами, но и на то, является ли страница неактивной в данный момент. Индикатором того, активна страница или нет, являются скорость аллоцирования JavaScript и частота вызовов JavaScript из браузера. Когда обе скорости падают ниже заданного порога, страница считается неактивной.</p>
  <figure id="EaNx" class="m_column">
    <img src="https://img2.teletype.in/files/58/a3/58a3df4b-efce-42b7-91ec-bf1a9fb50f10.png" width="1712" />
  </figure>
  <p id="e8a1">На рисунке показаны состояния и переходы <strong>memory reducer</strong>. В состоянии <code>done</code> контроллер ожидает сигнала о том, что было выполнено достаточное количество выделений, чтобы гарантировать запуск сборщика мусора. В этот момент применяется, так называемый <strong>non-idle Major Garbage Collection</strong>, который запускается по достижении лимита выделения, что является сигналом для перехода в состояние <code>wait(i)</code>. В этом состоянии контроллер ждет, когда Web-страница станет неактивной. Когда это происходит, запускает пошаговая маркировка и переход в состояние <code>run(i)</code>, до тех пор, пока <strong>Major Garbage Collection</strong> не завершит работу. Далее, если расчеты показывают, что еще одна <strong>Major Garbage Collection</strong>, вероятно, позволит высвободить еще памяти, контроллер переходит в состояние wait(i+1). В противном случае, он вернется в исходное состояние <code>done</code>.</p>
  <p id="hsoO">Важнейшей частью контроллера является хорошо настроенный порог частоты аллоцирования. Изначально, этот порог был фиксированной величины. В целом, такой вариант показывал себя неплохо на Desktop устройствах. Однако на более медленных системах, например, мобильных, это не работает. Поэтому разработчикам пришлось сделать динамическую адаптацию под разное аппаратное обеспечение.</p>
  <p id="pP4x"></p>
  <p id="xWTA">Пусть <code>g</code> - скорость  <strong>Major Garbage Collection</strong>,</p>
  <p id="Ehdz"><code>a</code> - частота аллоцирования.</p>
  <p id="Gz8q">Тогда</p>
  <pre id="t2Et">μ = g/(g+a)</pre>
  <p id="LakQ"></p>
  <p id="Hr6H">Соотношение <code>μ</code> можно рассматривать как использование мутатора во временном интервала с настоящего момента до следующей <strong>Major Garbage Collection</strong>. Предполагая, что текущая частота аллоцировани остается постоянной и куча в данный момент пуста:</p>
  <pre id="t4zH">Tmu = limit/a               (mutator time)

  Tgc = limit/g             (GC time)
  
    μ = Tmu /(Tmu + Tgc )   (mutator utilization)
    
      = (limit /a)/(limit /a + limit /g)
      
      = g/(g + a)</pre>
  <p id="KlMS">Это дает нам условие для неактивной скорости распределения: <code>μ ≥ µinactive</code> , где <code>µinactive</code> - фиксированное значение.</p>
  <p id="eKsn">В коде V8 <code>µinactive</code> имеет значение<code>0.993</code>.</p>
  <pre id="HO1h" data-lang="cpp">bool Heap::HasLowOldGenerationAllocationRate() {
  double mu = ComputeMutatorUtilization(
      &quot;Old generation&quot;,
      tracer()-&gt;OldGenerationAllocationThroughputInBytesPerMillisecond(),
      tracer()-&gt;CombinedMarkCompactSpeedInBytesPerMillisecond());
  const double kHighMutatorUtilization = 0.993;
  return mu &gt; kHighMutatorUtilization;
}</pre>
  <p id="4MgR"></p>
  <hr />
  <p id="Mkxv"></p>
  <p id="7pWR"><strong>Мои телеграмм-каналы:</strong></p>
  <section style="background-color:hsl(hsl(55,  86%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="YnAq">EN - <a href="https://t.me/frontend_almanac" target="_blank">https://t.me/frontend_almanac</a><br />RU - <a href="https://t.me/frontend_almanac_ru" target="_blank">https://t.me/frontend_almanac_ru</a></p>
  </section>
  <p id="Ri5D"><em>English version: <a href="https://blog.frontend-almanac.com/v8-garbage-collection" target="_blank">https://blog.frontend-almanac.com/v8-garbage-collection</a></em></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.frontend-almanac.ru/chromium-rendering</guid><link>https://blog.frontend-almanac.ru/chromium-rendering?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=frontend_almanac_ru</link><comments>https://blog.frontend-almanac.ru/chromium-rendering?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=frontend_almanac_ru#comments</comments><dc:creator>frontend_almanac_ru</dc:creator><title>Chromium. Отрисовка страницы с помощью Blink, CC и планировщика </title><pubDate>Tue, 27 Feb 2024 22:30:10 GMT</pubDate><media:content medium="image" url="https://img1.teletype.in/files/c7/9a/c79a1c19-768d-46be-b96a-2d6873c6425b.png"></media:content><description><![CDATA[<img src="https://img3.teletype.in/files/ac/84/ac84758c-8b7a-4d7d-a0bc-2f46c837c8c8.png"></img>Движок Chromium от компании Google состоит из огромного числа внутренних механизмов, подсистем и других движков. В этой статье мы погрузимся в процесса компоновки и вывода Web-страницы непосредственно на экран. А так же, чуть ближе познакомимся с движком Blink, композитором (или, как его еще называют, сопоставитель контента) и планировщиком задач.]]></description><content:encoded><![CDATA[
  <p id="KFhc">Движок <a href="https://www.chromium.org/Home/" target="_blank">Chromium</a> от компании Google состоит из огромного числа внутренних механизмов, подсистем и других движков. В этой статье мы погрузимся в процесса компоновки и вывода Web-страницы непосредственно на экран. А так же, чуть ближе познакомимся с движком Blink, композитором (или, как его еще называют, сопоставитель контента) и планировщиком задач.</p>
  <h2 id="5DMX">Парсинг Web-страницы</h2>
  <p id="Pjij">Давайте, для начала вспомним, как вообще происходит рендеринг Web-страницы.</p>
  <p id="HQvG">Получив документ HTML, браузер производит его парсинг. Так как HTML изначально разрабатывался совместимым с традиционной структурой XML, никаких, интересных для нас особенностей на этом этапе нет. В результате парсинга браузер получает иерархическое дерево объектов - <a href="https://ru.wikipedia.org/wiki/Document_Object_Model" target="_blank">DOM</a> (Document Object Model).</p>
  <p id="AtU6">По мере прохождения по структуре HTML и парсинга его в <a href="https://ru.wikipedia.org/wiki/Document_Object_Model" target="_blank">DOM</a>, браузер встречает такие элементы, как стили и JS-скрипты (как встроеные, так и в виде удаленных ресурсов). Такие элементы требуют дополнительной обработки. JS-скрипт парсится JavaScript-движком в структуру <a href="https://ru.wikipedia.org/wiki/%D0%90%D0%B1%D1%81%D1%82%D1%80%D0%B0%D0%BA%D1%82%D0%BD%D0%BE%D0%B5_%D1%81%D0%B8%D0%BD%D1%82%D0%B0%D0%BA%D1%81%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5_%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE" target="_blank">AST</a>, и далее раскладывается в память в виде внутренних объектов самого движка. Стили же выстраиваются в каскадное дерево <a href="https://developer.mozilla.org/ru/docs/Web/API/CSS_Object_Model" target="_blank">CSSOM</a>. В это же дерево будут добавлены и inline-стили элементов.</p>
  <p id="iHeD">Получив <a href="https://ru.wikipedia.org/wiki/Document_Object_Model" target="_blank">DOM</a> и <a href="https://developer.mozilla.org/ru/docs/Web/API/CSS_Object_Model" target="_blank">CSSOM</a>, браузер теперь имеет возможность произвести все необходимые расчеты позиционирования и отображения элементов и, как результат, составить одно общее дерево Render Tree, на основании которого и будет производиться отрисовка графики непосредственно на экране.</p>
  <h2 id="EMAr">Отрисовка Web-страницы</h2>
  <p id="vi0w">На сегодняшний день, большинство браузеров являются многопоточными. Не исключение и браузеры на основе движка <a href="https://www.chromium.org/Home/" target="_blank">Chromium</a>.</p>
  <h3 id="iL4N">Blink</h3>
  <p id="pNsc">За всё, что связано с отрисовкой контента в табе браузера отвечает отдельная система под названием <strong>Blink</strong>. Вообще, <strong>Blink</strong> - это большой и сложный движок, в обязанности которого входят такие функции как реализация <a href="https://html.spec.whatwg.org" target="_blank">спецификации HTML</a> в части DOM, CSS и Web IDL, интеграция движка <a href="https://v8.dev" target="_blank">V8</a> и запуск JavaScript-кода, отрисовка графики на экране (интеграция движка <a href="https://en.wikipedia.org/wiki/Skia_Graphics_Engine" target="_blank">Skia</a>), запрос сетевых ресурсов, обработка операций ввода, выстраивание  деревьев (DOM, CSSOM, Render Tree), расчет стилей и позиционирования и еще много чего другого, включая <strong>Chrome Compositor (CC)</strong>.</p>
  <p id="MsUd">Сам движок не является &quot;коробочным&quot; решением и не может быть запущен самостоятельно. Это, своего рода, форк компонента WebCore движка <a href="https://en.wikipedia.org/wiki/WebKit" target="_blank">WebKit</a>.</p>
  <p id="Spb9"><strong>Blink</strong> используется в таких платформах, как <a href="https://www.chromium.org/Home/" target="_blank">Chromium</a>, <a href="https://developer.android.com/reference/android/webkit/WebView" target="_blank">Android WebView</a>, <a href="https://www.opera.com" target="_blank">Opera</a>, <a href="https://www.microsoft.com/edge" target="_blank">Microsoft Edge</a> и многих других  Chromium-based браузерах.</p>
  <h3 id="hwGK">Chrome Compositor (CC)</h3>
  <p id="3i2J">В кодовой базе <a href="https://www.chromium.org/Home/" target="_blank">Chromium</a> этот механизм находится в директории <a href="https://cs.chromium.org/chromium/src/cc/" target="_blank">сс/</a>. Так исторически сложилось, что аббревиатуру <strong>СС</strong> расшифровывают как <strong>Chrome Compositor</strong> (&quot;компоновщик&quot;), хотя, на сегодняшний день, он компоновщиком, как таковым не является. Дана Дженсенс (danakj) даже предложила альтернативный вариант названия - <strong>Content Collator</strong> (&quot;обобщитель&quot; или &quot;сопоставитель&quot; контента).</p>
  <p id="GmTU"><strong>СС</strong> запускается движком <strong>Blink</strong> и может работать как в однопоточном, так и в многопоточном режимах. Работа <strong>СС</strong> требует отдельной статьи, здесь мы не будем подробно разбирать все механики системы. Отметим только, что основными сущностями, которыми оперирует <strong>СС</strong> являются слои и деревья слоев. Эти сущности доступны из <strong>CC</strong> в качестве API. Слои (<strong>Layer</strong>) могут быть множества разных видов, например, слой изображения, слой текстур, поверхностный слой и др. Задача клиент <strong>CC</strong> (в нашем случае, клиентом для <strong>СС</strong> является <strong>Blink</strong>) собрать свое дерево слоев (<strong>LayerTreeHost</strong>) и сообщить <strong>СС</strong> о том, что оно готово и может быть отрисовано. Такой подход позволяет сделать процесс формирования итоговой композиции атомарным.</p>
  <h3 id="QNxK">Принципиальная схема рендеринга</h3>
  <p id="u2or">Chromium является многопоточным движком. Под множество конкретных операций, связанных с рендерингом, выделяются отдельные поток. Основными потоками, условно, можно считать <strong>main thread</strong> и <strong>compositor thread</strong>.</p>
  <p id="MFQa"><strong>Main thread</strong> - основной поток, в котором работает <strong>Blink</strong>. Именно здесь составляются конечные <strong>RenderTree</strong> и <strong>LayerTreeHost</strong>. Здесь же обрабатываются сгруппированные операции ввода и исполняется JavaScript-код. После того, как все необходимые операции и расчеты произведены, <strong>Blink</strong> сообщит <strong>СС</strong> о том, что деревья готовы к отрисовке.</p>
  <p id="DjaL"><strong>Compositor thread</strong> отвечает за работу <strong>CC</strong>, т.е. за планирование задач рендеринга и непосредственную отрисовку на экране.</p>
  <p id="CY5H"></p>
  <figure id="FiCN" class="m_column">
    <img src="https://img3.teletype.in/files/ac/84/ac84758c-8b7a-4d7d-a0bc-2f46c837c8c8.png" width="1400" />
  </figure>
  <p id="sW5N"><strong>Blink</strong> стремиться отображать графику с частотой <code>60 FPS</code> (60 кадров в секунду), т.е. один кадр должен быть выведен на экран примерно за <code>16.6 ms</code>. Такой фреймрейт считается оптимальным для восприятия человеческим глазом. Меньшая частота может приводить к неравномерной анимации, скачкам и дрожаниям графики.</p>
  <p id="mjVX">На схеме выше изображена упрощенная схема рендеринга. Как я же упоминал, <strong>СС</strong> запускается в отдельном потоке. В определенный момент он решает, что наступило время инициировать рендеринг кадра. <strong>СС</strong> подает сигнал в главный поток о начале нового фрейма. <strong>Blink</strong> получает сигнал в главном потоке и выполняет необходимые запланированные операции, такие как пакетная обработка ввода, исполнение JavaScript-кода (точнее, JavaScript-задач из Event Loop) и обновление <strong>RenderTree</strong>. После того, как <strong>RenderTree</strong> готово, движок производит соответствующие изменения в <strong>LayerTreeHost</strong> (в его временной копии) и отправляет сигнал <code>commit</code> в <strong>СС</strong>, где тот, в свою очередь забирает все расчеты и отправляет задачи на отрисовку графики на конечном устройстве используя API соответствующих графических библиотек ОС (таких как <strong>OpenGL</strong> и <strong>DirectX</strong>).</p>
  <p id="RIjF">На весь этот процесс отведено окно в <code>1000/60 = ~16.6 ms</code>. Если движок не успеет выполнить все необходимые операции за это время, кадр будет задержан, что может привести к снижению фреймрейта. Поэтому, крайне важной задачей для <strong>Blink</strong> является подсчет и прогнозирование времени выполнения предстоящих задач. Зная, сколько времени займет та или иная операция, движок имеет возможность взять в работу только то, что успеет выполнить за отведенное время, остальные операции будут отложены до следующего раза.</p>
  <p id="Agbb">Существуют, так же, операции, в которых не производятся специфические расчеты и не задействован JavaScript, например скроллинг. Такие операции <strong>CC</strong> может выполнить самостоятельно в своем потоке, тем самым не блокируя основной поток. В то же время, например, анимация требует интенсивных расчетов на стороне основного потока, на протяжении большого количества кадров. Если основной поток занят другими приоритетными задачами, в случае большого объема часть операций по анимации может быть отложена или задержана.</p>
  <h2 id="qPTk">Планировщик задач</h2>
  <p id="a8zn">Планировщик задач предназначен для снижения вероятности задержек в обновлении кадра. Запускается он в основном потоке. Каждая задача ставится механизмами движка в одну из очередей, специфичных для этого типа задачи. Задачи <strong>СС</strong> попадают в свою очередь, операции по обработке ввода - в свою, выполнение JavaScript-кода - в свою, а процессы по загрузке страницы - в свою.</p>
  <p id="WSTL">Задачи внутри очереди выполняют в том порядке, в котором они туда попали (вспоминаем <strong>Event Loop</strong>). Однако, из какой именно очереди выполнить следующую задачу, выбирает планировщик, который волен решать это динамически, меняя приоритет очереди по своему усмотрению. Когда и какой установить приоритет, планировщик решает на основании получаемых сигналов от множества разных систем. Так, если страница находится еще на этапе загрузки, в приоритете будут сетевые запросы и парсинг HTML. А если зафиксировано событие <code>touch</code>, планировщик временно повысит приоритет операций ввода, в течение следующих <code>100 ms</code>, дабы корректно распознать возможные жесты. Предполагается, что именно в этот временной интервал следующие возможные события могут оказаться операциями скролла, тапа, зума и т.д.</p>
  <p id="ghOQ">Обладая полной информацией об очередях и задачах в них, а так же о сигналах от других компонентов, планировщик имеет возможность рассчитать примерное время простоя системы. Чуть выше мы рассматривали пример рендеринга кадров. Вместе с сигналом от <strong>CC</strong> о начале отрисовки кадра посылается, так же, предполагаемое время следующего кадра, если кадр имеется (<code>+16.6ms</code>). Зная, есть ли необходимые для отрисовки кадра, операции ввода и какой JavaScript-код требуется выполнить, планировщик может прикинуть продолжительность выполнения этих задач. А зная время следующего кадра, может, так же, рассчитать время, так называемого простоя (<strong>Idle time</strong>). На самом деле, этот период является не совсем простоем. Он может быть использован для выполнения ряда низко-приоритетных задач (<strong>Idle tasks</strong>). Такие задачи помещаются в свою очередь и выполняются порциями, только после того, как другие очереди опустели, и в ограниченный промежуток времени. В частности, эту очередь активно использует сборщик мусора. Именно здесь происходит большая часть работы по маркировке мертвых объектов и дефрагментации памяти. Принудительно, с высоким приоритетом, консервативный сборщик запускается только в крайних случаях, например, при обнаружении нехватки памяти. Подробнее об этом поговорим в статье <a href="https://blog.frontend-almanac.ru/editor/v8-garbage-collection" target="_blank">Сборка мусора в V8</a>.</p>
  <h2 id="nF0A">Регулярность частоты кадров</h2>
  <p id="x9og">Я уже говорил, что <a href="https://www.chromium.org/Home/" target="_blank">Chromium</a> стремиться достичь частоты кадров в 60 FPS. Для достижения этой цели движок оснащен планировщиком, <strong>СС</strong> и множеством других систем. Однако, на практике, добиться идеальной регулярности - задача практически невыполнимая, так как на процесс может влиять большое количество непредвиденных ситуаций, как внутренних (одна или несколько задач могут отрабатывать дольше, чем оценил планировщик), так и внешних (например, загруженность CPU или GPU другими процессами).</p>
  <figure id="EYfS" class="m_column">
    <img src="https://img1.teletype.in/files/c2/f9/c2f99aa0-b524-4a47-84c7-20a51fc11987.png" width="2444" />
  </figure>
  <p id="P9LU">Примерно вот так выглядит скролл страницы <a href="https://www.google.com/chrome" target="_blank">https://www.google.com/chrome</a> в трассировщике. Далеко не все кадры анимации смогли уложиться в отведенное окно <code>16.6 ms</code>.</p>
  <p id="5hZ2">Более того, кроме задержек в отрисовке, часть кадров и вовсе может быть отклонена самим движком <strong>Blink</strong>. Такое нередко случается, например, во время анимации. Причин сброса кадров анимации существует целое множество. <strong>Blink</strong> предусматривает таких причин порядка двадцати (версия <a href="https://www.chromium.org/Home/" target="_blank">Chromium</a> на момент написания статьи <a href="https://chromium.googlesource.com/chromium/src.git/+/refs/tags/124.0.6326.0/" target="_blank">124.0.6326.0</a>)</p>
  <p id="BiUB"><a href="https://chromium.googlesource.com/chromium/src.git/+/refs/tags/124.0.6326.0/third_party/blink/renderer/core/animation/compositor_animations.h#65" target="_blank">/third_party/blink/renderer/core/animation/compositor_animations.h#65</a></p>
  <pre id="pUEr" data-lang="cpp">enum FailureReason : uint32_t {
  kNoFailure = 0,
  
  // Cases where the compositing is disabled by an exterior cause.
  kAcceleratedAnimationsDisabled = 1 &lt;&lt; 0,
  kEffectSuppressedByDevtools = 1 &lt;&lt; 1,
  
  // There are many cases where an animation may not be valid (e.g. it is not
  // playing, or has no effect, etc). In these cases we would never composite
  // it in any world, so we lump them together.
  kInvalidAnimationOrEffect = 1 &lt;&lt; 2,
  
  // The compositor is not able to support all setups of timing values; see
  // CompositorAnimations::ConvertTimingForCompositor.
  kEffectHasUnsupportedTimingParameters = 1 &lt;&lt; 3,
  
  // Currently the compositor does not support any composite mode other than
  // &#x27;replace&#x27;.
  kEffectHasNonReplaceCompositeMode = 1 &lt;&lt; 4,
  
  // Cases where the target element isn&#x27;t in a valid compositing state.
  kTargetHasInvalidCompositingState = 1 &lt;&lt; 5,
  
  // Cases where the target is invalid (but that we could feasibly address).
  kTargetHasIncompatibleAnimations = 1 &lt;&lt; 6,
  kTargetHasCSSOffset = 1 &lt;&lt; 7,
  
  // This failure reason is no longer used, as multiple transform-related
  // animations are allowed on the same target provided they target different
  // transform properties (e.g. rotate vs scale).
  kObsoleteTargetHasMultipleTransformProperties = 1 &lt;&lt; 8,
  
  // Cases relating to the properties being animated.
  kAnimationAffectsNonCSSProperties = 1 &lt;&lt; 9,
  kTransformRelatedPropertyCannotBeAcceleratedOnTarget = 1 &lt;&lt; 10,
  kFilterRelatedPropertyMayMovePixels = 1 &lt;&lt; 12,
  kUnsupportedCSSProperty = 1 &lt;&lt; 13,
  
  // This failure reason is no longer used, as multiple transform-related
  // animations are allowed on the same target provided they target different
  // transform properties (e.g. rotate vs scale).
  kObsoleteMultipleTransformAnimationsOnSameTarget = 1 &lt;&lt; 14,
  
  kMixedKeyframeValueTypes = 1 &lt;&lt; 15,
  
  // Cases where the scroll timeline source is not composited.
  kTimelineSourceHasInvalidCompositingState = 1 &lt;&lt; 16,
  
  // Cases where there is an animation of compositor properties but they have
  // been optimized out so the animation of those properties has no effect.
  kCompositorPropertyAnimationsHaveNoEffect = 1 &lt;&lt; 17,
  
  // Cases where we are animating a property that is marked important.
  kAffectsImportantProperty = 1 &lt;&lt; 18,
  
  kSVGTargetHasIndependentTransformProperty = 1 &lt;&lt; 19,
  
  // When adding new values, update the count below *and* add a description
  // of the value to CompositorAnimationsFailureReason in
  // tools/metrics/histograms/enums.xml .
  // The maximum number of flags in this enum (excluding itself). New flags
  // should increment this number but it should never be decremented because
  // the values are used in UMA histograms. It should also be noted that it
  // excludes the kNoFailure value.
  kFailureReasonCount = 20,
};</pre>
  <p id="XPlw">Из всего набора, наиболее частыми причинами являются отсутствие визуального эффекта анимации (<code>kCompositorPropertyAnimationsHaveNoEffect</code>), когда результат анимации не приводит к изменениям в графике и, соответственно, не требует перерисовки.</p>
  <p id="gl8V">Так же, к сбросу кадра может привести не поддерживаемое CSS-свойство (<code>kUnsupportedCSSProperty</code>). Такое может случиться, если движок не понимает, как пересчитать то или иное свойство, даже если само это свойство выглядит вполне валидным.</p>
  <pre id="X4mQ" data-lang="html">&lt;style&gt;
  #block1 {
    animation: expand 1s linear infinite;
  }

  @keyframes expand {
    to {
      height: auto;
    }
  }
&lt;/style&gt;

&lt;div id=&quot;block1&quot;&gt;&lt;/div&gt;</pre>
  <p id="r3LW">В примере выше движок не знает, как ему высчитать высоту блока, так как конечное значение не определено и не может быть рассчитано на этапе анимации. В результате, движок сбросит первый кадр анимации, а дальнейшие даже не будет пытаться посчитать, так как в этом нет никакого смысла.</p>
  <p id="5tLQ">В трассировщике, в этом случае мы нейдем запись, вроде</p>
  <pre id="nmBH" data-lang="javascript">{&quot;args&quot;:{&quot;data&quot;:{&quot;compositeFailed&quot;:8224,&quot;unsupportedProperties&quot;:[&quot;height&quot;]}},&quot;cat&quot;:&quot;blink.animations,...</pre>
  <p id="2dKY">Всё это приводит к тому, что реальное количество кадров, отрисованных за секунду, может быть меньше 60-ти.</p>
  <h2 id="rLEU">Расхождение как показатель регулярности частоты кадров</h2>
  <p id="7av9">Помимо среднего фреймрейта, в анимированных приложениях важна, так же и регулярность отрисовки. Если приложение рендерит 60 (или близко к тому) кадров в секунду, но размер промежутков между кадрами сильно расходится, пользователь может наблюдать скачки или дрожание на экране. Существует множество методик оценки этого явления, от простого замера самого длинного кадра, до расчета расхождения в длинах кадров. У каждого метода есть свои плюсы, однако, большинство покрывает только ряд случаев и не учитывает временной порядок кадров. В частности, они не способны различить ситуации, когда два сброшенных кадра находятся рядом, и когда далеко.</p>
  <p id="5gos">В связи с этим, разработчики Google предложили свою методику оценки регулярности кадров. Метод основан на расхождении в последовательности длительностей кадров, по аналогии с математическим методом <a href="https://en.wikipedia.org/wiki/Monte_Carlo_integration" target="_blank">Monte Carlo integration</a>.</p>
  <p id="IqDy">Теоретическая база метода была представлена на 37-ой ежегодной конференции ACM SIGPLAN в 2016-м году.</p>
  <p id="EYff">На рисунке ниже представлен пример расхождения регулярности кадров.</p>
  <figure id="42S4" class="m_column">
    <img src="https://img4.teletype.in/files/77/59/77596c6c-a058-4b5c-b951-037dada49c3b.png" width="1188" />
  </figure>
  <p id="D6Iv">Каждая линия представляет набор временных меток. Черными точками обозначены отрисованные кадры. Белыми - сброшенные. Расстояние между точками - <code>1 VSYNC</code>, который равен <code>16.6 ms</code> для 60-герцовой частоты обновления. Итоговые расхождения расчитываются в VSYNC-интервалах:</p>
  <pre id="ZIGD">D(S1) = 1
D(S2) = 2
D(S3) = 2
D(S4) = 25/9
D(S5) = 3</pre>
  <p id="zFDP">В идеальном случае (<code>S1</code>) расхождение равно интервалу между кадрами (<code>1 VSYNC</code>). Если один из кадров был сброшен (<code>S2</code>), расхождение будет равно наибольшему расстоянию между двумя отрисованными кадрами. В случае <code>S2</code> наибольшее расстояние будет между точками 2 и 3 и равно <code>2 VSYNC</code>. Тоже самое будет и в случае, если имеется два сброшенных кадра, но далеко друг от друга (<code>S3</code>). Это обусловлено тем, что задача метода - определить наихудший случай производительности, а не средний, что отражено в расчетных формулах. Поэтому метод комбинируется со средней продолжительностью кадра, чтобы отличить один пропущенный кадр от серии повторяющихся пропусков (последнее, очевидно, хуже). В случае <code>S4</code> мы наблюдаем два сброшенных кадра, которые находятся близко друг к другу. Такие кадры составляют уже единую потерянную область и расхождение здесь составить <code>25/9 (~2.7) VSYNC</code>. Еще хуже дела обстоят в случае <code>S5</code>, так как между двумя сброшенными кадрами не было ни одного отрисованного. Наибольшее расстояние между отрисованными кадрами здесь будет между точками 2 и 4, что составляет 3 интервала (<code>3 VSYNC</code>).</p>
  <p id="8fUS"></p>
  <hr />
  <p id="VlsD"></p>
  <p id="7pWR"><strong>Мои телеграмм-каналы:</strong></p>
  <section style="background-color:hsl(hsl(55,  86%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="YnAq">EN - <a href="https://t.me/frontend_almanac" target="_blank">https://t.me/frontend_almanac</a><br />RU - <a href="https://t.me/frontend_almanac_ru" target="_blank">https://t.me/frontend_almanac_ru</a></p>
  </section>
  <p id="Ri5D"><em>English version: <a href="https://blog.frontend-almanac.com/chromium-rendering" target="_blank">https://blog.frontend-almanac.com/chromium-rendering</a></em></p>

]]></content:encoded></item><item><guid isPermaLink="true">https://blog.frontend-almanac.ru/iterators</guid><link>https://blog.frontend-almanac.ru/iterators?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=frontend_almanac_ru</link><comments>https://blog.frontend-almanac.ru/iterators?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=frontend_almanac_ru#comments</comments><dc:creator>frontend_almanac_ru</dc:creator><title>Итераторы в JavaScript</title><pubDate>Tue, 20 Feb 2024 17:08:23 GMT</pubDate><category>Базовый арсенал Frontend-разработчика</category><description><![CDATA[В этой статье рассмотрим механизм итераторов. Что это такое, какие они бывают, как их применять и как создать свои собственные.]]></description><content:encoded><![CDATA[
  <p id="OgKK">В этой статье рассмотрим механизм итераторов. Что это такое, какие они бывают, как их применять и как создать свои собственные.</p>
  <h2 id="HRx2">Основные понятия</h2>
  <p id="lb7T">Чтобы разобраться в механизме итерации, давайте взглянем на следующие интерфейсы.</p>
  <pre id="c76J" data-lang="typescript">interface Iterable {
  [Symbol.iterator]: () =&gt; Iterator;
}

interface Iterator {
  next: () =&gt; IteratorResult;
  
  return?: (value?: any) =&gt; IteratorResult;
  
  throw?: (exception?: any) =&gt; IteratorResult;
}

interface IteratorResult {
  done?: boolean;
  value?: any;
}</pre>
  <p id="oVao">Итак, объект считается <strong>итерируемым</strong>, если он реализует интерфейс <a href="https://tc39.es/ecma262/#sec-iterable-interface" target="_blank">Iterable</a>. Другими словами, если он содержит метод <strong>[Symbol.iterator]</strong> - функцию, возвращающую, непосредственно, объект-итератор.</p>
  <p id="3QUw"><strong>Итератор</strong> - это простой (<a href="https://tc39.es/ecma262/#ordinary-object" target="_blank">ordinary</a>) объект, который содержит метод <strong>next()</strong>. Остальные два метода в интерфейсе <strong>Iterator</strong> не являются обязательными. Метод <strong>next()</strong> обязан вернуть объект <strong>IteratorResult</strong>.</p>
  <p id="uq4e">Суть итерируемости заключается в том, что такой объект может быть перебран циклами <strong>for..of</strong> и <strong>for..in</strong>.</p>
  <pre id="UEYi" data-lang="javascript">for (const item of [1, 2, 3]) {
    console.log(item);
}
// 1
// 2
// 3</pre>
  <h2 id="2JCn">Встроенные итераторы</h2>
  <p id="MDX1">Механизм итераций является одним из самых широко используемых в JavaScript. Язык буквально пронизан встроенными итерируемыми объектами. К ним относятся: <a href="https://tc39.es/ecma262/#array-exotic-object" target="_blank">Array</a>, <a href="https://tc39.es/ecma262/#typedarray" target="_blank">TypedArray</a>, <a href="https://tc39.es/ecma262/#sec-map-objects" target="_blank">Map</a>, <a href="https://tc39.es/ecma262/#sec-set-objects" target="_blank">Set</a>, <a href="https://tc39.es/ecma262/#sec-weakmap-objects" target="_blank">WeakMap</a>, <a href="https://tc39.es/ecma262/#sec-weakset-objects" target="_blank">WeakSet</a>, <a href="https://tc39.es/ecma262/#string-exotic-object" target="_blank">String</a>.</p>
  <pre id="XEE3" data-lang="javascript">// Array
for (const item of new Array(1, 2, 3)) {
    console.log(item);
}
// 1
// 2
// 3

// TypedArray
for (const item of new Int8Array(3)) {
    console.log(item);
}
// 0
// 0
// 0

// Map
for (const item of new Map([[1, &#x27;one&#x27;], [2, &#x27;two&#x27;], [3, &#x27;three&#x27;]])) {
    console.log(item);
}
// [1, &#x27;one&#x27;]
// [2, &#x27;two&#x27;]
// [3, &#x27;three&#x27;]

// Set
for (const item of new Set([1, 2, 3])) {
    console.log(item);
}
// 1
// 2
// 3

// String
for (const item of &quot;abc&quot;) {
    console.log(item);
}
// a
// b
// c</pre>
  <p id="dPpz">Кроме JavaScript, некоторые объекты Web API так же реализуют интерфейс <a href="https://tc39.es/ecma262/#sec-iterable-interface" target="_blank">Iterable</a>. Например, в стандарте <a href="https://dom.spec.whatwg.org" target="_blank">DOM</a>, примерами таких объектов являются <a href="https://dom.spec.whatwg.org/#nodelist" target="_blank">NodeList</a> и <a href="https://dom.spec.whatwg.org/#domtokenlist" target="_blank">DOMTokenList</a>.</p>
  <pre id="MyH4" data-lang="javascript">// NodeList
for (const item of document.querySelectorAll(&quot;div&quot;)) {
    console.log(item);
}
// Node
// Node
// ...

// DOMTokenList
const block = document.createElement(&quot;div&quot;);
block.classList.add(&quot;classA&quot;, &quot;classB&quot;)

for (const item of block.classList) {
    console.log(item)
}
// classA
// classB</pre>
  <h2 id="a9kM">Собственные итераторы</h2>
  <p id="LFvp">По мимо встроенных итерируемых объектов, существует возможность создать собственный. Для этого достаточно просто реализовать интерфейс <a href="https://tc39.es/ecma262/#sec-iterable-interface" target="_blank">Iterable</a>, соблюдая протокол итерации.</p>
  <p id="axyQ">Итак, как уже было сказано выше, объект считается итерируемым, если его прототип содержит метод <strong>[Symbol.iterator]()</strong>. В противном случае, при попытке пропустить такой объект через цикл, будет сгенерировано исключение.</p>
  <pre id="Kzom" data-lang="javascript">const obj = {};

for (const item of obj) {
    console.log(item);
}

// Uncaught TypeError: obj is not iterable</pre>
  <p id="D3Bv">Теперь добавим метод-итератор в наш объект.</p>
  <pre id="QDkh" data-lang="javascript">const obj = {
    [Symbol.iterator]: () =&gt; {},
};

for (const item of obj) {
    console.log(item);
}

// Uncaught TypeError: Result of the Symbol.iterator method is not an object</pre>
  <p id="lyt5">Теперь, формально, объект считается итерируемым, но пропустить его через цикл  в таком виде все равно не получится, так как метод-итератор не возвращает объект, реализующий интерфейс <strong>Iterator</strong>, как того требует протокол.</p>
  <h3 id="U4Nb">next()</h3>
  <p id="lZ7w">Согласно протоколу, итератор должен быть простым объектом, содержащим, как минимум, один метод <strong>next()</strong>. Сам метод <strong>next()</strong> может принимать аргументы, но строго обязательства в этом нет. Спецификация <a href="https://tc39.es/ecma262" target="_blank">ECMA-262</a> гарантирует, что все встроенные пользователи итерируемых объектов вызывают этот метод без аргументов. Однако, в редких случаях, аргументы могут быть полезны при ручном вызове итератора.</p>
  <pre id="ll1B" data-lang="javascript">const obj = {
    [Symbol.iterator]: () =&gt; {
        return {
            next: () =&gt; {},
        };
    },
};

for (const item of obj) {
    console.log(item);
}

// Uncaught TypeError: Iterator result undefined is not an object</pre>
  <p id="qRjK">В примере выше мы добавили метод <strong>next()</strong>, но сам этот метод все еще не возвращает правильный <strong>IteratorResult</strong>.</p>
  <p id="SlAo">IteratorResult - это, так же простой объект, имеющий два свойства, <strong>done</strong> и <strong>value</strong>.</p>
  <p id="5hVv">Свойство <strong>done</strong> является булевым и выражает завершение процесса итерации. Другими словами, пока объект может итерироваться дальше, его итератор возвращает <strong>done: false</strong>, достигнув последнего шага итерации, итератор должен вернуть <strong>done: true</strong>, что будет являться сигналом для пользователя итерируемого объекта (например, цикла) к завершению процесса (выходу из цикла).</p>
  <p id="RGYZ">Свойство <strong>value</strong> - непосредственно значение, ассоциированное с данным шагом итерации. Ограничений на формат возвращаемого значения нет.</p>
  <p id="MWI7">Как я только что упоминал, если итератор вернул <strong>done: true</strong>, итерирование будет считаться завершенным, следующий шаг итерации не случится, а значит, в значении <strong>value</strong>, в этом случае, смысла больше нет, соответственно, и передавать его вместе с <strong>done: true</strong>, не обязательно.</p>
  <p id="VoX7">Стоит сказать, что спецификация <a href="https://tc39.es/ecma262" target="_blank">ECMA-262</a> к вопросу итераторов подошла крайне либерально. На самом деле, возвращать ни свойство <strong>done</strong>, ни свойство <strong>value</strong> вовсе не обязательно. Для обоих предусмотрены значения по умолчанию (<strong>false</strong> и <strong>undefined</strong> соответственно). Т.е. фактически, чтобы наш итерируемый объект наконец заработал, итератору достаточно вернуть просто пустой объект.</p>
  <pre id="DqYI" data-lang="javascript">const obj = {
    [Symbol.iterator]: () =&gt; {
        return {
            next: () =&gt; {
                return {}; // { done: false, value: undefined }
            },
        };
    },
};

for (const item of obj) {
    console.log(item);
}

// Осторожно!!! Данный код ведет в бесконечный цикл</pre>
  <p id="1MdV">Однако, пользы от такого итератора мало. Более того, так как итератор никогда не вернет <strong>done: true</strong>, весь код уйдет в бесконечный цикл. </p>
  <p id="jtDB">Итак, давайте, все таки, опишем функциональный итератор по всем правилам.</p>
  <pre id="0aBo" data-lang="javascript">const obj = {
    [Symbol.iterator]: () =&gt; {
        let i = 0;
        
        return {
            next: () =&gt; {
                return {
                    done: i &gt;= 3,
                    value: i++,
                };
            },
        };
    },
};

for (const item of obj) {
    console.log(item);
}

// 0
// 1
// 2</pre>
  <p id="IYon">В данном примере мы последовательно возвращаем числа <code>0</code>, <code>1</code>, <code>2</code>. Выход из цикла осуществляется по достижении  <code>i === 3</code>.</p>
  <h3 id="wOqb">return()</h3>
  <p id="nGzW">В ряде случаев, процесс итерации может быть принудительно остановлен тем или иным образом из вне. В таком случае, пользователь итерируемого объекта должен вызвать метод итератора <strong>return()</strong>, если он существует.</p>
  <pre id="W3db" data-lang="javascript">const obj = {
    [Symbol.iterator]: () =&gt; {
        let i = 0;
        
        return {
            next: () =&gt; {
                return {
                    done: i &gt;= 3,
                    value: i++,
                };
            },
            
            return: () =&gt; {
                console.log(&quot;Return&quot;, i);
                return {
                    done: i &gt;= 3,
                    value: i,
                }
            }
        };
    },
};

for (const item of obj) {
    console.log(item);
    
    if (item === 1) {
        break;
    }
}
// 0
// 1
// Return 2</pre>
  <p id="Tlv7">Как и метод <strong>next()</strong>, метод <strong>return()</strong> обязан вернуть объект <strong>IteratorResult</strong>. Данный метод является, своего рода, колбэком в момент принудительного прерывания итерации. Предполагается, что здесь могут быть выполнены специфические операции, необходимые по звершении процесса. Например, отвязка прослушивателей или очистка памяти.</p>
  <h3 id="MgMG">throw()</h3>
  <p id="4p8l">Еще один метод-колбэк по аналогии с <strong>return()</strong>. Суть его в том, чтобы дать возможность пользователю итерируемого объекта сообщить о найденой ошибке. Среди встроенных пользователей JavaScript нет таких, которые могут вызвать этот метод в какой-либо ситуации. Однако, данный колбэк может быть вызван вручную.</p>
  <p id="aGyU">Протокол позволяет передать в метод <strong>throw()</strong> аргумент, который, предлагается использовать в качестве ссылки на исключение, хотя формально, ограничений, как на количество, так и на типы аргументов - нет.</p>
  <p id="krvh">Будучи вызванным, метод <strong>throw()</strong> должен &quot;выкинуть&quot; переданное ему исключение, дабы остановить дальнейший процесс итерации. Опять же, такое поведение является рекомендованным и ожидаемым, но не обязательным. Автор итератора вправе самостоятельно определить дальнейшую логику.</p>
  <pre id="tQD6" data-lang="javascript">const obj = {
  [Symbol.iterator]: () =&gt; {
    let i = 0;
    
    return {
      next: () =&gt; {
        return {
          done: i &gt;= 3,
          value: i++,
        };
      },
      
      throw: (exception) =&gt; {
        console.log(&quot;Thrown:&quot;, exception);
        
        if (exception) {
          throw exception;
        }
        
        return {
          done: true,
        };
      },
    };
  },
};

for (const item of obj) {
  console.log(item);
  
  if (item === 1) {
    obj[Symbol.iterator]().throw(&#x60;Reached ${item}&#x60;);
  }
}

// 0
// 1
// Thrown: Reached 1
// Uncaught Reached 1</pre>
  <p id="nihK">Чаще всего, метод <strong>throw()</strong> применяется в асинхронных итераторах в паре с генераторами. Об этом далее.</p>
  <h2 id="2AqH">Ручной вызов итератора</h2>
  <p id="yq4o">Как уже стало понятным, основным пользователем итерируемых объектов являются конструкции <strong>for..of</strong> и <strong>for..in</strong>. Эти встроенные пользователи самостоятельно реализуют протокол итерации. Однако, иногда есть необходимость в создании своего собственного пользователя или, просто в проходе процесса итерации в ручном режиме. Как мы видели в последнем примере, к итератору можно обратиться напрямую и пройти весь цикл самостоятельно.</p>
  <pre id="PshJ" data-lang="javascript">const obj = {
  [Symbol.iterator]: () =&gt; {
    let i = 0;
    
    return {
      next: () =&gt; {
        return {
          done: i &gt;= 5,
          value: i++,
        };
      },
      
      return: () =&gt; {
        return {
          done: true,
          value: i,
        };
      },
      
      throw: (exception) =&gt; {
        if (exception) {
          throw exception;
        }
        
        return {
          done: true,
        };
      },
    };
  },
};

// получаем итератор посредством вызова метода-итератора
const iterator = obj[Symbol.iterator]();

// первый шаг итреации
let result = iterator.next();
console.log(result.value);

// остальные шаги будут вызываться в цикле, пока итератор
// не вернет &#x60;done: true&#x60;
while (!result.done) {
  const result = iterator.next();
  console.log(result.value);
  
  if (isSuccess(result.value)) {
    // по необходимости, мы можем остановить процесс итареции
    iterator.return();
    break;
  }
  
  if (isFailed(result.value)) {
    // или выкинуть исключение
    iterator.throw(new Error(&quot;failed&quot;));
  }
}</pre>
  <h2 id="HpfL">Самоитерируемые объекты</h2>
  <p id="kQUN">Мы уже познакомились с двумя понятиями: итерируемый объект (<a href="https://tc39.es/ecma262/#sec-iterable-interface" target="_blank">Iterable</a>), и итератор (<a href="https://tc39.es/ecma262/#sec-iterator-interface" target="_blank">Iterator</a>). До сих пор мы рассматривали обе сущности в отдельности. Следующая техника позволяет объединить все в один самоитерируемый объект.</p>
  <pre id="tLpL" data-lang="javascript">const obj = {
  i: 0,
  
  [Symbol.iterator]() {
    return this;
  },
  
  next() {
    return {
      done: this.i &gt;= 3,
      value: this.i++,
    };
  },
  
  return() {
    return {
      done: true,
      value: this.i,
    };
  },
  
  throw(exception) {
    if (exception) {
      throw exception;
    }
    
    return {
      done: true,
    };
  },
};

for (const item of obj) {
  console.log(item);
}</pre>
  <p id="rxxf">В этом примере, в методе-итераторе мы, в качестве итератора вернули ссылку на сам итерируемый объект, таки образом, <code>obj</code> одновременно является и итератором и итерируемым объектом.</p>
  <p id="svCY">Похожим образом может выглядеть и итерируемый класс.</p>
  <pre id="xZ2C" data-lang="javascript">class Obj {
  i = 0;
  
  [Symbol.iterator]() {
    return this;
  }
  
  next() {
    return {
      done: this.i &gt;= 3,
      value: this.i++,
    };
  }
  
  return() {
    return {
      done: true,
      value: this.i,
    };
  }
  
  throw(exception) {
    if (exception) {
      throw exception;
    }
    
    return {
      done: true,
    };
  }
}

const obj = new Obj();

for (const item of obj) {
  console.log(item);
}</pre>
  <h2 id="ggH7">Асинхронные итераторы</h2>
  <p id="BFsz">До сих пор мы говорили только о синхронных итераторах. В июне 2018-го была опубликована <a href="https://262.ecma-international.org/9.0/" target="_blank">9-я редакция спецификации ECMA-262</a>. В этой версии были анонсированы асинхронные итераторы и протокол <strong>AsyncIterator</strong>.</p>
  <p id="3nYG">По своей сути, асинхронный итератор мало чем отличается от синхронного. Давайте взглянем на следующие интерфейсы.</p>
  <pre id="P6Cf" data-lang="typescript">interface AsyncIterable {
  [Symbol.asyncIterator]: () =&gt; AsyncIterator;
}

interface AsyncIterator {
  next: () =&gt; Promise&lt;IteratorResult&gt;;
  
  return?: (value?: any) =&gt; Promise&lt;IteratorResult&gt;;
  
  throw?: (exception?: any) =&gt; Promise&lt;IteratorResult&gt;;
}</pre>
  <p id="bW7X">Итак, основная разница между синхронным и асинхронным итерируемыми объектами в том, что асинхронный должен иметь метод-итератор <strong>[Symbol.asyncIterator]</strong>, (вместо <strong>[Symbol.iterator]</strong>, как в случае с синхронным). А в качестве возвращаемых значений методами <strong>next()</strong>, <strong>return()</strong> и <strong>throw()</strong> ожидается <strong>Promise</strong>.</p>
  <p id="6gZo">Сами по себе асинхронные итераторы небыли бы так удобны в использовании, если бы спецификация не предусмотрела новую структуру - асинхронный цикл <strong>for..await..of</strong>.</p>
  <pre id="QsVj" data-lang="javascript">const obj = {
  [Symbol.asyncIterator]: () =&gt; {
    let i = 0;
    
    return {
      next: () =&gt; {
        return Promise.resolve({
          done: i &gt;= 3,
          value: i++,
        });
      },
      
      return: () =&gt; {
        return Promise.resolve({
          done: true,
          value: i,
        });
      },
      
      throw: (exception) =&gt; {
        if (exception) {
          return Promise.reject(exception);
        }
        
        return Promise.resolve({
          done: true,
        });
      },
    };
  },
};

(async () =&gt; {
  for await (const item of obj) {
    console.log(item);
    
    if (item === 1) {
      await obj[Symbol.asyncIterator]().throw(&#x60;Reached ${1}&#x60;);
    }
  }
})();</pre>
  <h2 id="suzu">Асинхронные генераторы</h2>
  <p id="uEzh">Еще одним важным нововведение <a href="http://9-%D1%8F%20%D1%80%D0%B5%D0%B4%D0%B0%D0%BA%D1%86%D0%B8%D1%8F%20%D1%81%D0%BF%D0%B5%D1%86%D0%B8%D1%84%D0%B8%D0%BA%D0%B0%D1%86%D0%B8%D0%B8%20ECMA-262" target="_blank">9-ой редакции ECMA-262</a> стали асинхронные генераторы, тесно связанные с асинхронными итераторами.</p>
  <p id="dpYM">Простой асинхронный генератор может выглядеть следующим образом.</p>
  <pre id="mWQe" data-lang="javascript">async function* gen() {
  yield 0;
  yield 1;
  yield 2;
}

(async () =&gt; {
  for await (const item of gen()) {
    console.log(item);
  }
})();

// 0
// 1
// 2</pre>
  <p id="0HCq">Асинхронный генератор реализует протокол <strong>AsyncIterator</strong>, а потому, мы можем пользоваться всеми доступными методами итератора вручную.</p>
  <pre id="Zmg3" data-lang="javascript">async function* gen() {
  yield 0;
  yield 1;
  yield 2;
}

const iterator = gen();

(async () =&gt; {
  console.log(await iterator.next());
  console.log(await iterator.next());
  console.log(await iterator.next());
  console.log(await iterator.next());
})();

// {value: 0, done: false}
// {value: 1, done: false}
// {value: 2, done: false}
// {value: undefined, done: true}</pre>
  <p id="8HKM">В том числе, остановить процесс итерации.</p>
  <pre id="cmDY" data-lang="javascript">async function* gen() {
  yield 0;
  yield 1;
  yield 2;
}

const iterator = gen();

(async () =&gt; {
  console.log(await iterator.next());
  console.log(await iterator.return(&quot;My return reason&quot;));
})();

// {value: 0, done: false}
// {value: &#x27;My return reason&#x27;, done: true}</pre>
  <p id="GRPu">Или выкинуть исключение.</p>
  <pre id="eKUJ" data-lang="javascript">async function* gen() {
  yield 0;
  yield 1;
  yield 2;
}

const iterator = gen();

(async () =&gt; {
  console.log(await iterator.next());
  console.log(await iterator.throw(&quot;My error&quot;));
})();

// {value: 0, done: false}
// Uncaught (in promise) My error</pre>
  <h2 id="WzZ5">Async-from-Sync итератор</h2>
  <p id="5yz6"><a href="https://tc39.es/ecma262/#sec-async-from-sync-iterator-objects" target="_blank">Async-from-Sync итератор</a> - это асинхронный итератор, полученный из синхронного посредством абстрактной операции <a href="https://tc39.es/ecma262/#sec-createasyncfromsynciterator" target="_blank">CreateAsyncFromSyncIterator</a>. Напомню, абстрактная операция - это внутренний механизм языка JavaScript. В обычно режиме такие операции не доступны внутри исполняемого контекста. Однако, например, движок V8 дает возможность воспользоваться этими операциями при включенном флаге <code>--allow-natives-syntax</code>.</p>
  <pre id="CQ8g" data-lang="shell">&gt; v8-debug --allow-natives-syntax iterator.js</pre>
  <pre id="CQ8g" data-lang="javascript">// iterator.js
const syncIterator = {
  [Symbol.iterator]() {
    return this;
  },
  
  next() {
    return {
      done: true,
      value: Promise.resolve(1),
    };
  },
};

const asyncIterator = %CreateAsyncFromSyncIterator(syncIterator);

console.log(asyncIterator);
// [object Async-from-Sync Iterator]

console.log(asyncIterator[Symbol.asyncIterator]);
// function [Symbol.asyncIterator]() { [native code] }</pre>
  <p id="q7Hb">Как мы можем видеть, эта абстрактная операция преобразует синхронный итератор в асинхронный. Как я уже говорил, в обычном исполняемом контексте мы не можем вызвать абстрактную операцию. Это делает сам движок в тех местах, где требуется. А требуется это, согласно спецификации, в одном конкретном случае:</p>
  <ul id="l0pP">
    <li id="Ys7r">Текущее окружение является асинхронным</li>
    <li id="p731">Итерируемый объект не имеет метода <strong>[Symbol.asyncIterarot]</strong></li>
    <li id="VdGa">Итерируемый объект имеет метод <strong>[Symbol.iterator]</strong></li>
  </ul>
  <p id="XWNn">Проще всего продемонстрировать этот процесс можно на генераторах.</p>
  <pre id="upOA" data-lang="javascript">function* syncGen() {
  yield 1;
  yield 2;
  yield Promise.resolve(3);
  yield Promise.resolve(4);
  yield 5;
}

for (const item of syncGen()) {
  console.log(item);
}

// 1
// 2
// Promise
// Promise
// 5</pre>
  <p id="PEQT">Данный синхронный генератор на 3 и 4 шагах вернет неразрешенные промисы. Однако, асинхронный пользователь <strong>for..await..of</strong> сможет преобразовать эти шаги в асинхронные.</p>
  <pre id="upOA" data-lang="javascript">function* syncGen() {
  yield 1;
  yield 2;
  yield Promise.resolve(3);
  yield Promise.resolve(4);
  yield 5;
}

(async () =&gt; {
  for await (const item of syncGen()) {
    console.log(item);
  }
})();

// 1
// 2
// 3
// 4
// 5</pre>
  <p id="MSLY">Такая же картина будет и с обычными синхронными итераторами.</p>
  <pre id="Jv55" data-lang="javascript">const syncIterator = {
  i: 0,
  
  [Symbol.iterator]() {
    return this;
  },
  
  next() {
    const done = this.i &gt;= 3;

    if (this.i === 1) {
      return {
        done: false,
        value: Promise.resolve(this.i++),
      };
    }

    return {
      done,
      value: this.i++,
    };
  },
};

for (const item of syncIterator) {
  console.log(item);
}

// 0
// Promise
// 2</pre>
  <p id="Jv55">Но, применив асинхронный пользователь, второй шаг будет преобразован в асинхронный.</p>
  <pre id="Jv55" data-lang="javascript">const syncIterator = {
  i: 0,
  
  [Symbol.iterator]() {
    return this;
  },
  
  next() {
    const done = this.i &gt;= 3;

    if (this.i === 1) {
      return {
        done: false,
        value: Promise.resolve(this.i++),
      };
    }

    return {
      done,
      value: this.i++,
    };
  },
};

(async () =&gt; {
  for await (const item of syncIterator) {
    console.log(item);
  }
})();

// 0
// 1
// 2</pre>
  <p id="FXJO"></p>
  <hr />
  <p id="93at"></p>
  <p id="VlsD"><strong>Мои телеграмм-каналы:</strong></p>
  <section style="background-color:hsl(hsl(55,  86%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="YnAq">EN - <a href="https://t.me/frontend_almanac" target="_blank">https://t.me/frontend_almanac</a><br />RU - <a href="https://t.me/frontend_almanac_ru" target="_blank">https://t.me/frontend_almanac_ru</a></p>
  </section>
  <p id="Ri5D"><em>English version: <a href="https://blog.frontend-almanac.com/iterators" target="_blank">https://blog.frontend-almanac.com/iterators</a></em></p>

]]></content:encoded></item></channel></rss>