web-dev-qa-db-ja.com

React renderToString()パフォーマンスとキャッシュReactコンポーネント

サーバー上で大きなコンポーネントツリーをレンダリングすると、reactDOM.renderToString()メソッドの速度が大幅に低下することに気付きました。

バックグラウンド

背景のビット。システムは完全に同型のスタックです。最高レベルのAppコンポーネントは、テンプレート、ページ、dom要素などのコンポーネントをレンダリングします。反応コードを見ると、〜1500個のコンポーネントをレンダリングしていることがわかりました(これには、単純なコンポーネント_<p>this is a react component</p>_として扱われる単純なdomタグが含まれます)。

開発では、最大1500個のコンポーネントのレンダリングに最大200〜300ミリ秒かかります。いくつかのコンポーネントを削除することで、最大175〜225ミリ秒でレンダリングする最大1200のコンポーネントを取得できました。

本番環境では、〜1500個のコンポーネントのrenderToStringには約50〜200msかかります。

時間は線形に見えます。 1つのコンポーネントが遅いのではなく、多くのコンポーネントの合計です。

問題

これにより、サーバー上でいくつかの問題が発生します。長い方法では、サーバーの応答時間が長くなります。 TTFBは本来よりもはるかに高いです。 API呼び出しとビジネスロジックでは、応答は250ミリ秒になりますが、250ミリ秒のrenderToStringでは2倍になります! SEOとユーザーにとって悪い。また、同期メソッドであるrenderToString()は、ノードサーバーをブロックし、後続のリクエストをバックアップできます(これは、2つの個別のノードサーバーを使用して解決できます。1つはWebサーバーとして、1つはサービスとして)。

試み

理想的には、本番環境でrenderToStringに5〜50ミリ秒かかります。私はいくつかのアイデアに取り組んできましたが、最善のアプローチが何であるか正確にはわかりません。

アイデア1:コンポーネントのキャッシュ

「静的」とマークされているコンポーネントはキャッシュできます。レンダリングされたマークアップでキャッシュを保持することにより、renderToString()はレンダリングの前にキャッシュをチェックできます。コンポーネントが見つかると、自動的に文字列を取得します。高レベルのコンポーネントでこれを行うと、ネストされた子コンポーネントのマウントをすべて節約できます。キャッシュされたコンポーネントマークアップの反応するrootIDを現在のrootIDに置き換える必要があります。

アイデア2:コンポーネントをシンプル/ダムとしてマークする

コンポーネントを「シンプル」として定義することで、reactはレンダリング時にすべてのライフサイクルメソッドをスキップできるはずです。 Reactは、コア反応domコンポーネント(_<p/>_、_<h1/>_など)に対して既にこれを行っています。同じ最適化を使用するようにカスタムコンポーネントを拡張すると便利です。

アイデア3:サーバー側のレンダリングでコンポーネントをスキップする

サーバーによって返される必要のないコンポーネント(SEO値なし)は、サーバー上で単純にスキップできます。クライアントが読み込まれたら、clientLoadedフラグをtrueに設定し、それを渡して再レンダリングを強制します。

閉会およびその他の試み

これまでに実装した唯一の解決策は、サーバーでレンダリングされるコンポーネントの数を減らすことです。

私たちが見ているいくつかのプロジェクトは次のとおりです。

誰かが同様の問題に直面していますか?何ができましたか?ありがとう。

58
Jon

React-router1.0とreact0.14を使用して、fluxオブジェクトを誤って複数回シリアル化していました。

RoutingContextは、react-routerルートのすべてのテンプレートに対してcreateElementを呼び出します。これにより、必要な小道具を注入できます。フラックスも使用します。ラージオブジェクトのシリアル化バージョンを送信します。この場合、createElement内でflux.serialize()を実行していました。シリアル化の方法は、〜20msかかります。 4つのテンプレートを使用すると、renderToString()メソッドの80ms余分になります!

古いコード:

_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() 'sを合計に追加する必要があります。これはrenderToString()の前に発生します)これは素晴らしく速い改善でした。 -直接的な影響がわからなくても、常に正しく処理することを忘れないでください!

13
Federico

アイデア1:コンポーネントのキャッシュ

Update 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 note: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');
_
6
antitoxic

その完全な解決策ではないが、反応する同形アプリで同じ問題が発生し、いくつかのものを使用しました。

1)nodejsサーバーの前でNginxを使用し、レンダリングされた応答を短時間キャッシュします。

2)アイテムのリストを表示する場合、リストのサブセットのみを使用します。たとえば、ビューポートを埋めるためにXアイテムのみをレンダリングし、WebsocketまたはXHRを使用してクライアント側でリストの残りをロードします。

3)サーバー側のレンダリングでは一部のコンポーネントが空であり、クライアント側のコード(componentDidMount)からのみロードされます。これらのコンポーネントは、通常、グラフ、またはプロファイル関連のコンポーネントです。これらのコンポーネントは通常、SEOの観点からは何のメリットもありません

4)SEOについて、同形アプリでの6か月の経験から。 Google Botは、クライアント側React Webページを簡単に読み取ることができるため、サーバー側のレンダリングに煩わ​​される理由はわかりません。

5)<Head >および<Footer>を静的な文字列として保持するか、テンプレートエンジン( Reactjs-handellbars )を使用し、ページのコンテンツのみをレンダリングします(レンダリングされたものをいくつか保存する必要があります)コンポーネント)。単一ページのアプリの場合、Router.Run内の各ナビゲーションでタイトルの説明を更新できます。

5
doron aviguy

fast-react-render が役立つと思います。サーバーレンダリングのパフォーマンスが3倍に向上します。

試してみるには、パッケージをインストールし、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倍の速度になります。ただし、そのためには、レンダリングする各コンポーネントを宣言する必要があります(fast-react-seedの例、webpackでの実行方法を参照)。

4
Andrey