React renderToString() Производительность и компоненты кэширования React
Я заметил, что reactDOM.renderToString()
Метод начинает значительно тормозить при рендеринге большого дерева компонентов на сервере.
Фон
Немного предыстории. Система является полностью изоморфным стеком. Высочайший уровень App
Компонент отображает шаблоны, страницы, элементы DOM и другие компоненты. Посмотрев код реакции, я обнаружил, что он рендерит ~1500 компонентов (это включает любой простой тег dom, который рассматривается как простой компонент, <p>this is a react component</p>
,
В разработке рендеринг ~1500 компонентов занимает ~200-300мс. Удалив некоторые компоненты, я смог получить ~1200 компонентов для визуализации за ~175-225 мс.
В производстве renderToString на ~1500 компонентов занимает около 50-200 мс.
Время кажется линейным. Ни один компонент не является медленным, скорее это сумма многих.
проблема
Это создает некоторые проблемы на сервере. Длительный метод приводит к увеличению времени ответа сервера. TTFB намного выше, чем должен быть. При вызовах API и бизнес-логике ответ должен составлять 250 мс, а при рендеринге 250 мс он удваивается! Плохо для SEO и пользователей. Кроме того, будучи синхронным методом, renderToString()
может блокировать сервер узлов и создавать резервные копии последующих запросов (это можно решить, используя 2 отдельных сервера узлов: 1 в качестве веб-сервера и 1 в качестве службы, предназначенной исключительно для реагирования).
попытки
В идеале, для рендеринга ToString в производстве потребуется 5-50 мс. Я работал над некоторыми идеями, но я не совсем уверен, какой будет лучший подход.
Идея 1: Кэширование компонентов
Любой компонент, помеченный как "статический", может быть кэширован. Сохраняя кэш с обработанной разметкой, renderToString()
мог проверить кеш перед рендерингом. Если он находит компонент, он автоматически захватывает строку. Выполнение этого на компоненте высокого уровня сохранит монтирование всех вложенных дочерних компонентов. Вам придется заменить реагирующий rootID размеченного компонента на текущий rootID.
Идея 2: Маркировка компонентов как простых / немых
Определяя компонент как "простой", response должен иметь возможность пропустить все методы жизненного цикла при рендеринге. Реакт уже делает это для основных компонентов реактива дом (<p/>
, <h1/>
, так далее). Было бы неплохо расширить пользовательские компоненты для использования той же оптимизации.
Идея 3: Пропустить компоненты при рендеринге на стороне сервера
Компоненты, которые не должны быть возвращены сервером (без значения SEO), могут быть просто пропущены на сервере. Как только клиент загрузится, установите clientLoaded
флаг для true
и передайте это вниз, чтобы провести повторную визуализацию.
Закрытие и другие попытки
Единственное решение, которое я реализовал до сих пор, - это уменьшить количество компонентов, отображаемых на сервере.
Некоторые проекты, которые мы рассматриваем, включают в себя:
- React-dom-stream https://github.com/aickin/react-dom-stream (все еще работает над реализацией этого для теста)
- Встроенные элементы Babel https://babeljs.io/docs/plugins/transform-react-inline-elements/ (похоже, что это соответствует идее 2)
Кто-нибудь сталкивался с подобными проблемами? Что ты смог сделать? Благодарю.
4 ответа
Используя response-router1.0 и реагировать на 0.14, мы ошибочно сериализовали наш объект потока несколько раз.
RoutingContext
позвоню createElement
для каждого шаблона в ваших маршрутах реакции-маршрутизатора. Это позволяет вам вводить любые реквизиты, которые вы хотите. Мы также используем флюс. Мы отправляем сериализованную версию большого объекта. В нашем случае мы делали flux.serialize()
внутри createElement. Метод сериализации может занять ~20 мс. С 4 шаблонами, это будет дополнительные 80 мс к вашему renderToString()
метод!
Старый код:
function createElement(Component, props) {
props = _.extend(props, {
flux: flux,
path: path,
serializedFlux: flux.serialize();
});
return <Component {...props} />;
}
var start = Date.now();
markup = renderToString(<RoutingContext {...renderProps} createElement={createElement} />);
console.log(Date.now() - start);
Легко оптимизируется для этого:
var serializedFlux = flux.serialize(); // serialize one time only!
function createElement(Component, props) {
props = _.extend(props, {
flux: flux,
path: path,
serializedFlux: serializedFlux
});
return <Component {...props} />;
}
var start = Date.now();
markup = renderToString(<RoutingContext {...renderProps} createElement={createElement} />);
console.log(Date.now() - start);
В моем случае это помогло уменьшить renderToString()
время от ~120мс до ~30мс. (Вам все еще нужно добавить 1x serialize()
~20 мс, что происходит до renderToString()
) Это было приятное быстрое улучшение. - Важно помнить, что всегда нужно делать что-то правильно, даже если вы не знаете немедленного воздействия!
Идея 1: Кэширование компонентов
Обновление 1: я добавил полный рабочий пример внизу. Кэширует компоненты в памяти и обновления data-reactid
,
Это на самом деле можно сделать легко. Вы должны обезьяна-патч ReactCompositeComponent
и проверьте кешированную версию:
import ReactCompositeComponent from 'react/lib/ReactCompositeComponent';
const originalMountComponent = ReactCompositeComponent.Mixin.mountComponent;
ReactCompositeComponent.Mixin.mountComponent = function() {
if (hasCachedVersion(this)) return cache;
return originalMountComponent.apply(this, arguments)
}
Вы должны сделать это перед вами require('react')
в любом месте вашего приложения.
Примечание: если вы используете что-то вроде new webpack.ProvidePlugin({'React': 'react'})
вы должны изменить его на new webpack.ProvidePlugin({'React': 'react-override'})
где вы делаете ваши изменения в react-override.js
и экспорт react
(т.е. module.exports = require('react')
)
Полный пример, который кеширует в памяти и обновляет reactid
атрибут может быть таким:
import ReactCompositeComponent from 'react/lib/ReactCompositeComponent';
import jsan from 'jsan';
import Logo from './logo.svg';
const cachable = [Logo];
const cache = {};
function splitMarkup(markup) {
var markupParts = [];
var reactIdPos = -1;
var endPos, startPos = 0;
while ((reactIdPos = markup.indexOf('reactid="', reactIdPos + 1)) != -1) {
endPos = reactIdPos + 9;
markupParts.push(markup.substring(startPos, endPos))
startPos = markup.indexOf('"', endPos);
}
markupParts.push(markup.substring(startPos))
return markupParts;
}
function refreshMarkup(markup, hostContainerInfo) {
var refreshedMarkup = '';
var reactid;
var reactIdSlotCount = markup.length - 1;
for (var i = 0; i <= reactIdSlotCount; i++) {
reactid = i != reactIdSlotCount ? hostContainerInfo._idCounter++ : '';
refreshedMarkup += markup[i] + reactid
}
return refreshedMarkup;
}
const originalMountComponent = ReactCompositeComponent.Mixin.mountComponent;
ReactCompositeComponent.Mixin.mountComponent = function (renderedElement, hostParent, hostContainerInfo, transaction, context) {
return originalMountComponent.apply(this, arguments);
var el = this._currentElement;
var elType = el.type;
var markup;
if (cachable.indexOf(elType) > -1) {
var publicProps = el.props;
var id = elType.name + ':' + jsan.stringify(publicProps);
markup = cache[id];
if (markup) {
return refreshMarkup(markup, hostContainerInfo)
} else {
markup = originalMountComponent.apply(this, arguments);
cache[id] = splitMarkup(markup);
}
} else {
markup = originalMountComponent.apply(this, arguments)
}
return markup;
}
module.exports = require('react');
Это не полное решение У меня была та же проблема, с моим изоморфным приложением реагировать, и я использовал несколько вещей.
1) Используйте Nginx перед вашим сервером nodejs и короткое время кешируйте полученный ответ.
2) В случае показа списка элементов я использую только подмножество списка. например, я буду отображать только элементы X, чтобы заполнить область просмотра, и загрузить остальную часть списка на стороне клиента, используя Websocket или XHR.
3) Некоторые из моих компонентов пусты при рендеринге на стороне сервера и будут загружаться только из кода на стороне клиента (componentDidMount). Этими компонентами обычно являются графики или компоненты, связанные с профилем. эти компоненты обычно не имеют никакой пользы с точки зрения SEO
4) О SEO, из моего опыта 6 месяцев с изоморфным приложением. Google Bot может легко читать веб-страницу React на стороне клиента, поэтому я не уверен, почему мы беспокоимся о рендеринге на стороне сервера.
5) Держите <Head >
а также <Footer>
в качестве статической строки или использовать шаблонизатор ( Reactjs-handellbars) и отображать только содержимое страницы (это должно сохранить несколько визуализированных компонентов). В случае одностраничного приложения вы можете обновить описание заголовка в каждой навигации внутри Router.Run
,
Я думаю, что быстрая реакция может помочь вам. Это увеличивает производительность вашего сервера рендеринга в три раза.
Для этого вам нужно всего лишь установить пакет и заменить ReactDOM.renderToString на FastReactRender.elementToString:
var ReactRender = require('fast-react-render');
var element = React.createElement(Component, {property: 'value'});
console.log(ReactRender.elementToString(element, {context: {}}));
Также вы можете использовать fast-реагирующий сервер, в этом случае рендеринг будет в 14 раз быстрее, чем традиционный рендеринг реакции. Но для этого каждый компонент, который вы хотите визуализировать, должен быть объявлен вместе с ним (см. Пример в fast-response-seed, как вы можете сделать это для веб-пакета).