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)。