React同构与极致的性能优化
xvqs9642
7年前
<p>注: 本文为 <a href="/misc/goto?guid=4958864570595779234" rel="nofollow,noindex">第12届D2前端技术论坛《打造高可靠与高性能的React同构解决方案》</a> 分享内容,已经过数据脱敏处理。</p> <h2>前言</h2> <ul> <li>随着React的兴起, 结合Node直出的性能优势和React的组件化,React同构已然成为趋势之一。享受技术福利的同时,直面技术挑战,在复杂场景下,挑战10倍以上极致的性能优化。</li> </ul> <h2>什么是同构?</h2> <ul> <li>一套代码既可以在服务端运行又可以在客户端运行,这就是同构应用。简而言之, 就是服务端直出和客户端渲染的组合, 能够充分结合两者的优势,并有效避免两者的不足。</li> </ul> <h2>为什么同构?</h2> <ul> <li>性能: 通过Node直出, 将传统的三次串行http请求简化成一次http请求,降低首屏渲染时间</li> <li>SEO: 服务端渲染对搜索引擎的爬取有着天然的优势,虽然阿里电商体系对SEO需求并不强,但随着国际化的推进, 越来越多的国际业务加入阿里大家庭,很多的业务依赖Google等搜索引擎的流量导入,比如Lazada.</li> <li>兼容性: 部分展示类页面能够有效规避客户端兼容性问题,比如白屏。</li> </ul> <h2>性能数据</h2> <p>性能是一个综合性的问题, 不能简单地断言同构应用一定比非同构应用性能好,只能说合适的场景加上合理的运用,同构应用确实能带来一定的性能提升, 先来看一个线上的案例。</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/70f000616083f997ab76f2e9cb1fda2e.gif"></p> <p>通常来说,网络状况越差,同构的优势越明显,下图是在不同网络状况下首屏渲染时间的一组对比</p> <p><img src="https://simg.open-open.com/show/32bc40f4e5fca04108f2e9408d49232c.png"></p> <h2>线上案例</h2> <ul> <li>近两年,无论是业界还是阿里内部都涌现了大量同构实践, 业界比较有影响力的包括非死book, Quora, Medium, 推ter, Airbnb, Walmart、手Q以及QQ兴趣部落等</li> <li> <p>阿里内部也有大量的应用,仅列举部分 <a href="/misc/goto?guid=4959755856321307251" rel="nofollow,noindex">beidou开发组</a> 做过技术支持的项目</p> <ul> <li>阿里云 - 大数据地产</li> <li>钉钉 - 企业主页</li> <li>钉钉 - 钉钉日志和审批模板市场</li> <li>菜鸟 - 物流大市场</li> <li>云零售 - 店掌柜</li> <li>Lazada - PDP</li> <li>国际事业部 - AGLA</li> <li>AILab - 行业解决方案</li> <li>AILab - 智能硬件平台</li> <li>AILab - AliGenie开放平台</li> <li>AILab - AR官网</li> <li>ICBU - ICBU店铺</li> <li>业务平台 - 门店评价</li> <li>国际UED - 数据运营</li> <li>国际UED - 知之</li> <li>国际UED - 探花</li> <li>国际UED - Nuke官网及过程管理</li> <li>国际UED - 会议记录,实时翻译</li> <li>国际UED - LBS数据地图</li> <li>国际UED - 数探</li> <li>国际UED - 微策</li> <li>国际UED - shuttle</li> <li>国际UED - fie portal</li> <li>...</li> </ul> </li> </ul> <h2>业界生态</h2> <ul> <li><a href="/misc/goto?guid=4959755856420919575" rel="nofollow,noindex">react-server</a> : React服务端渲染框架</li> <li><a href="/misc/goto?guid=4959724300407068588" rel="nofollow,noindex">next.js</a> : 轻量级的同构框架</li> <li><a href="/misc/goto?guid=4959755856321307251" rel="nofollow,noindex">beidou</a> : 阿里自己的同构框架,基于eggjs, 定位是企业级同构框架</li> </ul> <p>除了开源框架,底层方面React16重构了SSR, react-router提供了更加友好的SSR支持等等, 从某种程度上来说,同构也是一种趋势,至少是方向之一。</p> <h2>思考 与 实现</h2> <p>同构的出发点不是 “为了做同构,所以做了”, 而是回归业务,去解决业务场景中SEO、首屏性能、用户体验 等问题,驱动我们去寻找可用的解决方案。在这样的场景下,除了同构本身,我们还需要考虑的是:</p> <ul> <li>高性能的 Node Server</li> <li>可靠的 同构渲染服务</li> <li>可控的 运维成本</li> <li>可复用的 解决方案</li> <li>...</li> </ul> <p>简单归纳就是, 我们需要一个 企业级的同构渲染解决方案。</p> <p>我们是怎么做的?</p> <p>基于 eggjs 加入可拔插的同构能力</p> <ul> <li><a href="/misc/goto?guid=4959755856559971835" rel="nofollow,noindex">beidou-plugin-react</a><br> 作为原有MVC架构中, view 层的替换, 使用 React 组件作为视图层模板, 可以直接渲染 React Component 并输出给客户端</li> <li><a href="/misc/goto?guid=4959755856652973755" rel="nofollow,noindex">beidou-plugin-webpack</a><br> 集成 Webpack 到框架中, 在开发阶段, 提供代码的编译和打包服务</li> <li><a href="/misc/goto?guid=4959755856753074694" rel="nofollow,noindex">beidou-plugin-isomorphic</a><br> 服务端的 React 运行时: babel-register<br> polyfill 注入: 环境变量, BOM等<br> 非js文件解析: css, images, fonts...</li> <li>服务端支持css modules</li> <li><a href="/misc/goto?guid=4959755856832232944" rel="nofollow,noindex">自动路由</a> : 纯静态页面无需编写任何服务端代码,像写纯前端页面一样简单</li> <li>...</li> </ul> <p style="text-align: center;"><img src="https://simg.open-open.com/show/e363bb4c1e925f64f3bb81237befdd71.png"></p> <p>这里不再赘述具体如何实现,有兴趣的读者可以阅读我们的开源同构框架 <a href="/misc/goto?guid=4959755856321307251" rel="nofollow,noindex">beidou</a> -- <a href="/misc/goto?guid=4959755856321307251" rel="nofollow,noindex">https://github.com/alibaba/beidou</a></p> <h2>热点问题</h2> <p>任何一种技术都有其适用场景和局限性, 同构也不例外,以下试举一二,以做抛砖引玉.</p> <ul> <li>内存泄漏</li> <li>性能瓶颈</li> <li>...</li> </ul> <p>内存泄漏不是同构应用所特有的,理论上所有服务端应用都可能内存泄漏,但同构应用是“高危群体”, 具体如何解决请参考本人的 <a href="/misc/goto?guid=4959755856931461347" rel="nofollow,noindex">《Node应用内存泄漏分析方法论与实战》</a> , 接下来重点剖析下性能优化。</p> <h2>极致的性能优化</h2> <p>前面也提到了,同构应用并不一定就比非同构应用性能好,影响性能的因素实在太多了,再来看一组数据</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/f86bc780e557d362fb1f45fa62d126c1.png"></p> <p>上图是基于Node v8.9.1 和 React@15.5.4, 开4个进程采集到的数据, X轴是最终生成页面节点数量,Y轴红色的线表示RT(包括渲染时间和网络时间), 绿色的柱子表示QPS. 可以看出来:</p> <ul> <li>随着页面节点的增多渲染时间可能变得很长,QPS下降非常迅速。在页面节点超过3000左右的时候,QPS接近个位数了,而且实际页面中可能包含较复杂的逻辑以及不友好的写法,情况可能会更糟。</li> </ul> <p>顺带提一下, 笔者采样了 <a href="/misc/goto?guid=4959755857025874200" rel="nofollow,noindex">淘宝首页</a> 和 <a href="/misc/goto?guid=4959755857106335957" rel="nofollow,noindex">淘宝某详情页</a> 以及 <a href="/misc/goto?guid=4959755857184858088" rel="nofollow,noindex">Lazada某详情页</a> ,页面节点数分别是2620、2467和3701. 大部分情况下,页面节点数低于1000, 比如 <a href="/misc/goto?guid=4959755857282337787" rel="nofollow,noindex">菜鸟物流市场</a> 首页看起来内容不少,其实节点数是775.</p> <p>那针对3000节点以上的页面,我们该怎么做呢?笔者总结了以下策略并重点阐述其中一两点:</p> <ul> <li>采用编译后的React版本: 根据Sasha Aickin的博客,React15在Node4、Node6、Node8下,采用编译后的版本性能相比未编译版本分别提升了2.36倍、3倍、3.85倍</li> <li>模块拆分: 模块拆分有利于并发渲染,目前ICBU店铺装修采用的就是这种方式</li> <li>模块级别缓存: 页面中某些模块其实是很适合缓存的,比如Lazada详情页中节点数虽然高达3701, 但其实页头部分就占比55.5%,页尾占比3.5%,而页头页尾是常年不变的.</li> <li>组件级缓存: 最小粒度的缓存单位了,性能提升依赖于缓存的范围和命中率,运用得当,可能带来非常大的性能提升。参考 <a href="/misc/goto?guid=4959755857360843681" rel="nofollow,noindex">walmartlabs</a></li> <li>采用hsf代替http对外提供服务: hsf的网络消耗远低于http, 在店铺同构实践中,改用hsf, java端调用Node端的耗时缩短了一半.</li> <li>部分模块客户端渲染(对SEO无用的部分): 直接降低SSR部分的复杂度</li> <li>智能降级: 当流量暴增,接近或超过阈值时,会直接导致服务的RT快速上升。可以实时监测CPU和内存的使用率,超过一定的比例自动降级为客户端渲染,降低服务端压力,CPU和内存恢复常态时,自动切回服务端渲染。</li> <li>采用Node8: 同样在店铺实践中,采用Node8相比Node6, 渲染时间从28ms降低到了18ms, 提升幅度为36%.</li> <li>采用最新版React16: <a href="/misc/goto?guid=4959755857443022530" rel="nofollow,noindex">非死book官方数据</a> , 在Node8下,React16相比编译后的react15仍有3.8倍提升,相比未编译的React15更是有数量级的提升。</li> </ul> <p>组件级缓存</p> <p>如果说性能优化有"万能"的招式,那一定是缓存, 从Nigix缓存到模块级缓存到组件级缓存,其中最让人兴奋的就是组件级缓存,让我们一起来看看如何实现</p> <ul> <li> <p>拦截React的渲染逻辑,业界主要有三种实现方式</p> <ul> <li>Fork一份React, 暴力加入缓存逻辑, 代表库是 <a href="/misc/goto?guid=4959754790594457975" rel="nofollow,noindex">react-dom-stream</a> , 虽然这个库的人气很高,但笔者还是反对这种实现方式的。</li> <li>通过require hook拦截instantiateReactComponent的载入并注入缓存逻辑,参考 <a href="/misc/goto?guid=4959755857551199306" rel="nofollow,noindex">react-ssr-optimization</a></li> <li>扩展ReactCompositeComponent的mountComponent方法,参考 <a href="/misc/goto?guid=4959755857639241946" rel="nofollow,noindex">electrode-react-ssr-cachin</a></li> </ul> </li> <li>注入缓存逻辑, 代码如下</li> </ul> <pre> <code class="language-javascript">const ReactCompositeComponent = require("react/lib/ReactCompositeComponent"); ReactCompositeComponent.Mixin._mountComponent = ReactCompositeComponent.Mixin.mountComponent; ReactCompositeComponent.Mixin.mountComponent = function(rootID, transaction, context) { const hashKey = generateHashKey(this._currentElement.props); if (cacheStorage.hasEntry(hashKey)) { // 命中缓存则直接返回缓存结果 return cacheStorage.getEntry(hashKey); } else { // 若未命中,则调用react的mountComponent渲染组件,并缓存结果 const html = this._mountComponent(rootID, transaction, context); cacheStorage.addEntry(hashKey, html); return html; } };</code></pre> <ul> <li>设置最大缓存和缓存更新策略</li> </ul> <pre> <code class="language-javascript">lruCacheSettings: { max: 500, // The maximum size of the cache maxAge: 1000 * 5 // The maximum age in milliseconds }</code></pre> <p>上述缓存逻辑是基于属性的,能覆盖大部分的应用场景,但有一个要求,属性值必须可枚举且可选项很少. 请看下面的场景。</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/67c773754d2fbe5699b0976d888fa1c1.png"></p> <p>淘宝某页面上有大量的商品,而淘宝的商品又何止百万,就算某个被缓存,下次被命中的可能性依然微乎其微。那如何解决这个问题?聪明的读者可能已经看出来了,虽然每个商品最终渲染的结果千变万化,但结构始终是一致的,因此结构是可以缓存的。</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/2791943b376c316f5151da9a7378553a.png"></p> <p>要实现结构的缓存,需要在上述逻辑上额外新增三步。</p> <ul> <li> <p>生成中间结构:</p> <ul> <li>以组件 <Price>${price}</Price> 为例,将变量price以占位符 ${price} 代替 set(price, "${price}") , 再调用react原生的mountComponent方法则可以生成中间结构 <div>${price}</div</li> </ul> </li> <li>缓存中间结构</li> <li>生成最终组件</li> </ul> <p>以上就是组件级缓存的实现方式, 特别要提醒的是缓存是把双刃剑,运用不当可能会引发内存泄漏以及数据的不一致。</p> <p>React16 SSR</p> <ul> <li>FB在9.26发布了React16正式版,之前万众期待的 <a href="/misc/goto?guid=4959755857721588693" rel="nofollow,noindex">SSR性能提升</a> 没有让大家失望, 引用React核心开发Sasha Aickin的对比图</li> </ul> <p style="text-align: center;"><img src="https://simg.open-open.com/show/15b9a570bfb972a19a5732326c84e355.png"></p> <p>笔者拿之前的应用升级到React16, 对比下3909节点,RT从295ms降到了51ms, QPS从9提升到了44, 提升非常明显。</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/68b068cce678ba6cbf1726d322925c8f.png"></p> <h2>实战</h2> <p>接下来通过一个例子,展示如何一步步地提升性能。</p> <p><a href="/misc/goto?guid=4959755857820523879" rel="nofollow,noindex">代码仓库</a> -- <a href="/misc/goto?guid=4959755857908607042" rel="nofollow,noindex">https://github.com/alibaba/beidou/</a></p> <h3>10倍以上性能提升</h3> <ul> <li>首先构造一个非常复杂的页面, 页面节点数是3342, 对比之下, <a href="/misc/goto?guid=4959755857025874200" rel="nofollow,noindex">淘宝首页</a> 首屏的页面节点数是831, 异步充分加载之后(懒加载完成),整个页面节点数为3049. 注: 淘宝页面为动态页面,每次采样可能会有差异。</li> </ul> <p style="text-align: center;"><img src="https://simg.open-open.com/show/ad0b1817187ad8d137532599abb8c213.jpg"></p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/e108ad1f160fb630dedaf78dda529bd0.jpg"></p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/9310bec68374e9a66f064e2e5ed8b2d5.jpg"></p> <ul> <li>初始平均渲染时间为 295.75ms (Node6.92, React15.6.2), 注: 图中有 296.50ms , 317.25ms , 297.25ms , 295.75ms 四个平均值,是因为开启了四个进程,采样最后一个,下同。</li> </ul> <p style="text-align: center;"><img src="https://simg.open-open.com/show/b7a81bd620908ace84153d484a973d93.jpg"></p> <ul> <li>启用 <a href="/misc/goto?guid=4959755858003868480" rel="nofollow,noindex">babel性能加速插件</a> , 平均渲染时间为 219.00ms</li> </ul> <p style="text-align: center;"><img src="https://simg.open-open.com/show/60b67ca712e46abfe2e792206a7f37b1.jpg"></p> <ul> <li>采用Node8.9.1(或更新版本)平均渲染时间为 207ms</li> </ul> <p style="text-align: center;"><img src="https://simg.open-open.com/show/4f9e7cda1805152de8f0258a5fcd007f.jpg"></p> <ul> <li>采用 production 模式平均渲染时间为 81.75ms</li> </ul> <p style="text-align: center;"><img src="https://simg.open-open.com/show/e0ca5e5fef323d0af2507ebeebef9528.jpg"></p> <ul> <li>部分内容客户端渲染,平均渲染时间为 44.63ms</li> </ul> <p style="text-align: center;"><img src="https://simg.open-open.com/show/c5c73d2541eac5c8cad870ab5234200b.jpg"></p> <ul> <li>部分内容组件级别cache,平均渲染时间为 22.65ms</li> </ul> <p style="text-align: center;"><img src="https://simg.open-open.com/show/0245a257cce8f373ee05054654476d53.jpg"></p> <ul> <li>采用React16(或更新版本),平均渲染时间为 5.17ms</li> </ul> <p style="text-align: center;"><img src="https://simg.open-open.com/show/cfd70f404766ce7ae85e7d79ac82227a.jpg"></p> <ul> <li>结合React16和部分客户端渲染,平均渲染时间为 2.68ms</li> </ul> <p style="text-align: center;"><img src="https://simg.open-open.com/show/521942c9d7f59852238070dc9f8d269e.jpg"></p> <p>至此,服务端渲染时间已经最初的 295.75ms 降低到了 2.68ms ,提升了超过100倍。</p> <h3>更多性能策略</h3> <p>其实除了上述应用的策略,还有其它的策略,比如</p> <ul> <li>采用 Async , 有数据称性能提升30%, 笔者试了下,未见明显提升。应该是经过了babel的编译,最终没有发挥出 Async 的优势,这是因为 <a href="/misc/goto?guid=4959755856321307251" rel="nofollow,noindex">beidou框架</a> 在服务端要支持 import 等ES6的写法以及支持React的 JSX语法 。其实也非常简单,直接缩小 babel 的编译范围,在 <a href="/misc/goto?guid=4959755856321307251" rel="nofollow,noindex">beidou框架</a> 中是可以自己定义的。</li> <li>降低React组件的嵌套层级。试验数据,同样的页面节点数,服务端渲染时间和组件的嵌套层级是线性正相关的。</li> <li>热点缓存</li> </ul> <p>...</p> <h3>万变不离其宗</h3> <p>借用《功夫》中的一句经典台词 天下武功,无坚不破,唯快不破 ,同样的,</p> <p>随着时间的推移,上面这些策略策略迟早会 被破 ,比如react16 ssr重构之后,之前的组件级别缓存逻辑不再有效。</p> <p>另外,可能由于架构设计/技术选型根本就使不上劲,比如react16是今年9月26才正式发版,很多第三方组件还没来得及升级,如果应用中有些组件强依赖于react15或者更早的版本,可能根本就没法利用react16的性能优势。</p> <p>那么有没有一种 万能的办法 ,能够做到 唯快不破 呢?</p> <p>答案是: 有的。 只有掌握了方法论,才能在不断变化中,找到适合自己应用的性能优化策略。</p> <p> </p> <p> </p> <p>来自:https://segmentfault.com/a/1190000012464033</p> <p> </p>