performance - with - web components and react




React RenderToString() Desempenho e armazenamento em cache React Components (3)

Percebi que o método reactDOM.renderToString() começa a reactDOM.renderToString() significativamente mais lento ao renderizar uma grande árvore de componentes no servidor.

fundo

Um pouco de fundo. O sistema é uma pilha totalmente isomórfica. O componente App nível mais alto processa modelos, páginas, elementos dom e mais componentes. Olhando no código de reação, descobri que ele renderiza ~ 1500 componentes (isso inclui qualquer tag dom simples que é tratada como um componente simples, <p>this is a react component</p> .

No desenvolvimento, renderizar ~ 1500 componentes leva ~ 200-300ms. Ao remover alguns componentes, consegui obter ~ 1200 componentes para renderizar em ~ 175-225ms.

Na produção, renderToString em ~ 1500 componentes leva cerca de ~ 50-200ms.

O tempo parece ser linear. Nenhum componente é lento, é a soma de muitos.

Problema

Isso cria alguns problemas no servidor. O método demorado resulta em longos tempos de resposta do servidor. O TTFB é muito maior do que deveria. Com chamadas de API e lógica de negócios, a resposta deve ser 250ms, mas com um renderToString de 250ms, ela é duplicada! Ruim para SEO e usuários. Além disso, sendo um método síncrono, renderToString() pode bloquear o servidor do nó e fazer backup de solicitações subsequentes (isso pode ser resolvido usando 2 servidores de nó separados: 1 como servidor da web e 1 como serviço para renderizar apenas a reação).

Tentativas

Idealmente, levaria 5-50ms para renderToString na produção. Venho trabalhando em algumas idéias, mas não sei exatamente qual seria a melhor abordagem.

Ideia 1: Armazenando em Cache Componentes

Qualquer componente marcado como 'estático' pode ser armazenado em cache. Mantendo um cache com a marcação renderizada, o renderToString() pode verificar o cache antes da renderização. Se encontrar um componente, ele pega automaticamente a string. Fazer isso em um componente de alto nível salvaria a montagem de todo o componente filho aninhado. Você precisaria substituir o rootID de reação da marcação do componente em cache pelo rootID atual.

Ideia 2: Marcando componentes como simples / burros

Ao definir um componente como 'simples', o react deve poder ignorar todos os métodos do ciclo de vida durante a renderização. O React já faz isso para os componentes principais do reator ( <p/> , <h1/> , etc). Seria bom estender componentes personalizados para usar a mesma otimização.

Ideia 3: Ignorar componentes na renderização do servidor

Componentes que não precisam ser devolvidos pelo servidor (sem valor de SEO) podem simplesmente ser ignorados no servidor. Depois que o cliente carregar, defina um sinalizador clientLoaded como true e passe-o para impor uma nova renderização.

Encerramento e outras tentativas

A única solução que eu implementei até agora é reduzir o número de componentes que são renderizados no servidor.

Alguns projetos que estamos analisando incluem:

Alguém já enfrentou problemas semelhantes? O que você conseguiu fazer? Obrigado.


Ideia 1: Armazenando em Cache Componentes

Atualização 1 : adicionei um exemplo de trabalho completo na parte inferior. Ele armazena em cache os componentes na memória e atualiza os data-reactid .

Na verdade, isso pode ser feito facilmente. Você deve monkey-patch ReactCompositeComponent e verificar se há uma versão em 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)
}

Você deve fazer isso antes de require('react') em qualquer lugar do seu aplicativo.

Nota do Webpack: Se você usar algo como o new webpack.ProvidePlugin({'React': 'react'}) , altere-o para o new webpack.ProvidePlugin({'React': 'react-override'}) onde você faz suas modificações em module.exports = require('react') react-override.js e export module.exports = require('react') (isto é, module.exports = require('react') )

Um exemplo completo que armazena em cache na memória e atualiza o atributo reactid pode ser este:

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

Eu acho que fast-react-render pode ajudá-lo. Aumenta o desempenho da renderização do servidor três vezes.

Para experimentá-lo, você só precisa instalar o pacote e substituir ReactDOM.renderToString por FastReactRender.elementToString:

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

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

Também é possível usar fast-react-server , nesse caso a renderização será 14 vezes mais rápida que a renderização de reação tradicional. Mas para isso, cada componente, que você deseja renderizar, deve ser declarado com ele (veja um exemplo em fast-react-seed, como você pode fazê-lo no webpack).


Usando react-router1.0 e react0.14, estávamos serializando por engano nosso objeto de fluxo várias vezes.

RoutingContext chamará createElement para todos os modelos nas rotas do roteador de reação. Isso permite que você injete os adereços que desejar. Também usamos fluxo. Enviamos uma versão serializada de um objeto grande. No nosso caso, estávamos executando flux.serialize() dentro de createElement. O método de serialização pode demorar ~ 20ms. Com 4 modelos, isso representaria 80ms extras para o método renderToString() !

Código antigo:

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

Facilmente otimizado para isso:

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

No meu caso, isso ajudou a reduzir o tempo de renderToString renderToString() de ~ 120ms para ~ 30ms. (Você ainda precisa adicionar os ~ 20ms de 1x serialize() ao total, o que acontece antes do renderToString() ). Foi uma melhoria rápida e agradável. - É importante lembrar sempre de fazer as coisas corretamente, mesmo que você não saiba o impacto imediato!





react-dom