React renderToString()性能和cachingReact组件

我注意到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作为服务来完成呈现反应)。

尝试

理想情况下,在生产中使用ToString需要5-50ms。 我一直在做一些想法,但我不确定最好的方法是什么。

想法1:caching组件

任何标记为“静态”的组件都可以被caching。 通过使渲染标记保持caching, renderToString()可以在渲染之前检查caching。 如果它find一个组件,它会自动抓取该string。 在高级别组件中执行此操作将保存所有嵌套的子组件的安装。 您将不得不用当前的rootIDreplacecaching的组件标记的反应rootID。

想法2:标记组件简单/哑巴

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

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

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

closures和其他尝试

到目前为止我唯一的解决scheme是减less在服务器上呈现的组件数量。

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

  • React-dom-stream https://github.com/aickin/react-dom-stream (仍在努力实现这个testing)
  • 巴别克内联元素https://babeljs.io/docs/plugins/transform-react-inline-elements/ (看起来这是沿着想法2的线)

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

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

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

旧代码:

 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()时间从renderToString()减less到renderToString() 。 (你仍然需要将1x serialize()renderToString()添加到总数,这发生在renderToString()之前)这是一个很好的快速改进。 – 即使你不知道直接的影响,记住要始终正确地做事是很重要的!

想法1:caching组件

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

这实际上可以轻松完成。 你应该猴子补丁 ReactCompositeComponent并检查一个caching的版本:

 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和export react (即module.exports = require('react')

在内存中caching并更新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'); 

它不完整的解决scheme我有同样的问题,与我的反应同构的应用程序,我用了几件事情。

1)在nodejs服务器之前使用Nginx,并caching渲染的响应一段时间。

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

3)我的一些组件在服务器端渲染时是空的,只会从客户端代码(componentDidMount)加载。 这些组件通常是graphics或configuration文件相关的组件。 这些组件通常对SEO的观点没有任何好处

4)关于search引擎优化,从我的经验6个月同构的应用程序。 谷歌机器人可以轻松地阅读客户端反应网页,所以我不知道为什么我们打扰服务器端渲染。

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

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

对于尝试它,您只需要安装包并将ReactDOM.renderToStringreplace为FastReactRender.elementToString:

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

也可以使用快速响应服务器 ,在这种情况下,渲染速度将是传统反应渲染的14倍。 但是为了让每一个你想渲染的组件都必须用它来声明(参见fast-react-seed中的一个例子,你可以怎么做webpack)。