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

Ключевая идея этого метода заключается в использовании простого алгоритма сравнения/исправления вместо рендеринга целых страниц с нуля.

Поскольку алгоритм diff/patch применяется к существующему состоянию, нам нужно будет его сгенерировать. Он будет представлен в виде древовидной структуры данных с узлами, которые содержат узлы DOM в строковом формате и экземпляры компонентов.

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

Чертежи

Blueprint — это древовидная структура данных, которая содержит предварительно визуализированные узлы DOM в строковом формате и экземпляры компонентов.

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

Генерация схемы — это простой рекурсивный алгоритм, который проходит через дерево Virtual DOM и создает соответствующие узлы схемы.

Узлы элементов Blueprint должны содержать ссылки на дочерние экземпляры и предварительно обработанные строки для открытия и закрытия элемента. Предварительно обработанные строки должны быть разделены на две части, поскольку дочерние узлы могут содержать динамическое содержимое.

interface BlueprintElementNode {
  vnode: VNode;
  children: VNodeInstance[];
  openString: string;
  closeString: string;
}

Текстовые узлы Blueprint должны содержать содержимое в экранированном формате.

interface BlueprintTextNode {
  vnode: VNode;
  content: string;
}

Узлы компонента Blueprint должны содержать ссылку на экземпляр компонента и на корневой узел компонента.

interface BlueprintComponentNode {
  vnode: VNode;
  instance: Component;
  root: VNodeInstance;
}

Эта простая структура позволит нам реализовать эффективный рендеринг diff/patch.

Рендеринг в строку

Средство визуализации строк должно быть реализовано как чистое средство визуализации, которое не использует чертежи и средство визуализации различий/исправлений. Чистый рендерер будет использоваться для создания динамических элементов, которых не было в блупринте, и в качестве запасного варианта, когда не стоит выполнять diff/patch, поэтому мы можем просто перерендерить какую-то часть страницы.

Чистый рендерер может быть реализован точно так же, как традиционные реализации renderToString(node: VNode). Этот метод никак не повлияет на его производительность, поэтому мы можем улучшить его, только применяя diff/patch к частям страницы, которые не слишком сильно меняются.

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

Также важно, чтобы средство визуализации различий/исправлений не тратило значительное количество времени на сравнение, поэтому вместо того, чтобы пытаться найти мелкие изменения, мы будем выполнять простые проверки подлинности входных параметров и перестраивать строки, когда что-то изменяется. Большую часть времени мы будем перестраивать openString в BlueprintElementNode.

Доказательство концепции

Здесь — простое доказательство реализации концепции с использованием библиотеки Virtual DOM ivi, она была реализована за пару часов и, вероятно, есть много способов сделать это еще быстрее. Например, мы можем применить diff/patch к чертежам и разделить части деревьев между разными чертежами, чтобы уменьшить использование памяти.

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

Ориентиры

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

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

Этот тест отображает структуру html, аналогичную заголовку Medium. В основном это статический рендеринг, но он также содержит несколько простых динамических частей.

Вот результаты этого теста (Node 6.10):

render from scratch x 73,017 ops/sec ±0.37% (94 runs sampled)
apply diff/patch x 622,810 ops/sec ±0.74% (87 runs sampled)
Fastest is apply diff/patch

Реальные приложения, которые содержат нетривиальный код в render методах, должны оказывать гораздо большее влияние на эту оптимизацию. Этот бенчмарк не содержит какой-либо специфичной для приложения логики, которой можно было бы избежать в реальном приложении с оптимизацией shouldComponentUpdate().

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