история обвала производительности v8 в react [перевод]
Published: 2020-04-22 10:20:47

Вольный перевод статьи Benedikt Meurer и Mathias Bynens "[The story of a V8 performance cliff in React](https://v8.dev/blog/react-cliff)"

# История обвала производительности V8 в React

**Примечание:**  если тексту вы предпочитаете презентацию, ниже приложено видео.

video: [https://www.youtube.com/watch?v=0I0d8LkDqyc&feature=emb_logo](https://www.youtube.com/watch?v=0I0d8LkDqyc&feature=emb_logo)

## Типы в JavaScript

Каждое значение в JavaScript имеет ровно один (в настоящий момент) из восьми типов: `Number`, `String`, `Symbol`, `BigInt`, `Boolean`, `Undefined`, `Null` и `Object`.

![https://v8.dev/_img/react-cliff/01-javascript-types.svg](https://v8.dev/_img/react-cliff/01-javascript-types.svg)

С одним заметным исключением, JavaScript типы можно выявить с помощью оператора `typeof`:

    typeof 42;
    // → 'number'
    typeof 'foo';
    // → 'string'
    typeof Symbol('bar');
    // → 'symbol'
    typeof 42n;
    // → 'bigint'
    typeof true;
    // → 'boolean'
    typeof undefined;
    // → 'undefined'
    typeof null;
    // → 'object' 🤔
    typeof { x: 42 };
    // → 'object'

`typeof null` возвращает `'object'`, а не `'null'`, несмотря на то, что `Null` является типом сам по себе. Чтобы понять почему, рассмотрим набор всех JavaScript типов в виде двух групп:

- *объекты* (т.е. типа `Object`)
- *примитивы* (т.е. любого значения, отличного от объекта)

Таким образом, `null` означает “значение, отличное от объекта”, тогда как `undefined` означает “нет значения”.

![https://v8.dev/_img/react-cliff/02-primitives-objects.svg](https://v8.dev/_img/react-cliff/02-primitives-objects.svg)

Следуя этой мысли, Брендан Айк спроектировал JavaScript таким образом, чтобы `typeof` возвращал `'object'` для всех значений по правую сторону, т.е. всех объектов и `null`, в духе Java. Поэтому `typeof null === 'object'`, несмотря на спецификацию, имеет отдельный тип `Null`.

![https://v8.dev/_img/react-cliff/03-primitives-objects-typeof.svg](https://v8.dev/_img/react-cliff/03-primitives-objects-typeof.svg)

## Представление значений

JavaScript движки должны иметь возможность представлять в памяти произвольные значения. Однако, стоит заметить, что тип значения JavaScript отделен от того, как JavaScript движки представляют это занчение в памяти.

Значение `42`, например, имее тип `number`.

    typeof 42;
    // → 'number'

Есть несколько способов представления в памяти целого числа вроде `42`:

[Untitled](https://www.notion.so/967a6b3d57ad416383020d0447407f31)

ECMAScript стандартизирует числа как 64-битные значения с плавающей запятой, также известные как *числа двойной точности (double precision floating-point)* или *Float64*. Однако, это не означает, что JavaScript движки хранят числа в Float64 представлении всё время — такой способ был бы ужасно неэффективным! Движки могут выбирать другие внутренние представления до тех пор, пока наблюдаемое поведение в точности соответствует Float64.

Большинство числе в реальных JavaScript приложениях являются [валидными индексами массива ECMAScript](https://tc39.es/ecma262/#array-index), т.е. целочисленными значениями в промежутке от 0 до 2³²−2.

    array[0]; // Наименьший из возможных индекс массива.
    array[42];
    array[2**32-2]; // Наибольший из возможных индекс массива.

Чтобы оптимизировать код, который обращается к элементам массива по индексу, JavaScript движки могут выбирать оптимальное представление таких чисел в памяти. Чтобы процессор имел доступ к операциям с памятью, индекс массива должен быть доступен в виде [дополнительного кода](https://en.wikipedia.org/wiki/Two%27s_complement).  Представление же индексов массива в виде Float64 будет расточительством, поскольку движку придется конвертировать значение между Float64 и дополнительным кодом каждый раз, когда кто-то обращается к элементу массива.

Представление же в виде 32-битного дополнительного кода не просто полезно для операций с массивами, поскольку **процессоры выполняют операции над целыми числами гораздо быстрее, чем над числами с плавающей запятой**. Как раз поэтому первый цикл из следующего примера будет как минимум вдвое быстрее второго.

    for (let i = 0; i < 1000; ++i) {
      // быстро 🚀
    }
    
    for (let i = 0.1; i < 1000.1; ++i) {
      // медленно 🐌
    }

Принцип работает и с операциями. Производительность модульного оператора в следующем примере будет зависеть от того, работаете ли вы с целыми числами или нет.

    const remainder = value % divisor;
    // Быстрый 🚀 если `value` и `divisor` представлены целыми числами,
    // медленный 🐌 в противном случае.

Если оба операнда представлены целыми числами, CPU сможет вычислить результат очень эффективно, а V8 имеет дополнительные ускорялки для случаев, когда `divisor` имеет степень двойки. Для чисел с плавающей запятой вычисление намного сложнее и занимает гораздо больше времени.

Поскольку целочисленные операции производятся гораздо быстрее, может показаться, что движку стоит использовать представление в виде дополнительного кода для всех целых чисел и операций над ними. К сожалению, такой подход будет являться нарушением спецификации ECMAScript. ECMAScript стандартизирует Float64, потому **некоторые целочисленные операции на самом деле создают числа с плавающей запятой**. В таких случаях важно, чтобы JS движки выдавали правильные результаты.

    // Float64 имеет безопасный целочисленный диапазон в 53 бита, вне которого
    // вы можете потерять точность вычислений.
    2**53 === 2**53+1;
    // → true
    
    // Float64 поддерживает отрицательные нули, потому -1 * 0 должен равняться -0, но
    // нет никакого способа представить отрицательный нуль в виде дополнительного кода.
    -1*0 === -0;
    // → true
    
    // Float64 имеет бесконечности, которые можно получить путем деления
    // на нуль.
    1/0 === Infinity;
    // → true
    -1/0 === -Infinity;
    // → true
    
    // Float64 также имеет NaNы.
    0/0 === NaN;

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

V8 использует специальное представление `Smi` для небольших целочисленных значений в диапазоне 31-битных целых чисел со знаком, для всех остальных - `HeapObject` который является адресом для некоего объекта в памяти. Для представления чисел, не попадающих в диапазон `Smi`, используется специальный тип `HeapObject`, который называется `HeapNumber`.

     -Infinity // HeapNumber
    -(2**30)-1 // HeapNumber
      -(2**30) // Smi
           -42 // Smi
            -0 // HeapNumber
             0 // Smi
           4.2 // HeapNumber
            42 // Smi
       2**30-1 // Smi
         2**30 // HeapNumber
      Infinity // HeapNumber
           NaN // HeapNumber

Как показано в примере выше, некоторые числа в JavaScript представлены, как `Smi`, остальные - `HeapNumber`. V8 особым образом оптимизирует `Smi`, поскольку небольшие целочисленные значения очень часто используются в реальных  JavaScript программах. Под `Smi` значения в памяти не нужно выделять отдельные участки, что позволяет ускорить операции над целыми числами в целом.

Важный момент в том, что, для оптимизации, **даже значения одного JavaScript типа могут быть представлены совершенно разными способами**.

### `Smi` против `HeapNumber` против `MutableHeapNumber`

Вот как это работает под капотом. Представим, что у вас есть объект такого вида:

    const o = {
      x: 42,  // Smi
      y: 4.2, // HeapNumber
    };

Значение `42` для `x` может кодироваться как `Smi`, потому может храниться внутри самого объекта. Значению `4.2` же для хранения значения необходим отдельный участок, на который будет ссылаться наш объект.

![https://v8.dev/_img/react-cliff/04-smi-vs-heapnumber.svg](https://v8.dev/_img/react-cliff/04-smi-vs-heapnumber.svg)

А теперь представим, что у нас есть код такого вида:

    o.x += 10;
    // → o.x теперь имеет значение 52
    o.y += 1;
    // → o.y теперь имеет значение 5.2

В первом случае значение `x` может быть обновлено на месте, поскольку новое значение `52` принадлежит диапазону `Smi`.

![https://v8.dev/_img/react-cliff/05-update-smi.svg](https://v8.dev/_img/react-cliff/05-update-smi.svg)

Однако, новое значение `y=5.2` не только не входит в диапазон `Smi`, но еще и отличается от предыдущего значения  `4.2`, потому V8 придется выделить новый `HeapNumber` участок для того, чтобы присвоить `y`.

![https://v8.dev/_img/react-cliff/06-update-heapnumber.svg](https://v8.dev/_img/react-cliff/06-update-heapnumber.svg)

`HeapNumber` иммутабелен, что позволяет проводить некоторые оптимизации. Например, если мы `x``у присвоим значение `y`:

    o.x = o.y;
    // → o.x теперь имеет значение 5.2

…и теперь мы можем просто связать его с тем же  `HeapNumber`, вместо того чтобы выделять новый участок памяти для такого же значения.

![https://v8.dev/_img/react-cliff/07-heapnumbers.svg](https://v8.dev/_img/react-cliff/07-heapnumbers.svg)

Один минус в иммутабельность `HeapNumber` в том, что процесс обновления полей со значениями, не попадающими в диапазон  `Smi`, будет замедляться. Например:

    // Создаем экземпляр `HeapNumber`.
    const o = { x: 0.1 };
    
    for (let i = 0; i < 5; ++i) {
      // Создаем дополнительный экземпляр `HeapNumber`.
      o.x += 1;
    }

Первая строка создаст экземпляр `HeapNumber` с начальным значением `0.1`. Тело цикла изменит это значение на `1.1`, `2.1`, `3.1`, `4.1` и, наконец, на `5.1`, создавая целых шесть экземпляров `HeapNumber` по пути, пять из которых будут являться мусором, как только цикл завершит работу.

![https://v8.dev/_img/react-cliff/08-garbage-heapnumbers.svg](https://v8.dev/_img/react-cliff/08-garbage-heapnumbers.svg)

Чтобы изюежать этого, V8 предоставляет возможность обновлять не-`Smi` числовые поля на месте. Когда числовое поле хранит значения, не входящие в диапазон `Smi`, V8 помечает это поле как `Double` и определяет `MutableHeapNumber`, который хранит фактическое значение, закодированное в Float64.

![https://v8.dev/_img/react-cliff/09-mutableheapnumber.svg](https://v8.dev/_img/react-cliff/09-mutableheapnumber.svg)

Окгда значение поля меняется, для V8 больше нет необходимости определять новый `HeapNumber`, потому он просто обновляет `MutableHeapNumber` на месте.

![https://v8.dev/_img/react-cliff/10-update-mutableheapnumber.svg](https://v8.dev/_img/react-cliff/10-update-mutableheapnumber.svg)

Однако, и в этом подходе есть подвох. Поскольку значение `MutableHeapNumber` может меняться, важно, чтобы оно никуда не передавалось.

![https://v8.dev/_img/react-cliff/11-mutableheapnumber-to-heapnumber.svg](https://v8.dev/_img/react-cliff/11-mutableheapnumber-to-heapnumber.svg)

Например, если вы присвоите `o.x` какой-нибудь другой переменной, вроде `y`, вы не захотите, чтобы значение `y` менялось вместе со значением `o.x` — это будет нарушением JavaScript спецификации! Поэтому, когда `o.x` станет доступен, число должно быть переконвертировано в обычный `HeapNumber` перед присвоением его `y`.

Для чисел с плавающей запятой V8 выполняет всю ранее описанную магию "конвертации" за кулисами, но такой подход будет избыточен для `MutableHeapNumber`, поскольку `Smi` в этом случае более эффективное представление.

    const object = { x: 1 };
    // → для `x` в объекте конвертация не производится
    
    object.x += 1;
    // → значение `x` обновляется внутри объекта

Чтобы избежать неэффективность, все что нам нужно сделать, так это пометить небольшие целочисленные значения, как `Smi`, и просто обновить числовое значение на месте, пока оно входит в диапазон.

![https://v8.dev/_img/react-cliff/12-smi-no-boxing.svg](https://v8.dev/_img/react-cliff/12-smi-no-boxing.svg)

## Устаревшие формы и миграции

А что, если поле первоначально содержит `Smi`, но после меняет значение на вне диапазона? Как в примере ниже, два объекта, использующих одинакового вида, где `x` изначально представлен в `Smi`:

    const a = { x: 1 };
    const b = { x: 2 };
    // → объекты имеют поля `x` в виде `Smi`
    
    b.x = 0.2;
    // → `b.x` меняет представление на `Double`
    
    y = a.x;

Начинается все с того, что объекты одного вида, где `x` представлен в виде `Smi`:

![https://v8.dev/_img/react-cliff/13-shape.svg](https://v8.dev/_img/react-cliff/13-shape.svg)

Когда представление `b.x` меняется на `Double`, V8 определяет новый вид, при которой `x` меняет представление на  `Double` и указывает на пустой вид. V8 также определяет `MutableHeapNumber` для хранения нового значения `0.2` свойства `x`. Потом мы обновляем объект `b`, чтобы он указывал на новый вид, и меняем слот объекта, чтобы он указывал на ранее выделенный `MutableHeapNumber` без смещения. И, наконец, мы помечаем старый вид как как устаревший и отвязываем его от дерева перехода. Это получается сделать с помощью нового перехода для `'x'` от пустой формы к только что созданной.

![https://v8.dev/_img/react-cliff/14-shape-transition.svg](https://v8.dev/_img/react-cliff/14-shape-transition.svg)

На жтом этапе мы не можем польностью избавиться от старой формы, поскольку она все еще используется `a`, и проход по памяти для нахождения и обновления всех объектов старой формы будет слишком дорогой операцией. Вместо того, V8 делает это лениво: доступ или присвоение любого свойства к `a` сначала мигрирует его к новой форме. Идея в том, чтобы по итогу сделать устаревшую форму недоступной и удалить ее с помощью сборщика мусора.

![https://v8.dev/_img/react-cliff/15-shape-deprecation.svg](https://v8.dev/_img/react-cliff/15-shape-deprecation.svg)

Более сложный случай случается, если свойство, меняющее представление, *не последнее* в цепи:

    const o = {
      x: 1,
      y: 2,
      z: 3,
    };
    
    o.y = 0.1;

В таком случае V8 придется найти так называемую *форму разделения*,  которая является предыдущей в цепочке до определения нужного свойства. Мы меняем `y`, потому нам нужно найти ее предпоследнюю форму, которой, в нашем примере, будет являться `x`.

![https://v8.dev/_img/react-cliff/16-split-shape.svg](https://v8.dev/_img/react-cliff/16-split-shape.svg)

Начиная с формы разделения, мы создаем новую цепочку переходов для  `y`, который повторяет все прошлые переходы с пометкой  `'y'` в `Double` представлении, и используем ее, помечая все старые поддеревья как устаревшие. Последним шагом мы мигрируем экземпляр `o` к новой форме, с помощью `MutableHeapNumber`. Таким образом, новые объекты не идут старым путем, и, как только все ссылки на старые значения будут удалены, часть дерева со старой формой исчезнет.

## Расширяемость и переходы целостного уровня

`Object.preventExtensions()` предотвращает добавление объекту новых свойств, выбрасывая исключение. (в нестрогом режиме работает аналогично, но не выбрасывает исключение)

    const object = { x: 1 };
    Object.preventExtensions(object);
    object.y = 2;
    // TypeError: Cannot add property y;
    //            object is not extensible

`Object.seal` делает то же, что и `Object.preventExtensions`, но также помечает все свойства, как неперенастраиваемые. Это значит, что их нельзя удалять и менять их перечислимость, конфигурируемость и перезаписываемость.

    const object = { x: 1 };
    Object.seal(object);
    object.y = 2;
    // TypeError: Cannot add property y;
    //            object is not extensible
    delete object.x;
    // TypeError: Cannot delete property x

`Object.freeze` делает то же, что и `Object.seal`, но также предотвращает изменения значений существующих свойств, помечая их как неперезаписываемые.

    const object = { x: 1 };
    Object.freeze(object);
    object.y = 2;
    // TypeError: Cannot add property y;
    //            object is not extensible
    delete object.x;
    // TypeError: Cannot delete property x
    object.x = 3;
    // TypeError: Cannot assign to read-only property x

Давайте рассмотрим пример, когда у нас есть два объекта со свойством `x`, и ограничим добавление новых свойств у второго.

    const a = { x: 1 };
    const b = { x: 2 };
    
    Object.preventExtensions(b);

Как мы уже знаем, все начнется с перехода от пустой формы к новой, которая хранит свойство `'x'` (представленное как `Smi`). Когда мы предотвращаем добавление свойств объекта`b`, мы выполняем специальный переход к новой форме, помеченной как неперенастраиваемая. Этот переход не вводит никаких новых свойств, а служит простым маркером.

![https://v8.dev/_img/react-cliff/17-shape-nonextensible.svg](https://v8.dev/_img/react-cliff/17-shape-nonextensible.svg)

Заметьте, мы не можем просто обновить форму `x` на месте, поскольку она все еще используется все еще расширяемым объектом `a`.

## Та самая проблема производительности в React

Давайте соберем все вместе и попробуем разобраться с [недавней React проблемой #14365](https://github.com/facebook/react/issues/14365). Когда разработчики из команды React разрабатывали реальное приложение, они заметили странный обвал производительность V8, который влиял на ядро React. Вот упрощенное воспроизведение этого бага:

    const o = { x: 1, y: 2 };
    Object.preventExtensions(o);
    o.y = 0.2;

Есть объект с двумя свойствами, представленными как `Smi`. Мы ограничиваем добавление свойств к нему и насильно меняем представление второго свойства на `Double`.

Как мы разобрались раньше, это приведёт к примерно следующей настройке:

![https://v8.dev/_img/react-cliff/18-repro-shape-setup.svg](https://v8.dev/_img/react-cliff/18-repro-shape-setup.svg)

Оба свойства представлены как `Smi`, и финальным переходом будет переход расширяемости, чтобы пометить форму как неперенастраиваемую.

Теперь нам нужно изменить представление `y` на `Double`, и, значит, нужно снова начать с поиска разделительной формы. В этом случае, искомая форма - это `x`. И здесь V8 начинает путаться, поскольку разделительная форма помечена как настраиваемая, а текущая - ненастраиваемая. V8, по сути, не знает как правильно повторить переходы в этом случае, потому перестаёт пытаться разобраться, и, вместо этого создает раздельную форму, которая не связана с уже существующим деревом и не используется другими объектами. Ее можно назвать *сиротской формой*:

![https://v8.dev/_img/react-cliff/19-orphaned-shape.svg](https://v8.dev/_img/react-cliff/19-orphaned-shape.svg)

Можете представить, насколько плохи дела, когда такое поведение применяется к большому количеству объектов. Вся система форм становится бесполезной.

В случае с React, происходит следующее: каждый `FiberNode` имеет несколько свойств, которые должны хранить временные метки при включенном профилировании.

    class FiberNode {
      constructor() {
        this.actualStartTime = 0;
        Object.preventExtensions(this);
      }
    }
    
    const node1 = new FiberNode();
    const node2 = new FiberNode();

Поля, вроде `actualStartTime`, инициализируются со значениями `0` или `-1`, таким образом начиная со `Smi` представления. Но позже, временные метки из `[performance.now()](https://w3c.github.io/hr-time/#dom-performance-now)`, хранящиеся в этих полях и являющиеся числами с плавающей запятой, заставляют их менять представление на `Double`, поскольку больше не помещаются в диапазон`Smi`. Более того, React также ограничивает перенастраиваемость для экземпляров `FiberNode`.

Первоначально упрощенный пример выглядел следующим образом:

![https://v8.dev/_img/react-cliff/20-fibernode-shape.svg](https://v8.dev/_img/react-cliff/20-fibernode-shape.svg)

Есть два экземпляра, которые используют дерево форм. Все работает, как задумано. Но как только вы начинаете хранить настоящие временные метки, V8 начинает путаться и не может найти разделительную форму:

![https://v8.dev/_img/react-cliff/21-orphan-islands.svg](https://v8.dev/_img/react-cliff/21-orphan-islands.svg)

V8 присваивает новую сиротскую форму к `node1`, и та же вещь произойдет с `node2` позже. В результате мы получаем два *сиротских острова* с непересекающимися формами. А большинство реальных React приложений имеют не два, а десятки тысяч этих `FiberNode` Как вы можете представить, ситуация для производительности V8 складывается не лучшим образом.

К счастью, [мы починили эту утечку](https://chromium-review.googlesource.com/c/v8/v8/+/1442640/) в [V8 версии 7.4](https://v8.dev/blog/v8-release-74) и [ищем способ облегчить операцию по смене представления](https://bit.ly/v8-in-place-field-representation-changes), чтобы исправить оставшиеся утечки. Вместе с этим фиксом V8 начал делать всё правильно:

![https://v8.dev/_img/react-cliff/22-fix.svg](https://v8.dev/_img/react-cliff/22-fix.svg)

Два экземпляра `FiberNode` указывают на неперенастраиваемую форму, где `'actualStartTime'` является свойством в`Smi`. Когда происходит первое присвоение `node1.actualStartTime`, новая цепочка переходов создается, а старая помечается устаревшей:

![https://v8.dev/_img/react-cliff/23-fix-fibernode-shape-1.svg](https://v8.dev/_img/react-cliff/23-fix-fibernode-shape-1.svg)

Обратите внимание, что теперь переход расширяемости повторяется в новой цепочке правильным образом.

![https://v8.dev/_img/react-cliff/24-fix-fibernode-shape-2.svg](https://v8.dev/_img/react-cliff/24-fix-fibernode-shape-2.svg)

После присвоения `node2.actualStartTime`, оба узла ссылаются на новую форму, а помеченные устаревшими части дерева переходов могут быть удалены сборщиком мусора.

**Примечание:** Вы можете подумать, что все эти формы и миграции - сложная штука, и будете правы. На деле, у нас есть подозрения, что на реальных вебсайтах это вызывает больше проблем (с точки зрения производительности, использования памяти и комплексности), чем помогает. В особенности потому вместе с [pointer compression](https://bugs.chromium.org/p/v8/issues/detail?id=7703) мы больше не сможем хранить двойные значения в свойствах объекта. Потому мы надеемся  [полностью удалить механизм устаревания форм V8](https://bugs.chromium.org/p/v8/issues/detail?id=9606). Можно сказать, что он сам по себе **надевает крутые очки** устарел. ЕЕЕЕ*…*

Команда React [уменьшили проблему на своей стороне](https://github.com/facebook/react/pull/14383), убедившись, что все свойства со значениями времени и продолжительности в `FiberNode` сразу представлены как `Double`:

    class FiberNode {
      constructor() {
        // Представление `Double` присваивается насильно.
        this.actualStartTime = Number.NaN;
        // И вы все еще можете присвоить свойству нужное значение:
        this.actualStartTime = 0;
        Object.preventExtensions(this);
      }
    }
    
    const node1 = new FiberNode();
    const node2 = new FiberNode();

Кроме `Number.NaN` может быть использовано любое значение с плавающей запятой, не входящее в диапазон `Smi`. Например, `0.000001`, `Number.MIN_VALUE`, `-0` и `Infinity`.

Стоит отметить, что конкретно баг React`а был специфичен для определенной версии V8, и, в целом, разработчики не должны проводить привязывать оптимизации конкретной версии движка. Но все же, хорошо иметь решение, когда что-то не хочет работать.

Имейте ввиду, что JavaScript движок под капотом выполняет некоторую магию, которой вы можете помочь не смешивая типы. Например, не инициализируйте числовые свойства с помощью `null`, так как это лишит вас всех преимуществ отслеживания представлений свойств:

    // Не делайте так!
    class Point {
      x = null;
      y = null;
    }
    
    const p = new Point();
    p.x = 0.1;
    p.y = 402;

Другими словами, **пишите читабельный код,  и производительность придет сама!**

## Заключение

Мы глубже рассмотрели следующие темы:

- JavaScript различает “примитивы” и “объекты”, а `typeof`  - лжец.
- Даже значения одинакового JavaScript типа за кулисами могут представляться по-разному.
- V8 пытается отыскать оптимальное представление для каждого свойства в ваших JavaScript программах.
- Обсудили, как V8 разбирается с устаревшими формами и миграциями, включая переходы расширяемости.

Основываясь на этом, для ускорения производительности можно выделить несколько практичных советов по написанию JavaScript кода:

- Всегда инициализируйте объекты одним способом, чтобы формы были более эффективны.
- Разумно выбирайте начальные значения для свойств, чтобы помочь JavaScript движку с выбором представления.