Когда виртуальный DOM имеет смысл?

Виртуальный DOM - одна из причин использования React. В предыдущем посте я продемонстрировал, что React ускоряет изменение существующих представлений и существенно не замедляет рендеринг новых представлений. В качестве образца представления я использовал страницу с таблицей, содержащей 40 000 ячеек. Ячейки таблицы примитивов отображали свои координаты в виде содержимого и заголовков. Отрисовка неоптимизированной таблицы заняла так много времени, что измеримые накладные расходы React, несомненно, были незначительными. В этом посте я хочу исследовать, может ли задержка рендеринга, вызванная React, быть ощутимой, если стили страницы оптимизированы для максимального сокращения времени рендеринга страницы.

Я повторю эксперимент, который описал ранее, но на этот раз я поэкспериментирую с более крупной таблицей, которая визуализируется в кратчайшие сроки, потому что она стилизована с производительностью рендеринга, улучшающей свойства CSS. Вы можете увидеть образец страницы по адресу https://reactvsnoreact.onrender.com/:

Код для отображения таблицы без React:

function row(vals) {
    const rowDiv = document.createElement("div");
    rowDiv.className = 'row';
    rowDiv.append(...vals.map(val => {
        const cell = document.createElement("div");
        cell.title = val;
        cell.append(val);
        return cell;
    }));
    return rowDiv;
}
function table(data) {
    return data.map(vals => row(vals));
}
export function render(data) {
    root.replaceChildren(...table(data));
}

По сути, функция render(data) преобразует исходный массив 5000 * 20 строк в массив из 5000 div, действующих как строки, каждая из которых содержит 20 div ячеек. Я использовал сокращенные методы append() и replaceChildren(), чтобы немного упростить код. Узлы таблиц вставляются в div с id root.

Эквивалентный непереносимый JSX-код имеет аналогичную структуру:

function Row({ row }) {
    return <div className="row">
        {row.map((text, i) => <div key={i} title={text}>{text}</div>)}
    </div>
}
function Table({ rows }) {
    return rows.map((row, i) => <Row key={i} row={row}/>);
}
export function render(data) {
    ReactDOM.render(<Table rows={data} />, root);
}

ReactDOM.render() вставляет узлы таблиц в тот же div с id root.

Во время измерений коды React-free и React-based используют одни и те же исходные данные для создания таблицы.

В моем эксперименте я измеряю общее время от вызова render(data) до момента, когда браузер возобновляет выполнение JavaScript после того, как отрисовывает изменения в DOM:

export function execute(label, render) {
    return new Promise(resolve => {
        requestAnimationFrame(() => {
            const start = Date.now();
            render();
            setTimeout(() => {
                addResult(label, start, Date.now());
                resolve();
            });
        });
    });
}

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

Результаты сравнительного анализа

Задержка, вызванная React

Сначала я измеряю и сравниваю время, необходимое для отображения тестовой таблицы, используя показанный выше код React-free и React-based. Посмотрите на первые две строки таблицы результатов. Среднее время показывает, что React задерживает рендеринг нового дерева узлов на ~ (575–469) / 469 = 22%. В% разница кажется довольно значительной. Но с другой стороны, пользователи не будут очень благодарны, если страница появится на их экранах на 575–469 = 106 мс раньше. Я бы сказал, что React существенно не задерживает отрисовку структурированного содержимого с оптимальным стилем.

Более быстрый просмотр обновлений с помощью React

Виртуальная DOM становится полезной, когда код изменяет не всю DOM, а только несколько ее элементов. В следующей части эксперимента, после отображения таблицы, код отображает другую таблицу, которая отличается от ранее отображаемой таблицы в ячейке с координатами 10,20 . В одной таблице эта ячейка содержит ее координаты, в альтернативной таблице значение ячейки заменяется строкой TEST:

Две таблицы создаются с использованием двух разных массивов исходных данных. Альтернативный массив отличается от первого только одним элементом, содержащим TEST вместо 10,20.

Результаты отображаются в строках, начиная с третьей. Строки с именами noreact cell или react cell содержат время, затраченное на замену альтернативной таблицы первой таблицей. И наоборот, строки noreact cell2 и react cell2 содержат время, потраченное на замену первой таблицы альтернативной таблицей.

По результатам видно, что при отрисовке изменений в DOM браузер не выполняет никаких оптимизаций на основе предыдущего содержимого DOM. Если дочерние элементы заменяются идентичными или почти идентичными, браузер тратит на рисование столько же времени (460 и 439 мс), сколько он тратит на рисование тех же элементов, вставленных в пустой родительский элемент (469 мс).

Напротив, точечные изменения в DOM, вводимые посредством виртуальной DOM (108 и 103 мс), появляются на экране намного быстрее, чем вся таблица, вставленная в пустой контейнер (575 мс). Это ожидаемый результат, демонстрирующий назначение виртуальной DOM. Однако визуально разница между временем, необходимым для обновлений с React 108 мс, и временем, необходимым для перерисовки всей таблицы без React 469 мс, невелика.

Выводы

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

Образец кода можно загрузить с https://github.com/marianc000/reactVsNoReact, и вы можете поиграть с образцом страницы https://reactvsnoreact.onrender.com/