-
Notifications
You must be signed in to change notification settings - Fork 107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
React同构直出优化总结 #9
Comments
React 数据流管理架构之Redux 这个链接不对. |
@myheartwillgoon 已更正,多谢。 |
请教下你们首屏时延统计的是从哪个时间点到哪个时间点? |
@chenwery 同构的话,打在JS加载前。正常的话打在Cgi拉取回来后渲染的时间。要看具体代码逻辑来定。 |
babel-node 编译的时候,碰到css文件(import(a.scss))会直接报错,请问有什么方法可以解决吗? |
@ibufu 使用 webpack 去编译的话可以使用 https://github.com/joeyguo/ignored-loader 去忽略掉 scss, 或sass这些。如果直接用 babel-node的话,不知道有没有现有的babel plugin,我之前是自己写一个 babel-plugin 去忽略,不过觉得还是webpack合适些 |
请教一下,服务端render完的htmlstring不是带了数据的么,客户端再拉一次是出于什么原因?需要更新数据时不也应该在update中做吗? |
@ImJoeHs 说的是这点吗?“3. 服务端需提前拉取数据,客户端则在 componentDidMount 调用”。客户端的 componentDidMount 拉取数据是需要做一层判断的:如果已经是同构的,那么将不再拉取。非同构的,则需要去拉取。所以是一次拉取而已哈 |
@joeyguo 原来如此,非常感谢。 |
好文章 都是干货 |
请教一个问题,单页应用首页后端同构直出了,使用react-router作为路由,利用require.ensure做按需加载js,在客户端没问题。但是如果直接访问做了按需加载的url地址会出现错误。后端渲染的话,是否无法做到异步加载呢? |
@jypblue 是说 后端没问题,而默认的前端渲染却报错的意思吗? 后端渲染,在客户端上去加载异步资源,这个有遇到什么问题吗? |
@jypblue 用webpack的defined插件做一下区别对待就可以了,服务端全部加载,客户端按需 |
实例:
使用:
参考项目:isomorphic-order |
@joeyguo 我直接贴代码吧。
我是后端同构渲染的,默认访问localhots:3000,再在页面上点击about的Link标签链接,可以异步加载about的相关js进来,但是如果我直接输入url,localhost:3000/about直接访问的话,就会报错。就会报错说require.ensure is not a function. |
@joeyguo thank you,受用 |
你好,感谢你的文章使我学到很多。我想问问如果所有页面请求都做服务端渲染,是不是开发效率比较低,但用户体验比较好? |
@Penggggg 服务端渲染主要减少的是数据拉取的rtt时间,如果只是静态页面(无需再拉取数据)的话,那其实速度并不会提升。反之,则会在一定程度上提速,用户体验也会较好。可以看下另外一篇文章对服务端渲染耗时的对比分析Node直出理论与实践总结 |
「10」. 两个 action 在同个component中数据存在依赖关系时,考虑setState的异步问题 (redux) 这里的兼容处理是怎么进行的,就是数据同步的具体方案是什么 我的理解是引入 async/await 等待服务端数据拉取完毕 触发dispatch |
@evilemon 你这个问题不存在啊,如果依赖的数据需要被action改变,那它永远应该是在reduxStore里的,反之才有可能存在state里面,那为什么需要同步呢。setState异步这个特性不需要“被解决”啊,这样才能确保我们依赖的值是同一组,需要“被解决”的是服务端渲染导致的不同state。 |
@evilemon 这里本质问题是,在同个component中,前一个action A更新的state还没来得及更新到component中,接下去的action B触发时机或参数(或其他逻辑)又依赖了上一个action所更新state值,或者如果前一个是异步 action (如请求数据),那是需要考虑这个“setState的异步“(component中所使用的state是否是更新了的问题),因为上一个action在服务端上已更新了state,component上也就是更新后的值,这样的话在同构就需要做区分处理了。另外在非异步action下,如果依赖的是reduxStore里面的state,是不需要考虑setState的异步问题的,如 @ImJoeHs 所说。 |
为什么不用RN开发呢? |
@yongbingz 直接上 RN 需要涉及到客户端的改造,周期会比较长;那会 RN 也比较不稳定,对 React 的使用也处于尝试阶段。现在有将一些页面尝试用 RN 来改造,对 React 的熟悉也能够增强对 RN 的把握。 |
想问下有跟将静态资源提前压缩内置客户端的方案的数据对比吗? |
@dickeylth 有的,在该项目中首屏可视时间 同构直出的耗时稍少于离线包方案(也就是资源提前拉取内置客户端方案),不过这跟具体项目有所关系。实际上这两种方案会一起使用,一个主要针对线上(无离线包情况),一个针对离线。 |
@joeyguo 了解了,感谢! |
干货,支持一下 |
后端返回了HTMlString之后,这样爬虫就可以爬到了对吗? |
@cqupt-yifanwu 是的,可以提高浏览器的收录,优化SEO。 |
@cqupt-yifanwu 同构直出,显然是服务端渲染啊……还 ajax 做这个优化有啥意义 |
结果看起来很吸引人啊。 |
@joeyguo 关于第14条,现在使用webpack3的话build时带上-p (production)参数,默认进行uglify, server端code并没有什么问题, 估计已经fix uglifyjs的问题了。 不过server的code确实不需要uglify, 还不知道如何在加入production option后disable uglifyjs |
@joeyguo 好文章,受教了! |
@chesscai 支持同构的框架下最终用的代码为一份,只是需要加一些渲染逻辑兼容。可以理解为服务端渲染出错或着说失败的情况下使用原客户端的html返回,接着就走原有的客户端渲染逻辑了。 |
有一个疑问请指点一下:
|
你好,服务端渲染的时候如果忽略scss文件,那么返回的首屏页面是没有样式的,样式在客户端通过js注入到页面中,这样会出现一段时间的空白样式,这个要怎么处理呢? |
@fightingm 可以抽离首屏的样式inline到页面 |
原文地址
React 的实践从去年在 PC QQ家校群开始,由于 PC 上的网络及环境都相当好,所以在使用时可谓一帆风顺,偶尔遇到点小磕绊,也能够快速地填补磨平。而最近一段时间,我们将手Q的家校群重构成 React,除了原有框架上存在明显问题的原因外,选择React也是因为它确实有足够的吸引力以及优势,加之在PC家校群上的实践经验,斟酌下便开始了,到现在已有页面在线上正常跑起。
由于移动端上的网络及环境迥异,性能偏差。所以在移动端上用 React 时,遇到了不少的坑点,也花了一些力气在上面。关于在移动端上的优化,可看我们团队的另一篇文章的 React移动端web极致优化
一提到优化,不得不提直出
关于这块可以查看 Node直出理论与实践总结,这篇文章较详细的分析直出的概念及一步步优化,也结合了 手Q家校群使用快速的数据直出方式来优化性能的总结与性能数据分析
一提到 React,不得不提同构
同构基于服务端渲染,却不止是服务端渲染。
服务端渲染到同构的这一路
后台包办
服务端渲染的方案早在后台程序前后端包办的时代上就有了,那时候使用JSP、PHP等动态语言将数据与页面模版整合后输出给浏览器,一步到位
这个时候,前端开发跟后端揉为一体,项目小的时候,前后端的开发和调试还真可以称为一步到位。但当项目庞大起来的时候,无论是修改某个样式要起一个庞大服务的尴尬,还是前后端糅合的地带变得越来越难以维护,都很难过。
前后分离
前后端分离后,服务端渲染的模式就开始被淡化了。这时候的服务端渲染比较尴尬,由于前后端的编码语言不同,连页面模板都不能复用,只能让在前后端开发完成后,再将前端代码改为给后端使用的页面模板,增大了工作量。最终也还是跟后台包办殊途同归。
语言变通
Node 驾着祥云腾空而来,谷歌 V8 引擎给力支持,众前端拿着看家本领(JavaScript)开始涉足服务端,于是服务端渲染上又一步进阶
由于前后端时候的相同的语言,所以前后端在代码的共用上达到了新的高度,页面模版、node modules 都可以做成前后通用。同构的雏形,只是共用的代码还是有局限。
前后同构
有了Node 后,前端便有了更多的想象空间。前端框架开始考虑兼容服务端渲染,提供更方便的 API,前后端共用一套代码的方案,让服务端渲染越来越便捷。当然,不只是 React 做了这件事,但 React 将这种思想推向高潮,同构的概念也开始广为人传。
关于 React 网上已有大多教程,可以查看阮老师的react-demos。关于 React 上的数据流管理方案,现在最为火热的 Redux 应该是首选,具体可以查看另一篇文章 React 数据流管理架构之Redux,此篇就不再赘述,下面讲讲 React 同构的理论与在手Q家校群上的具体实践总结。
React 同构
React 虚拟 Dom
React 的虚拟 Dom 以对象树的形式保存在内存中,并存在前后端两种展露原型的形式
React 同构的关键要素
完善的 Component 属性及生命周期与客户端的 render 时机是 React 同构的关键。
DOM 的一致性
在前后端渲染相同的 Component,将输出一致的 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
React 在客户端通过 checksum 判断是否需要重新render
相同则不重新render,省略创建DOM和挂载DOM的过程,接着触发 componentDidMount 等事件来处理服务端上的未尽事宜(事件绑定等),从而加快了交互时间;不同时,组件将客户端上被重新挂载 render。
renderToStaticMarkup 则不会生成与 react 相关的data-*,也不存在 checksum,输出的 html 如下
在客户端时组件会被重新挂载,客户端重新挂载不生成 checknum( 也没这个必要 ),所以该方法只当服务端上所渲染的组件在客户端不需要时才使用
2. 服务端上的数据状态与同步给客户端
服务端上的产生的数据需要随着页面一同返回,客户端使用该数据去 render,从而保持状态一致。服务端上使用 renderToString 而在客户端上依然重新挂载组件的情况大多是因为在返回 HTML 的时候没有将服务端上的数据一同返回,或者是返回的数据格式不对导致,开发时可以留意 chrome 上的提示如
3. 服务端需提前拉取数据,客户端则在 componentDidMount 调用
平台上的差异,服务端渲染只会执行到 compnentWillMount 上,所以为了达到同构的目的,可以把拉取数据的逻辑写到 React Class 的静态方法上,一方面服务端上可以通过直接操作静态方法来提前拉取数据再根据数据生成 HTML,另一方面客户端可以在 componentDidMount 时去调用该静态方法拉取数据
4. 保持数据的确定性
这里指影响组件 render 结果的数据,举个例子,下面的组件由于在服务端与客户端渲染上会因为组件上产生不同随机数的原因而导致客户端将重新渲染。
可以将 Math.random() 封装至Component 的 props 中,在服务端上生成随机数并传入到这个component中,从而保证随机数在客户端和服务端一致。如
服务端上传入randomNum
5. 平台区分
当前后端共用一套代码的时候,像前端特有的 Window 对象,Ajax 请求 在后端是无法使用上的,后端需要去掉这些前端特有的对象逻辑或使用对应的后端方案,如后端可以使用 http.request 替代 Ajax 请求,所以需要进行平台区分,主要有以下几种方式
1.代码使用前后端通用的模块,如 isomorphic-fetch
2.前后端通过webpack 配置 resolve.alias 对应不同的文件,如
客户端使用 /browser/request.js 来做 ajax 请求
服务端 webpack 上使用 /server/request.js 以 http.request 替代 ajax 请求
3.使用 webpack.DefinePlugin 在构建时添加一个平台区分的值,这种方式的在 webpack UglifyJsPlugin 编译后,非当前平台( 不可达代码 )的代码将会被去掉,不会增加文件大小。如
在服务端的 webpack 加上下面配置
在JS逻辑上做判断
4.window 是浏览器上特有的对象,所以也可以用来做平台区分
6. 只直出首屏页面可视内容,其他在客户端上延迟处理
这是为了减少服务端的负担,也是加快首屏展示时间,如在手Q家校群列表中存在 “我发布的” 和 “全部” 两个 tab,内容都为作业列表,此次实践在服务端上只处理首屏可视内容,即只输出 “我发布的” 的完整HTML,另外一个tab的内容在客户端上通过 react 的 dom diff 机制来动态挂载,无页面刷新的感知。
7. componentWillReceiveProps 中,依赖数据变化的方法,需考虑在 componentDidMount 做兼容
举个例子,identity 默认为 UNKOWN,从后台拉取到数据后,更新其值,从而触发 setButton 方法
同构时,由于服务端上已做了第一次数据拉取,所以上面代码在客户端上将由于 identity 已存在而导致永不执行 setButton 方法,解决方式可在 componentDidMount 做兼容处理
8. redux在服务端上的使用方式 (redux)
下图为其中一种形式,先进行数据请求,再将请求到的数据 dispatch 一个 action,通过在reducer将数据进行 redux 的 state 化。还有其他方式,如直接 dispatch 一个 action,在action里面去做数据请求,后续是一样的,不过这样就要求请求数据的模块是 isomorphism 即前后端通用的。
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 改变后的值,即更新后的值。这时,同构需要做兼容处理。
11. immutable 在同构上的姿势 (immutable/redux)
手Q家校群上使用了 immutable 来保证数据的不可变,提高数据对比速度,而在同构时需要注意两点
1.服务端上,从 store 中拿到的 state 为immutable对象,需转成 string 再同HTML返回
2.客户端上,从服务端注入到HTML上的 state 数据,需要将其转成 immutable对象,再放到 configureStore 中,如
12. 使用 webpack 去做 ES6 语法兼容 (webpack)
实际上,如果是一个单独的服务的话,可以使用babel提供的方式来让node环境兼容好 E6
但如果是以同一个直出服务器,多个项目的直出代码都放在这个服务上,那么,还是建议使用 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,如下
经 webpack 编译后输出如下代码,可看出 __dirname 和 __filename 将正确输出(注:需考虑生成的路径是否能在不同系统上跑,如下图是在window下,使用的是双斜杠)
而不在webpack上配置时,__dirname则为 / ,__filename则为文件名,这是不正确的
16.将 webpack 编译后的文件暴露出来 (webpack)
使用 webpack 将一个模块编译后将形成一个立即执行函数,函数中返回对象。如果需要将编译后的代码也作为一个模块供其他地方使用时,那么需要重新将该模块暴露出去( 如当业务上的直出代码只是作为直出服务器的其中一个任务时,那么需要将编译后的代码作为一个模块 exports 出去,即在编译后代码前重新加上 module.exports =,从而直出服务将能够使用到这个编译后的模块代码 )。写了一个 webpack 插件来自动添加 module.exports,比较简单,有兴趣的欢迎使用 webpack-add-module-expors,效果如下
编译前
编译后
使用 webpack-add-module-expors编译后将带上module.exports
17. 去掉index.scss和浏览器专用模块(webpack)
当服务端上不想处理样式模块或一些浏览器才需要的模块(如前端上报)时,需要在服务端上将其忽略。尝试 webpack 自带的 webpack.IgnorePlugin 插件后出现一些奇奇怪怪的问题,重温 如何开发一个 Webpack Loader ( 一 ) 时想起 webpack 在执行时会将原文件经webpack loaders进行转换,如 jsx 转成 js等。所以想法是将在服务端上需要忽略的模块,在loader前执行前就将其忽略。写了个 ignored-loader,可以将需要忽略的模块在 loader 执行前直接返回空,所以后续就不再做其他处理,简单但也满足现有需求。
优化成果
服务端上的耗时增加了,但整体上的首屏渲染完成时间大大减少
服务端上增加的耗时
服务端渲染方案将数据的拉取和模板的渲染从客户端移到了服务端,由于服务端的环境以及数据拉取存在优势(详见 Node直出理论与实践总结),所以在相比下,这块耗时大大减少,但确实存在,这两块耗时是服务端渲染相比于客户端渲染在服务端上多出来。所以本次也做了耗时的数据统计,如下图
从统计的数据上看,服务端上数据拉取的时间约 61.75 ms,服务端render耗时为16.32 ms,这两块时间的和为 78 ms,这耗时还是比较大。所以此次在同构耗时在计算上包含了服务端数据拉取与模板渲染的时间
首屏渲染完成时间对比
服务端渲染时由于不需要等待 JS 加载和 数据请求(详见 Node直出理论与实践总结),在首屏展示时间耗时上将大大减少,此次在手Q家校群列表页首屏渲染完成时间上,优化前平均耗时约1643.914 ms,而同构优化后平均耗时为 696.62 ms,有了 947ms 的优化,提升约 57.5% 的性能,秒开搓搓有余!
优化前与优化后的页面展示情况对比
1.优化前
2.优化后(同构直出)
可明显看出同构直出后,白屏时间大大减少,可交互时间也得到了提前,产品体验将变得更好。
总结
服务端渲染的方式能够很好的减少首屏展示时间,React 同构的方式让前后端模板、类库、以及数据模型上共用,大大减少的服务端渲染的工作量。
由于在服务端上渲染模板,render 时过多的调用栈增加了服务端负载,也增加了 CPU 的压力,所以可以只直出首屏可视区域,减少Component层级,减少调用栈,最后,做好容灾方案,如真的服务端挂了( 虽然情况比较少 ),可以直接切换到普通的客户端渲染方案,保证用户体验。
以上,便是近期在 React 同构上的实践总结,如有不妥,恳请斧正,谢谢。
查看更多文章 >>
https://github.com/joeyguo/blog
The text was updated successfully, but these errors were encountered: