首页 文章

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

提问于
浏览
55

我注意到,在服务器上渲染大型组件树时, 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作为服务来单独呈现反应) .

尝试

理想情况下, 生产环境 中渲染TMString需要5-50ms . 我一直在研究一些想法,但我不确定最好的方法是什么 .

想法1:缓存组件

任何标记为'static'的组件都可以被缓存 . 通过使用呈现的标记保持缓存, renderToString() 可以在呈现之前检查缓存 . 如果找到一个组件,它会自动抓取该字符串 . 在高级别组件中执行此操作将保存所有嵌套子组件's mounting. You would have to replace the cached component markup' s使rootID与当前rootID做出反应 .

想法2:将组件标记为简单/笨

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

构思3:在服务器端渲染时跳过组件

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

结束和其他尝试

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

我们正在研究的一些项目包括:

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

4 回答

  • 10

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

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

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

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

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

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

  • 5

    我想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执行此操作) .

  • 6

    想法1:缓存组件

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

    您应该在应用程序中的任何位置之前执行此操作 .

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

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

    在我的例子中,这有助于将时间从~120ms缩短到~30ms . (你仍然需要将1x serialize() 的〜20ms添加到总数中,这发生在 renderToString() 之前)这是一个很好的快速改进 . - 它's important to remember to always do things correctly, even if you don'知道直接影响!

相关问题