62

我注意到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 个作为单独渲染反应的服务)。

尝试

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

思路一:缓存组件

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

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

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

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

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

关闭和其他尝试

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

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

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

4

4 回答 4

13

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

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

于 2016-01-20T17:57:50.917 回答
8

思路一:缓存组件

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

这实际上可以很容易地完成。您应该猴子补丁 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');
于 2016-07-25T08:29:52.573 回答
6

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

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

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

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

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

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

于 2016-01-17T06:25:33.377 回答
4

我认为快速反应渲染可以帮助你。它将服务器渲染的性能提高了三倍。

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

于 2016-09-12T13:39:07.760 回答