【问题标题】:React renderToString() Performance and Caching React ComponentsReact renderToString() 性能和缓存 React 组件
【发布时间】:2016-04-16 04:48:34
【问题描述】:

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

背景

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

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

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

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

问题

这会在服务器上产生一些问题。冗长的方法导致服务器响应时间长。 TTFB 远高于应有的水平。对于 api 调用和业务逻辑,响应应该是 250 毫秒,但是对于 250 毫秒的 renderToString,它会加倍!对 SEO 和用户不利。另外,作为同步方法,renderToString() 可以阻塞节点服务器并备份后续请求(这可以通过使用 2 个单独的节点服务器来解决:1 个作为 Web 服务器,1 个作为服务单独渲染 react)。

尝试

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

想法一:缓存组件

任何标记为“静态”的组件都可以被缓存。通过使用渲染标记保存缓存,renderToString() 可以在渲染之前检查缓存。如果它找到一个组件,它会自动抓取字符串。在高级组件上执行此操作将保存所有嵌套子组件的安装。您必须将缓存的组件标记的 react rootID 替换为当前的 rootID。

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

通过将组件定义为“简单”,react 应该能够在渲染时跳过所有生命周期方法。 React 已经为核心的 react dom 组件(<p/><h1/> 等)做到了这一点。扩展自定义组件以使用相同的优化会很好。

想法 3:在服务器端渲染时跳过组件

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

关闭和其他尝试

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

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

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

【问题讨论】:

  • 这是一个有趣的问题。您是否遇到过需要解决的性能问题?如果是这样,您是否能够将问题定位到特定的子树?
  • 对于我的一些更复杂的页面,有 1500 多个组件正在呈现(因为 react 将所有内容分解为一个“组件”)。任何有大约 1000 多个组件的地方似乎都非常慢。我用 ReactCompositeComponent 弄乱了反应源代码,如果它们符合某些条件,则将它们添加到缓存中。但是,这会导致从缓存中提取时不变量,因为每个组件都有不正确的根反应 ID。但是,如果我从缓存中提取结果字符串而不是安装它,它确实大大提高了性能;)
  • 我假设“组件”是指已定义的组件,而不是内置的 jsx 标签。如果包括这些,那么 1500 并非完全不合理。但无论如何,我仍在努力寻找问题的根源。是数据层慢还是计算层慢?如果所有数据都在内存中,还会出现问题吗?
  • 我没有缓存问题的答案,但我希望这里有人知道。同时,我会尝试找出哪些树存在问题并尝试优化这些树(例如,尽可能使用纯的、无状态的组件来避免额外的生命周期方法等等)。对于特别大的页面,我还会考虑仅预渲染关键路径,然后在客户端加载其余路径。这可能会影响 SEO,但可能不会影响 Google。您可以为自我识别的爬虫预渲染更多内容。
  • 回复。想法 2,React 已经支持stateless functional components。它们只有一个渲染方法,没有状态,也绝对没有生命周期方法。我怀疑 react 是否对这些组件进行了记忆,但将来可能会改变。

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


【解决方案1】:

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

RoutingContext 将为您的 react-router 路由中的每个模板调用 createElement。这允许你注入任何你想要的道具。我们也使用助焊剂。我们发送一个大对象的序列化版本。在我们的例子中,我们在 createElement 中执行flux.serialize()。序列化方法可能需要大约 20 毫秒。使用 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() 的时间从 ~120ms 减少到 ~30ms。 (您仍然需要将 1x serialize() 的 ~20ms 添加到总数中,这发生在 renderToString() 之前)这是一个很好的快速改进。 -- 重要的是要记住始终正确地做事,即使您不知道直接影响!

【讨论】:

    【解决方案2】:

    想法一:缓存组件

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

    【讨论】:

      【解决方案3】:

      这不是一个完整的解决方案 我的反应同构应用程序也遇到了同样的问题,并且我使用了一些东西。

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

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

      3. 我的一些组件在服务器端渲染中是空的,只会从客户端代码 (componentDidMount) 加载。 这些组件通常是图形或配置文件相关的组件。从 SEO 的角度来看,这些组件通常没有任何好处

      4. 关于 SEO,根据我使用同构应用程序 6 个月的经验。 Google Bot 可以轻松读取客户端 React 网页,所以我不确定我们为什么要为服务器端渲染而烦恼。

      5. 保持&lt;Head&gt;&lt;Footer&gt;为静态字符串或使用模板引擎(Reactjs-handlebars),只渲染页面的内容,(它应该保存一些渲染的组件)。如果是单页应用,您可以在Router.Run内的每个导航中更新标题描述。

      【讨论】:

      • 关于 #4,SSR 提供​​了巨大的性能优势,而不仅仅是 SEO 优势。
      【解决方案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 做到这一点)。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-04-04
        • 1970-01-01
        • 2021-10-03
        • 2016-10-18
        • 2017-05-02
        • 2019-03-19
        • 1970-01-01
        • 2018-11-05
        相关资源
        最近更新 更多