React renderToString()性能和缓存React组件

时间:2016-01-11 18:55:58

标签: performance reactjs isomorphic-javascript render-to-string react-dom

我注意到在服务器上渲染大型组件树时,reactDOM.renderToString()方法开始显着减慢。

背景

一点背景知识。该系统是一个完全同构的堆栈。最高级App组件呈现模板,页面,dom元素和更多组件。查看反应代码,我发现它渲染了~1500个组件(这包括任何简单的dom标记,它被视为一个简单的组件,<p>this is a react component</p>

在开发中,渲染~1500个组件需要大约200-300ms。通过删除一些组件,我能够在~175-225ms内获得~1200个组件。

在生产中,〜1500个组件上的renderToString大约需要50-200ms。

时间似乎是线性的。没有一个组件是慢的,而是它的总和。

问题

这会在服务器上产生一些问题。冗长的方法导致服务器响应时间过长。 TTFB比它应该高很多。使用api调用和业务逻辑,响应应该是250ms,但是使用250ms renderToString它会加倍! SEO和用户不好。此外,作为同步方法,renderToString()可以阻止节点服务器并备份后续请求(这可以通过使用2个单独的节点服务器来解决:1作为Web服务器,1作为服务来单独呈现反应)。

尝试

理想情况下,生产中需要5-50ms的renderToString。我一直在研究一些想法,但我不确定最佳方法是什么。

想法1:缓存组件

任何标记为&#39;静态&#39;的组件可以缓存。通过使用呈现的标记保持缓存,renderToString()可以在呈现之前检查缓存。如果找到一个组件,它会自动抓取该字符串。在高级组件中执行此操作将保存所有嵌套子组件的安装。您必须将高速缓存的组件标记替换为rootID与当前的rootID。

创意2:将组件标记为简单/哑巴

通过将组件定义为&#39; simple&#39;,react应该能够在渲染时跳过所有生命周期方法。 React已经为核心反应dom组件(<p/><h1/>等)执行此操作。很高兴扩展自定义组件以使用相同的优化。

创意3:在服务器端渲染

时跳过组件

服务器上可以简单地跳过不需要由服务器返回的组件(没有SEO值)。加载客户端后,将clientLoaded标记设置为true并将其传递给强制重新呈现。

结束和其他尝试

到目前为止,我实现的唯一解决方案是减少服务器上呈现的组件数量。

我们正在关注的一些项目包括:

有人遇到过类似的问题吗?你有什么能做的? 感谢。

4 个答案:

答案 0 :(得分:10)

使用react-router1.0和react0.14,我们错误地多次序列化我们的flux对象。

RoutingContext将为您的react路由器路由中的每个模板调用createElement。这允许你注入你想要的任何道具。我们也使用助焊剂。我们发送一个大型对象的序列化版本。在我们的例子中,我们在createElement中执行flux.serialize()。序列化方法可能需要大约20ms。使用4个模板,对于renderToString()方法,这将是额外的80毫秒!

旧代码:

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() ~20ms,这在renderToString()之前发生。这是一个很好的快速改进。 - 重要的是要记住要始终正确地做事,即使你不知道直接的影响!

答案 1 :(得分:6)

想法1:缓存组件

更新1 :我在底部添加了一个完整的工作示例。它将组件缓存在内存中并更新data-reactid

这实际上可以轻松完成。您应该monkey-patch 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');

答案 2 :(得分:5)

它不是完整的解决方案 我有同样的问题,我的反应同构应用程序,我使用了几件事。

1)在nodejs服务器前使用Nginx,并在短时间内缓存渲染的响应。

2)在显示项目列表的情况下,我仅使用列表的子集。例如,我将只渲染X项以填充视口,并使用Websocket或XHR在客户端加载列表的其余部分。

3)我的一些组件在服务器端呈现中是空的,只会从客户端代码(componentDidMount)加载。 这些组件通常是图形或配置文件相关的组件。这些组件通常在SEO的观点上没有任何好处

4)关于SEO,根据我的经验6个月与同构应用程序。 Google Bot可以轻松阅读客户端React网页,因此我不确定为什么我们会厌烦服务器端渲染。

5)将<Head ><Footer>保持为静态字符串或使用模板引擎(Reactjs-handellbars),并仅渲染页面内容(它应该保存一些渲染组件) )。 如果是单页应用,您可以在Router.Run内的每个导航中更新标题说明。

答案 3 :(得分:4)

我认为fast-react-render可以帮到你。它可以将服务器渲染的性能提高三倍。

试试吧,你只需要安装包并将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执行此操作)。