React renderToString () производительность и кэширование компонентов React

я заметил, что reactDOM.renderToString() метод начинает значительно замедляться при отрисовке большого дерева компонентов на сервере.

фон

немного предыстории. Система представляет собой полностью изоморфный стек. Самый высокий уровень App компонент отображает шаблоны, страницы, элементы dom и другие компоненты. Глядя в коде react, я обнаружил, что он отображает ~1500 компонентов (включая любой простой тег dom, который обрабатывается как простой компонент, <p>this is a react component</p>.

в развитии, представлять ~ 1500 компонентов принимает ~200-300мс. Удалив некоторые компоненты, я смог получить ~1200 компонентов для рендеринга в ~175-225ms.

в продукции, renderToString на ~ 1500 компонентах принимает вокруг ~50-200ms.

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

это создает некоторые проблемы на сервере. Длительный метод приводит к длительное время ответа сервера. TTFB намного выше, чем должно быть. С вызовами api и бизнес-логикой ответ должен быть 250ms, но с 250ms renderToString он удваивается! Плохо для SEO и пользователей. Кроме того, будучи синхронным методом,renderToString() может блокировать сервер узлов и создавать резервные копии последующих запросов (это можно решить, используя 2 отдельных сервера узлов: 1 в качестве веб-сервера и 1 в качестве службы исключительно для рендеринга react).

попытки

в идеале, это займет 5-50ms к renderToString в продукции. Я работал над некоторыми идеями, но я не совсем уверен, каким будет лучший подход.

Идея 1: кэширование компонентов

любой компонент, помеченный как "статический", может быть кэширован. Сохраняя кэш с отображаемой разметкой,renderToString() можно проверить кэш перед отрисовкой. Если он находит компонент, он автоматически захватывает строку. Выполнение этого на компоненте высокого уровня сохранит все вложенные дочерние компоненты монтаж. Вам нужно будет заменить rootID реакции разметки кэшированного компонента на текущий rootID.

Идея 2: маркировка компонентов, как простой/тупой

определив компонент как "простой", react должен иметь возможность пропускать все методы жизненного цикла при рендеринге. React уже делает это для основных компонентов react dom (<p/>, <h1/> и т. д.). Было бы неплохо расширить пользовательские компоненты, чтобы использовать ту же оптимизацию.

Идея 3: пропустить компоненты серверная визуализация

компоненты, которые не должны быть возвращены сервером (без значения SEO), могут быть просто пропущены на сервере. После загрузки клиента установите clientLoaded флаг true и передайте его, чтобы принудительно выполнить рендеринг.

закрытие и другие попытки

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

некоторые проекты, которые мы рассматриваем включить:

кто-нибудь сталкивался с подобными проблемами? Что вы смогли сделать? Спасибо.

4 ответов


используя реагировать-маршрутизатор 1.0 и react0.14, мы по ошибке были сериализовать объект нашего потока несколько раз.

RoutingContext будем называть createElement для каждого шаблона в ваших маршрутах react-router. Это позволяет вам вводить любые реквизиты, которые вы хотите. Мы также используем флюс. Мы отправляем вниз сериализованную версию большого объекта. В нашем случае, мы делали flux.serialize() внутри createElement. Метод сериализации может занять ~20 мс. С 4 шаблонами это будет дополнительный 80ms для вашего 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() время от ~120ms до ~30ms. (Вам все равно нужно добавить 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') в любом месте в вашем приложении.

Webpack Примечание: если вы используете что-то вроде 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');

его не полное решение У меня была та же проблема с моим изоморфным приложением react , и я использовал пару вещей.

1) Используйте Nginx перед вашим сервером nodejs и кэшируйте отображаемый ответ в течение короткого времени.

2 )в случае отображения списка элементов я использую только подмножество списка. например, я буду отображать только X элементов для заполнения окна просмотра и загружать остальную часть списка на стороне клиента с помощью Websocket или XHR.

3) Некоторые из моих компоненты пусты в рендеринге на стороне сервера и загружаются только со стороны клиента (componentDidMount). Эти компоненты обычно являются графиками или компонентами, связанными с профилем. эти компоненты обычно не имеют никакой выгоды с точки зрения SEO

4) о SEO , из моего опыта 6 месяцев с изоморфным приложением. Google Bot может легко читать веб-страницу React на стороне клиента , поэтому я не уверен, почему мы беспокоимся о рендеринге на стороне сервера.

5)сохранить <Head >и <Footer> в качестве статической строки или используйте template engine (Reactjs-handellbars), и отображать только содержимое страницы ( он должен сохранить несколько компонентов renderd). В случае одностраничного приложения вы можете обновить описание заголовка в каждой навигации внутри 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-react-server, в этом случае рендеринг будет в 14 раз быстрее, чем традиционный рендеринг react. Но для этого каждый компонент, который вы хотите отобразить, должен быть объявлен с ним (см. пример в fast-react-seed, как вы можете сделать это для webpack).