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


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

фон

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

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

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

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

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

попытки

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

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

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

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

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

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

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

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

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

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

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

4 54

4 ответа:

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

RoutingContext будем называть createElement для каждого шаблона в ваших маршрутах react-router. Это позволяет вам вводить любые реквизиты, которые вы хотите. Мы также используем поток. Мы посылаем вниз сериализованную версию большого объекта. В нашем случае, мы делали 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() ' s ~20 мс в общей сложности, что происходит до renderToString()) Это было хорошее быстрое улучшение. -- Важно помнить, чтобы всегда поступать правильно, даже если вы не знаете последствий!

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

обновление 1: я добавил полный рабочий пример внизу. Он кэширует компоненты в памяти и обновляет data-reactid.

это действительно можно сделать легко. Вы должны monkey-patchReactCompositeComponent и проверьте наличие кэшированной версии:

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> в качестве статической строки или использовать шаблонный движок ( Reactjs-handellbars), и отображать только содержимое страницы, (он должен сохранить несколько компонентов renderd). В случае одностраничного приложения вы можете обновить описание заголовка в каждой навигации внутри Router.Run.

Я думаю быстро реагировать рендерить могу помочь вам. Это увеличивает производительность рендеринга вашего сервера в три раза.

для попробуйте, вам только нужно установить пакет и заменить ReactDOM.рендертостринг в FastReactRender.elementToString:

var ReactRender = require('fast-react-render');

var element = React.createElement(Component, {property: 'value'});
console.log(ReactRender.elementToString(element, {context: {}}));

также вы можете использовать быстро реагировать-сервера, в этом случае рендеринг будет в 14 раз быстрее, чем традиционный рендеринг react. Но для этого каждый компонент, который вы хотите отобразить, должен быть объявлен с его помощью (см. пример в fast-react-seed, как вы можете сделать это для webpack).