React renderToString() 性能和缓存 React 组件
React renderToString() Performance and Caching React Components
我注意到 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 毫秒。我一直在研究一些想法,但我不确定最好的方法是什么。
想法 1:缓存组件
任何标记为 'static' 的组件都可以被缓存。通过使用呈现的标记保留缓存,renderToString()
可以在呈现之前检查缓存。如果它找到一个组件,它会自动获取字符串。在高级组件上执行此操作将保存所有嵌套子组件的安装。您必须将缓存的组件标记的 react rootID 替换为当前的 rootID。
想法 2:将组件标记为 simple/dumb
通过将组件定义为'simple',React 应该能够在渲染时跳过所有生命周期方法。 React 已经为核心 React dom 组件(<p/>
、<h1/>
等)做到了这一点。扩展自定义组件以使用相同的优化会很好。
想法 3:在服务器端渲染时跳过组件
不需要由服务器返回的组件(无 SEO 值)可以在服务器上简单地跳过。客户端加载后,将 clientLoaded
标志设置为 true
并将其向下传递以强制重新呈现。
关闭和其他尝试
到目前为止,我实施的唯一解决方案是减少在服务器上呈现的组件数量。
我们正在关注的一些项目包括:
- React-dom-stream(仍在努力实施此测试)
- Babel inline elements(好像是思路二)
有人遇到过类似的问题吗?你能做什么?
谢谢。
这不是一个完整的解决方案
我的反应同构应用程序遇到了同样的问题,我使用了一些东西。
在你的 nodejs 服务器前面使用 Nginx,并在短时间内缓存呈现的响应。
在显示项目列表的情况下,我只使用列表的一个子集。例如,我将只渲染 X 个项目来填充视口,并使用 Websocket 或 XHR 在客户端加载列表的其余部分。
我的一些组件在服务器端渲染中是空的,只会从客户端代码加载 (componentDidMount
)。
这些组件通常是图形或配置文件相关组件。从 SEO 的角度来看,这些组件通常没有任何好处
关于 SEO,根据我 6 个月使用同构应用程序的经验。 Google Bot 可以轻松读取客户端 React 网页,所以我不确定为什么我们要为服务器端渲染而烦恼。
将<Head>
和<Footer>
保留为静态字符串或使用模板引擎(Reactjs-handlebars),只渲染页面内容,(它应该保存一些渲染的组件)。如果是单页应用,您可以在 Router.Run
.
内更新每个导航中的标题描述
使用 react-router1.0 和 react0.14,我们错误地多次序列化了 flux 对象。
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()
之前)这是一个很好的快速改进。 -- 重要的是要记住始终正确地做事,即使您不知道直接的影响!
想法 1:缓存组件
更新 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');
我认为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,这样渲染速度将是传统react渲染的14倍。但是对于你想要渲染的每个组件,必须用它来声明(参见 fast-react-seed 中的示例,你如何为 webpack 做到这一点)。
我注意到 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 毫秒。我一直在研究一些想法,但我不确定最好的方法是什么。
想法 1:缓存组件
任何标记为 'static' 的组件都可以被缓存。通过使用呈现的标记保留缓存,renderToString()
可以在呈现之前检查缓存。如果它找到一个组件,它会自动获取字符串。在高级组件上执行此操作将保存所有嵌套子组件的安装。您必须将缓存的组件标记的 react rootID 替换为当前的 rootID。
想法 2:将组件标记为 simple/dumb
通过将组件定义为'simple',React 应该能够在渲染时跳过所有生命周期方法。 React 已经为核心 React dom 组件(<p/>
、<h1/>
等)做到了这一点。扩展自定义组件以使用相同的优化会很好。
想法 3:在服务器端渲染时跳过组件
不需要由服务器返回的组件(无 SEO 值)可以在服务器上简单地跳过。客户端加载后,将 clientLoaded
标志设置为 true
并将其向下传递以强制重新呈现。
关闭和其他尝试
到目前为止,我实施的唯一解决方案是减少在服务器上呈现的组件数量。
我们正在关注的一些项目包括:
- React-dom-stream(仍在努力实施此测试)
- Babel inline elements(好像是思路二)
有人遇到过类似的问题吗?你能做什么? 谢谢。
这不是一个完整的解决方案 我的反应同构应用程序遇到了同样的问题,我使用了一些东西。
在你的 nodejs 服务器前面使用 Nginx,并在短时间内缓存呈现的响应。
在显示项目列表的情况下,我只使用列表的一个子集。例如,我将只渲染 X 个项目来填充视口,并使用 Websocket 或 XHR 在客户端加载列表的其余部分。
我的一些组件在服务器端渲染中是空的,只会从客户端代码加载 (
componentDidMount
)。 这些组件通常是图形或配置文件相关组件。从 SEO 的角度来看,这些组件通常没有任何好处关于 SEO,根据我 6 个月使用同构应用程序的经验。 Google Bot 可以轻松读取客户端 React 网页,所以我不确定为什么我们要为服务器端渲染而烦恼。
将
内更新每个导航中的标题描述<Head>
和<Footer>
保留为静态字符串或使用模板引擎(Reactjs-handlebars),只渲染页面内容,(它应该保存一些渲染的组件)。如果是单页应用,您可以在Router.Run
.
使用 react-router1.0 和 react0.14,我们错误地多次序列化了 flux 对象。
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()
之前)这是一个很好的快速改进。 -- 重要的是要记住始终正确地做事,即使您不知道直接的影响!
想法 1:缓存组件
更新 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');
我认为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,这样渲染速度将是传统react渲染的14倍。但是对于你想要渲染的每个组件,必须用它来声明(参见 fast-react-seed 中的示例,你如何为 webpack 做到这一点)。