web-dev-qa-db-fra.com

React renderToString () Performances et mise en cache React Composants

J'ai remarqué que la méthode reactDOM.renderToString() commence à ralentir considérablement lors du rendu d'une arborescence de composants volumineuse sur le serveur.

Contexte

Un peu de fond. Le système est une pile totalement isomorphe. Le composant de niveau supérieur App rend les modèles, les pages, les éléments dom et plus de composants. En regardant dans le code de réaction, j'ai trouvé qu'il restituait environ 1 500 composants (cela inclut toute balise dom simple qui est traitée comme un composant simple, <p>this is a react component</p>.

En développement, le rendu de ~ 1500 composants prend environ 200-300ms. En supprimant certains composants, j'ai pu obtenir un rendu d'environ 1200 composants en environ 175-225 ms.

En production, renderToString sur ~ 1500 composants prend environ 50-200 ms.

Le temps semble être linéaire. Aucun composant n'est lent, c'est plutôt la somme de nombreux.

Problème

Cela crée des problèmes sur le serveur. La méthode longue entraîne des temps de réponse du serveur longs. Le TTFB est beaucoup plus élevé qu'il ne devrait l'être. Avec les appels api et la logique métier, la réponse devrait être de 250 ms, mais avec un renderToString de 250 ms, elle est doublée! Mauvais pour le référencement et les utilisateurs. De plus, étant une méthode synchrone, renderToString() peut bloquer le serveur de noeud et sauvegarder les requêtes suivantes (ceci peut être résolu en utilisant 2 serveurs de noeud distincts: 1 en tant que serveur Web et 1 en tant que service pour rendre uniquement ).

Tentatives

Idéalement, le rendu de ToTtring en production prendrait 5-50ms. J'ai travaillé sur quelques idées, mais je ne sais pas exactement quelle serait la meilleure approche.

Idée 1: Mise en cache des composants

Tout composant marqué comme "statique" peut être mis en cache. En conservant un cache avec le balisage rendu, la renderToString() pourrait vérifier le cache avant le rendu. S'il trouve un composant, il saisit automatiquement la chaîne. Effectuer cette opération sur un composant de haut niveau permettrait d’éviter le montage de tous les composants imbriqués. Vous devrez remplacer l'ID root du balisage du composant mis en cache par l'ID root actuel.

Idée 2: Marquage des composants comme simple/muet

En définissant un composant comme 'simple', react doit pouvoir ignorer toutes les méthodes de cycle de vie lors du rendu. React le fait déjà pour les composants principaux de la réaction (<p/>, <h1/>, Etc.). Cela serait bien d’étendre les composants personnalisés pour utiliser la même optimisation.

Idée 3: Ignorer les composants lors du rendu côté serveur

Les composants qui n'ont pas besoin d'être renvoyés par le serveur (aucune valeur de référencement) peuvent simplement être ignorés sur le serveur. Une fois le client chargé, définissez un indicateur clientLoaded sur true et transmettez-le pour appliquer un nouveau rendu.

Fermeture et autres tentatives

La seule solution mise en œuvre à ce jour consiste à réduire le nombre de composants rendus sur le serveur.

Certains projets que nous étudions incluent:

Quelqu'un at-il rencontré des problèmes similaires? Qu'as-tu pu faire? Merci.

58
Jon

En utilisant react-router1.0 et react0.14, nous avons erronément numérisé notre objet flux plusieurs fois.

RoutingContext appellera createElement pour chaque modèle de vos routes réact-router. Cela vous permet d'injecter tout ce que vous voulez. Nous utilisons également du flux. Nous envoyons une version sérialisée d'un objet volumineux. Dans notre cas, nous faisions flux.serialize() dans createElement. La méthode de sérialisation peut prendre environ 20 ms. Avec 4 modèles, cela représente 80 ms de plus par rapport à votre méthode renderToString()!

Ancien code:

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);

Facilement optimisé pour cela:

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);

Dans mon cas, cela a contribué à réduire le temps renderToString() de ~ 120 ms à ~ 30 ms. (Vous devez toujours ajouter les ~ 20 ms de la 1x serialize() au total, ce qui se produit avant la renderToString()). C'était une amélioration rapide et intéressante. - Il est important de ne pas oublier de toujours faire les choses correctement, même si vous ne connaissez pas l'impact immédiat!

13
Federico

Idée 1: Mise en cache des composants

Mise à jour 1 : J'ai ajouté un exemple de travail complet en bas. Il met en cache les composants en mémoire et met à jour data-reactid.

Cela peut être fait facilement. Vous devriez monkey-patchReactCompositeComponent et rechercher une version en cache:

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)
}

Vous devriez le faire avant de vous require('react') n'importe où dans votre application.

Remarque sur le Webpack: Si vous utilisez quelque chose comme new webpack.ProvidePlugin({'React': 'react'}), vous devez le remplacer par new webpack.ProvidePlugin({'React': 'react-override'}) où vous effectuez vos modifications. dans react-override.js et exportez react (c.-à-d. module.exports = require('react'))

Un exemple complet de mise en cache en mémoire et de mise à jour de l'attribut reactid pourrait être le suivant:

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

Ce n'est pas une solution complète. J'avais le même problème, avec mon application Rea isomorphic, et j'ai utilisé deux ou trois choses.

1) Utilisez Nginx devant votre serveur nodejs et mettez en cache la réponse rendue pendant un court instant.

2) En cas d’affichage d’une liste d’articles, j’utilise uniquement un sous-ensemble de liste. Par exemple, je ne rendrai que X éléments pour remplir la fenêtre d'affichage et chargerai le reste de la liste côté client à l'aide de Websocket ou XHR.

3) Certains de mes composants sont vides dans le rendu de serveride et ne seront chargés qu'à partir du code côté client (composantDidMount). Ces composants sont généralement des graphiques ou des composants liés au profil. ces composants ne présentent généralement aucun avantage du point de vue du référencement

4) À propos de SEO, de mon expérience 6 mois avec une application isomorphe. Google Bot peut lire le côté client React page Web facilement, donc je ne suis pas sûr de savoir pourquoi nous nous préoccupons du rendu côté serveur.

5) Gardez le <Head >et <Footer> en tant que chaîne statique ou utilisez le moteur de template ( Reactjs-handellbars ), et ne restituez que le contenu de la page (il convient de sauvegarder quelques composants renderd). Dans le cas d'une application à une seule page, vous pouvez mettre à jour la description du titre dans chaque navigation à l'intérieur de Router.Run.

5
doron aviguy

Je pense que fast-react-render peut vous aider. Il augmente les performances de votre serveur trois fois.

Pour l'essayer, il vous suffit d'installer le package et de remplacer ReactDOM.renderToString par FastReactRender.elementToString:

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

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

Vous pouvez aussi utiliser fast-react-server , dans ce cas, le rendu sera 14 fois plus rapide que le rendu react traditionnel. Mais pour cela, chaque composant que vous voulez rendre doit être déclaré avec lui (voir un exemple dans fast-react-seed, comment vous pouvez le faire pour webpack).

4
Andrey