React同构直出优化总结

收录待用,修改转载已取得腾讯云受权css


做者:郭林烁 joeyguohtml

原文地址前端

React 的实践从去年在 PC QQ家校群开始,因为 PC 上的网络及环境都至关好,因此在使用时可谓一路顺风,偶尔遇到点小磕绊,也可以快速地填补磨平。而最近一段时间,咱们将手Q的家校群重构成 React,除了原有框架上存在明显问题的缘由外,选择React也是由于它确实有足够的吸引力以及优点,加之在PC家校群上的实践经验,斟酌下便开始了,到如今已有页面在线上正常跑起。node

因为移动端上的网络及环境迥异,性能误差。因此在移动端上用 React 时,遇到了很多的坑点,也花了一些力气在上面。关于在移动端上的优化,可看咱们团队的另外一篇文章的 React移动端web极致优化react

一提到优化,不得不提直出
关于这块能够查看 Node直出理论与实践总结,这篇文章较详细的分析直出的概念及一步步优化,也结合了 手Q家校群使用快速的数据直出方式来优化性能的总结与性能数据分析webpack

一提到 React,不得不提同构
同构基于服务端渲染,却不止是服务端渲染。git

服务端渲染到同构的这一路

后台包办

服务端渲染的方案早在后台程序先后端包办的时代上就有了,那时候使用JSP、PHP等动态语言将数据与页面模版整合后输出给浏览器,一步到位es6

22

这个时候,前端开发跟后端揉为一体,项目小的时候,先后端的开发和调试还真能够称为一步到位。但当项目庞大起来的时候,不管是修改某个样式要起一个庞大服务的尴尬,仍是先后端糅合的地带变得愈来愈难以维护,都很难过。github

先后分离

先后端分离后,服务端渲染的模式就开始被淡化了。这时候的服务端渲染比较尴尬,因为先后端的编码语言不一样,连页面模板都不能复用,只能让在先后端开发完成后,再将前端代码改成给后端使用的页面模板,增大了工做量。最终也仍是跟后台包办异曲同工。web

语言变通

Node 驾着祥云腾空而来,谷歌 V8 引擎给力支持,众前端拿着看家本领(JavaScript)开始涉足服务端,因而服务端渲染上又一步进阶

33

因为先后端时候的相同的语言,因此先后端在代码的共用上达到了新的高度,页面模版、node modules 均可以作成先后通用。同构的雏形,只是共用的代码仍是有局限。

先后同构

有了Node 后,前端便有了更多的想象空间。前端框架开始考虑兼容服务端渲染,提供更方便的 API,先后端共用一套代码的方案,让服务端渲染愈来愈便捷。固然,不仅是 React 作了这件事,但 React 将这种思想推向高潮,同构的概念也开始广为人传。

55

关于 React 网上已有大多教程,能够查看阮老师的react-demos。关于 React 上的数据流管理方案,如今最为火热的 Redux 应该是首选,具体能够查看另外一篇文章 React 数据流管理架构之Redux,此篇就再也不赘述,下面讲讲 React 同构的理论与在手Q家校群上的具体实践总结。

React 同构

React 虚拟 Dom

React 的虚拟 Dom 以对象树的形式保存在内存中,并存在先后端两种展露原型的形式

rendertype

  1. 客户端上,虚拟 Dom 经过 ReactDOM 的 Render 方法渲染到页面中
  2. 服务端上,React 提供的另外两个方法:ReactDOMServer.renderToString 和 ReactDOMServer.renderToStaticMarkup 可将其渲染为 HTML 字符串。

    React 同构的关键要素

完善的 Compponent 属性及生命周期与客户端的 render 时机是 React 同构的关键。
DOM 的一致性
在先后端渲染相同的 Compponent,将输出一致的 Dom 结构。
不一样的生命周期
在服务端上 Component 生命周期只会到 componentWillMount,客户端则是完整的。
客户端 render 时机
同构时,服务端结合数据将 Component 渲染成完整的 HTML 字符串并将数据状态返回给客户端,客户端会判断是否能够直接使用或须要从新挂载。

以上即是 React 在同构/服务端渲染的提供的基础条件。在实际项目应用中,还须要考虑其余边角问题,例如服务器端没有 window 对象,须要作不一样处理等。下面将经过在手Q家校群上的具体实践,分享一些同构的 Tips 及优化成果

以手Q家校群 React 同构实践为例

手Q家校群使用 React + Redux + Webpack 的架构

同构实践 Tips

1. renderToString 和 renderToStaticMarkup

ReactDOMServer 提供 renderToString 和 renderToStaticMarkup 的方法,大多数状况使用 renderToString,这样会为组件增长 checksum

checknum

React 在客户端经过 checksum 判断是否须要从新render
相同则不从新render,省略建立DOM和挂载DOM的过程,接着触发 componentDidMount 等事件来处理服务端上的未尽事宜(事件绑定等),从而加快了交互时间;不一样时,组件将客户端上被从新挂载 render。

renderToStaticMarkup 则不会生成与 react 相关的data-*,也不存在 checksum,输出的 html 以下

3333

在客户端时组件会被从新挂载,客户端从新挂载不生成 checknum( 也没这个必要 ),因此该方法只当服务端上所渲染的组件在客户端不须要时才使用

checknum

2. 服务端上的数据状态与同步给客户端

服务端上的产生的数据须要随着页面一同返回,客户端使用该数据去 render,从而保持状态一致。服务端上使用 renderToString 而在客户端上依然从新挂载组件的状况大可能是由于在返回 HTML 的时候没有将服务端上的数据一同返回,或者是返回的数据格式不对致使,开发时能够留意 chrome 上的提示如

noti

3. 服务端需提早拉取数据,客户端则在 componentDidMount 调用

平台上的差别,服务端渲染只会执行到 compnentWillMount 上,因此为了达到同构的目的,能够把拉取数据的逻辑写到 React Class 的静态方法上,一方面服务端上能够经过直接操做静态方法来提早拉取数据再根据数据生成 HTML,另外一方面客户端能够在 componentDidMount 时去调用该静态方法拉取数据

4. 保持数据的肯定性

这里指影响组件 render 结果的数据,举个例子,下面的组件因为在服务端与客户端渲染上会由于组件上产生不一样随机数的缘由而致使客户端将从新渲染。

Class Wrapper extends Component {
  render() {
    return (<h1>{Math.random()}</h1>);
  }
};

能够将 Math.random() 封装至Component 的 props 中,在服务端上生成随机数并传入到这个component中,从而保证随机数在客户端和服务端一致。如

Class Wrapper extends Component {
  render() {
    return (<h1>{this.props.randomNum}</h1>);
  }
};

服务端上传入randomNum

let randomNum = Math.random()
var html = ReacDOMServer.renderToString(<Wrapper randomNum={randomNum} />);

5. 平台区分

当先后端共用一套代码的时候,像前端特有的 Window 对象,Ajax 请求 在后端是没法使用上的,后端须要去掉这些前端特有的对象逻辑或使用对应的后端方案,如后端可使用 http.request 替代 Ajax 请求,因此须要进行平台区分,主要有如下几种方式

1.代码使用先后端通用的模块,如 isomorphic-fetch
2.先后端经过webpack 配置 resolve.alias 对应不一样的文件,如
客户端使用 /browser/request.js 来作 ajax 请求

resolve: {
    alias: {
        'request': path.join(pathConfig.src, '/browser/request'),
    }
}

服务端 webpack 上使用 /server/request.js 以 http.request 替代 ajax 请求

resolve: {
    alias: {
        'request': path.join(pathConfig.src, '/server/request'),
    }
}

3.使用 webpack.DefinePlugin 在构建时添加一个平台区分的值,这种方式的在 webpack UglifyJsPlugin 编译后,非当前平台( 不可达代码 )的代码将会被去掉,不会增长文件大小。如
在服务端的 webpack 加上下面配置

new webpack.DefinePlugin({
    "__ISOMORPHIC__": true
}),

在JS逻辑上作判断

if(__ISOMORPHIC__){
    // do server thing
} else {
    // do browser thing
}

4.window 是浏览器上特有的对象,因此也能够用来作平台区分

var isNode = typeof window === 'undefined';
if (isNode) {
    // do server thing
} else {
    // do browser thing
}

6. 只直出首屏页面可视内容,其余在客户端上延迟处理

这是为了减小服务端的负担,也是加快首屏展现时间,如在手Q家校群列表中存在 “我发布的” 和 “所有” 两个 tab,内容都为做业列表,这次实践在服务端上只处理首屏可视内容,即只输出 “我发布的” 的完整HTML,另一个tab的内容在客户端上经过 react 的 dom diff 机制来动态挂载,无页面刷新的感知。

default

7. componentWillReceiveProps 中,依赖数据变化的方法,需考虑在 componentDidMount 作兼容

举个例子,identity 默认为 UNKOWN,从后台拉取到数据后,更新其值,从而触发 setButton 方法

componentWillReceiveProps(nextProps) {
    if (nextProps.role.get('identity') !== UNKOWN &&
        nextProps.role.get('identity')  !== this.props.role.get('identity'))) {
        this.setButton();
    }
}

同构时,因为服务端上已作了第一次数据拉取,因此上面代码在客户端上将因为 identity 已存在而致使永不执行 setButton 方法,解决方式可在 componentDidMount 作兼容处理

componentDidMount() {
    // .. 判断是否为同构 
    if (identity !== UNKOWN) {
        this.setButton(identity);
    }
}

8. redux在服务端上的使用方式 (redux)

下图为其中一种形式,先进行数据请求,再将请求到的数据 dispatch 一个 action,经过在reducer将数据进行 redux 的 state 化。还有其余方式,如直接 dispatch 一个 action,在action里面去作数据请求,后续是同样的,不过这样就要求请求数据的模块是 isomorphism 即先后端通用的。
default

9. 设计好 store state (redux)

设计好 store state 是使用 redux 的关键,而在服务端上,合理的扁平化 state 能在其被序列化时,减小 CPU 消耗

10. 两个 action 在同个component中数据存在依赖关系时,考虑setState的异步问题 (redux)

客户端上,因为 react 中 setState 的异步机制,因此在同个component中触发多个action,会出现一种状况是:第一个 action 对 state 的改变还没来得及更新component时,第二个action便开始执行,即第二个 action 将使用到未更新的值。
而在同构中,若是第一个 action (以下的 fetchData)是在服务端执行了,第二个 action 在客户端执行时将使用到的是第一个 action 对 state 改变后的值,即更新后的值。这时,同构须要作兼容处理。

fetchData() {
    this.props.setCourse(lastCourseId, lastCourseName);
}
render() {
    this.props.updateTab(TAB);
}

11. immutable 在同构上的姿式 (immutable/redux)

手Q家校群上使用了 immutable 来保证数据的不可变,提升数据对比速度,而在同构时须要注意两点
1.服务端上,从 store 中拿到的 state 为immutable对象,需转成 string 再同HTML返回
2.客户端上,从服务端注入到HTML上的 state 数据,须要将其转成 immutable对象,再放到 configureStore 中,如

var __serverData__ = Immutable.fromJS(window.__serverData__);
var store = configureStore(__serverData__);

12. 使用 webpack 去作 ES6 语法兼容 (webpack)

实际上,若是是一个单独的服务的话,可使用babel提供的方式来让node环境兼容好 E6

require("babel-register")({
    extensions: [".jsx"],
    presets: ['react']
});
require("babel-polyfill");

但若是是以同一个直出服务器,多个项目的直出代码都放在这个服务上,那么,仍是建议使用 webpack 的方式去兼容 ES6,减小 babel 对全局环境的影响。使用 webpack 的话,在项目完成后,可将 es6 代码编译成 es5 再放到真正的 server 上,这样也能够减小动态编译耗时。

13. 不使用 webpack 的 css in js 的方式

使用webpack时,默认是将css文件以 css in js 的方式打包起来,这种状况将增长服务端运行耗时,经过将 css 外链,或在webpack打包成独立的css文件后再inline进去,能够减小服务端的处理耗时及负荷。

14. UglifyJsPlugin 在服务端编译时慎用

上面说起使用webpack编译后的代码放到真正的server上去跑,在前端发布前通常会进行代码uglify,然后端实际上没多大必要,在实际应用中发现,使用 UglifyJsPlugin 后运行服务端会报错,需慎用。

15. 纠正 dirname 与 filename 的值 (webpack)

当服务端代码须要使用到 dirname 时,需在 webpack.config.js 配置 target 为 node,并在 node 中声明filename和dirname为true,不然拿不到准确值,如在服务端代码上添加 console.log(dirname); 和 console.log(__filenam );
在服务端使用的 webpack 上指定 target 为 node,以下

target: 'node', 
node: {
    __filename: true,
    __dirname: true
}

经 webpack 编译后输出以下代码,可看出 dirname 和 filename 将正确输出(注:需考虑生成的路径是否能在不一样系统上跑,以下图是在window下,使用的是双斜杠)
node

而不在webpack上配置时,dirname则为 / ,filename则为文件名,这是不正确的
target node

16.将 webpack 编译后的文件暴露出来 (webpack)

使用 webpack 将一个模块编译后将造成一个当即执行函数,函数中返回对象。若是须要将编译后的代码也做为一个模块供其余地方使用时,那么须要从新将该模块暴露出去( 如当业务上的直出代码只是做为直出服务器的其中一个任务时,那么须要将编译后的代码做为一个模块 exports 出去,即在编译后代码前从新加上 module.exports =,从而直出服务将可以使用到这个编译后的模块代码 )。写了一个 webpack 插件来自动添加 module.exports,比较简单,有兴趣的欢迎使用 webpack-add-module-expors,效果以下

编译前
222222222

编译后
exports

使用 webpack-add-module-expors编译后将带上module.exports
3331

17. 去掉index.scss和浏览器专用模块(webpack)

当服务端上不想处理样式模块或一些浏览器才须要的模块(如前端上报)时,须要在服务端上将其忽略。尝试 webpack 自带的 webpack.IgnorePlugin 插件后出现一些奇奇怪怪的问题,重温 如何开发一个 Webpack Loader ( 一 ) 时想起 webpack 在执行时会将原文件经webpack loaders进行转换,如 jsx 转成 js等。因此想法是将在服务端上须要忽略的模块,在loader前执行前就将其忽略。写了个 ignored-loader,能够将须要忽略的模块在 loader 执行前直接返回空,因此后续就再也不作其余处理,简单但也知足现有需求。

优化成果

服务端上的耗时增长了,但总体上的首屏渲染完成时间大大减小

服务端上增长的耗时

服务端渲染方案将数据的拉取和模板的渲染从客户端移到了服务端,因为服务端的环境以及数据拉取存在优点(详见 Node直出理论与实践总结),因此在相比下,这块耗时大大减小,但确实存在,这两块耗时是服务端渲染相比于客户端渲染在服务端上多出来。因此本次也作了耗时的数据统计,以下图

default

从统计的数据上看,服务端上数据拉取的时间约 61.75 ms,服务端render耗时为16.32 ms,这两块时间的和为 78 ms,这耗时仍是比较大。因此这次在同构耗时在计算上包含了服务端数据拉取与模板渲染的时间

首屏渲染完成时间对比

服务端渲染时因为不须要等待 JS 加载和 数据请求(详见 Node直出理论与实践总结),在首屏展现时间耗时上将大大减小,这次在手Q家校群列表页首屏渲染完成时间上,优化前平均耗时约1643.914 ms,而同构优化后平均耗时为 696.62 ms,有了 947ms 的优化,提高约 57.5% 的性能,秒开搓搓有余!

default

default

优化前与优化后的页面展现状况对比

1.优化前
predata

2.优化后(同构直出)
iso

可明显看出同构直出后,白屏时间大大减小,可交互时间也获得了提早,产品体验将变得更好。

总结

服务端渲染的方式可以很好的减小首屏展现时间,React 同构的方式让先后端模板、类库、以及数据模型上共用,大大减小的服务端渲染的工做量。
因为在服务端上渲染模板,render 时过多的调用栈增长了服务端负载,也增长了 CPU 的压力,因此能够只直出首屏可视区域,减小Component层级,减小调用栈,最后,作好容灾方案,如真的服务端挂了( 虽然状况比较少 ),能够直接切换到普通的客户端渲染方案,保证用户体验。

以上,即是近期在 React 同构上的实践总结,若有不妥,恳请斧正,谢谢。

查看更多文章 >>
https://github.com/joeyguo/blog


原文连接:https://www.qcloud.com/community/article/361792

相关文章
相关标签/搜索