React 在中后台业务里已经很好落地了,但对付 C 端(给用户利用的端,比如 PC/H5)业务有其分外性,对性能哀求比较苛刻,且有 SEO 需求。其余团队层面也希望能够统一技能栈,小伙伴们希望发展,那么如何能够完成既要、也要、还要呢?
本次分享紧张环绕 C 端业务下得 React SSR 实践,会讲解各种 SSR 方案,包括 Next.js 同构开拓,并以一次优化的过程作为实例进行讲解。实在这些铺垫都是在事情中做的 Web 框架的设计而衍生出来的总结。这里先卖个关子,自研框架基于 Umi 框架并支持 SSR 的干系内容留到广州 QCon上讲,感兴趣的同学可以来 5 月的QCon 环球软件开拓大会广州站聊。下面开始正题。
曾和小弟谈论什么是 SSR?他开始以为 React SSR 便是 SSR,这是不完备对的,忽略了 Server-side Render 的实质。实在从早期的 cgi,到 PHP、ASP,jsp 等 server page,这些动态网页技能都是做事器端渲染的。而 React SSR 更多是强调基于 React 技能栈进行的做事器端渲染,是做事器端渲染的分类之一,本文会以 React SSR 为主进行讲解。
1、为什么要上 SSR?
对付 SSR,大家的认知大概因此下 3 个方面。
• SEO:强需求,被搜索引擎收录是网站的基本能力。
• C 端性能:至少要担保首屏渲染效率,如果秒开率都无法担保,那么用户体验是极差的。
• 统一技能栈:目前团队以 React 为主,无论从团队发展,还是个人发展角度,统一技能栈的好处都是非常明显的。
诚然,以上都是大家想用 SSR 的缘故原由,但对笔者来说,SSR 的意义远不止如此。在技能架构升级的过程中,如果能够同时带给团队和小伙伴发展,才是两全其美的选择。目前我卖力优酷 PC/H5 业务,在优酷落地 Node.js,目前在做 React SSR 干系整合事情。玉伯曾讲过在 All in Mobile 的时期的尴尬——对付多端来说是毁灭性的灾害。押宝移动端在当时是精确的选择,但在本日获客本钱过高,且移动端增速不敷,最好的选择便是多端在产品细节上做
PK,PC/H5 业务的活气也正在于此。
然而历史包袱如此的重,有几方面缘故原由。1)页面年久失落修;2)移动端在 All in Mobile 时期并没有给多端供应技能支持,PC/H5 是掉队的,须要补齐 App 真个基本能力;3)技能栈老旧,很多页面还是采取 jQuery 开拓的,对付团队来说,这才是最痛楚的事儿。
实在所有公司都是类似的,都是用有限资源干事,希望最少的投入带来最大化的产出。可以说,通过整合 SSR 一举三得,将 Node.js 和 React 一同落地,顺便将根本框架也落地升级,这样的投入产出是比较高的。
2、从 CSR 到 SSR 演进之路SSR 看起来很大略,如果细分一下,还是略微卖力的,下面和我一起看一下从 CSR 到 SSR 演进之路。
客户端渲染 (CSR)
客户端渲染是目前最大略的开拓办法,以 React 为例,CSR 里所有逻辑,数据获取、模板编译、路由等都是在浏览器做的。
Webpack 在工程化与构建方便供应了足够多便利,除了供应 Loader 和 Plugin 机制外,还将所有构建干系步骤都进行了封装,乃至连模块按需加载都内置,还具备 Tree-shaking 等能力,外加 Node cluster 利用多核并行构建。很明显这是非常方便的,对前端意义重大的。开拓者只须要关注业务模块即可。
常见做法是本地通过 Webpack 打包出 bundle.js,嵌入到大略的 HTML 模板里,然后将 HTML 和 bundle.js 都发布到 CDN 上。这样开拓办法是目前最常见的,对付做一些内部利用的管理系统是够的。
CSR 缺陷也是非常明显的,首屏性能无法保障,毕竟 React 百口桶根本库就很大,外加业务模块,纵使按需加载,依然很难担保秒开的。
为了优化 CSR 性能,业界有很多最佳实践。在 2018 年,笔者以为 React 最成功的项目是 CRA(create-react-app),支付宝开拓的 Umi 实在也是类似的。他们通过内置 Webpack 和常见 Webpack 中间件,办理了 Webpack 过于分散的问题。通过约定目录,统一开拓者的开拓习气。
与此同时,也产生了很多与时俱进的最佳实践。利用 react-router 结合 react-loadable,更优雅的做 dynamic import。在页面中切换路由时按需加载,在 Webpack 中做过代码分割,这是极好的实践。
以前是打包 bundle 是非常大的,现在以路由为切分标准,按需加载,效率自然是高的。
Umi 基于 react-router 又进步增强了,约定页面有布局的观点。
复制代码
export default { routes: [ { path: '/', component: './a' }, { path: '/list', component: './b', Routes: ['./routes/PrivateRoute.js'] }, { path: '/users', component: './users/_layout', routes: [ { path: '/users/detail', component: './users/detail' }, { path: '/users/:id', component: './users/id' } ] }, ],};
这样做的好处,就有点模板引擎中 include 类似的效果。布局提效也是极其明显的。为了演示优化后的效果,这里以 Umi 为例。它首先会加载 index 页面,找到 index 布局,先加载布局,然后再加载 index 页面里的组件。下图加了断点,你可以很清楚的看出加载过程。
在 create-react-app(cra)和 Umi 类似,都是通过约定,隐蔽详细实现细节,让开发者不须要关注构建。在未来,类似的封装还会有更多的封装,偏于运用层面。笔者以为前端开拓本钱在降落,未来有可能规模化的,由于框架一旦稳定,就有大量培训跟进,导致规模化开拓。这是把双刃剑,能知足企业开拓和招人的问题,但也在创新探索领域上了枷锁。
预渲染 (Prerending)
SPA(单页面运用) 的紧张内容都依赖于 JavaScript(bundle.js) 的实行,当首页 HTML 下载下来的时候,并不是完全的页面,而是浏览器里加载 HTML 并 JavaScript 文件才能完成渲染。用户在访问的时候体验会很好,但是对付搜索引擎是不好收录的,由于它们不能实行 JavaScript,这种场景下预渲染 (Prerending) 就派上用场了,它可以帮忙把页面渲染完成之后再返回给爬虫工具,我们的页面也就能被解析到了。
CSR 是由 bundle.js 来掌握渲染的,以是它外层的 HTML 都很薄。对付首屏渲染来说,如果能够先展示一部分布局内容,然后在走 CSR 的其他加载,效果会更好。其余业内有太多类似的事宜了,比如用 less 写 css,coffee 写 js,用 markdown 写博客,都是须要编译一次才能利用的。比如 Jekyll/Hexo 等著名项目,它们都非常好用。那么基于 React 技能,也一定会做预处理的,Gatsby/Next.js 都有类似的功能。将 React 组建编译成 HTML,可以编译全部,也可以只编译布局,对付页面性能来说,预渲染是非常大略的提升手段。其事理 JSX 模板和 Webpack stats 结合,进行预编译。
• 编译全部:纯静态页面。
• 只编译布局:对付 SPA 类项目是非常好,当然多页运用也可以只编译布局的。
天生纯 HTML,可以直接放到 CDN 上,这是大略的静态渲染。如果不直接天生 HTML,由 Node.js 来接管,那么就可以转换为大略的 SSR。
无论 CSR 还是静态渲染,都不得不面对数据获取问题。如果 bundle.js 加载完成,Ajax 再获取的话,全体过程还要增加 50ms 以上的交互韶光。如果预先能够得到数据,肯定是更好的。
类似上图中的数据,放在 Node.js 层去获取,并注入到页面,是做事器端渲染最常用的手段,当然,做事器端远不止这么大略。
做事器端(SSR)
纯做事器渲染实在很大略,便是做事器向浏览器写入 HTML。范例的 CGI 或 ASP、PHP、JSP 这些都算,其核心事理便是模板 + 数据,终极编译为 HTML 并写入到浏览器。
第一种办法是直接将 HTML 写入到浏览器,详细如下。
上图中的 renderToString 是 react SSR 的 API,可以理解成将 React 组件编译成 HTML 字符串,普通点,可以理解 React 便是当模板利用。在做事器向浏览器写入的第一个字节,便是 TTFB 韶光,然后网络传输韶光,然后浏览器渲染,一样平常关注首屏渲染。如果一次将所有 HTML 写入到浏览器,可能会比较大,在编译 React 组件和网络传输韶光上会比较长,渲染韶光也会拉长。
第二种办法是就采取 Bigpipe 进行分块传输,虽然 Bigpipe 是一个相比拟较”古老“的技能,但在实战中还是非常好用的。在 Node.js 里,默认 res.write 就支持分块传输,以是利用 Node.js 做 Bigpipe 是非常得当的,在去哪儿的 PC 业务里就大量利用这种办法。
以上 2 种方法都是做事器渲染,在没有客户端 bundle.js 助力的情形下,第一种情形除了首屏后
复制代码
res.write(\"大众<script>alert('something')</script>\"大众)
渐进混搭法(Progressive Rehydration)
渐进混搭法是将 CSR 和 SSR 一起利用的办法。SSR 卖力接口要乞降首屏渲染,并客户端准备数据或合营完成某些生命周期的操作。
首先,在做事器端天生布局文件,用于首屏渲染,在布局文件里会嵌入 bundle.js。当页面加载 bundle.js 成功后,客户端渲染就开始了。常日客户端渲染过程都会在 domReady 之前,以是优化效果是极其明显的。
Bigpipe 可以利用在分块里写入脚本,在 React SSR 里也可以利用 renderToNodeStream 搞定。React 16 现在支持直接渲染到节点流。渲染到流可以减少你的内容的第一个字节(TTFB)的韶光,在文档的下一部分天生之前,将文档的开头至结尾发送到浏览器。当内容从做事器流式传输时,浏览器将开始解析 HTML 文档。渲染到流的另一个好处是能够相应。 实际上,这意味着如果网络被备份并且不能接管更多的字节,则渲染器会得到旗子暗记并停息渲染,直到堵塞打消。这意味着您的做事器利用更少的内存,并更加适应 I / O 条件,这两者都可以帮助您的做事器处于具有寻衅性的条件。
最大略的示例,你只须要 stream.pipe(res, { end: false })。
复制代码
// 做事器端// using Expressimport { renderToNodeStream } from \"大众react-dom/server\"大众import MyPage from \"大众./MyPage\"大众app.get(\公众/\"大众, (req, res) => { res.write(\"大众<!DOCTYPE HTML><HTML><head><title>My Page</title></head><body>\"大众); res.write(\"大众<div id='content'>\"大众); const stream = renderToNodeStream(<MyPage/>); stream.pipe(res, { end: false }); stream.on('end', () => { res.write(\"大众</div></body></HTML>\"大众); res.end(); });});
当 MyPage 组件的 HTML 片段写到浏览器里,你须要通过 hydrate 进行绑定。
复制代码
// 浏览器端import { hydrate } from \公众react-dom\公众import MyPage from \公众./MyPage\公众hydrate(<MyPage/>, document.getElementById(\"大众content\公众))
至此,你大概能够理解 React SSR 的事理了。做事器编译后的组件更多的是偏于 HTML 模板,而详细事宜和 vdom 操作须要依赖前端 bundle.js 做,即前端 hydrate 时须要做的事儿。
可是,如果有多个组件,须要写入多次流呢?利用 renderToString 就大略很多,普通模板的办法,流却使得这种玩法变得很麻烦。
React SSR 里还有一个新增 API:renderToNodeStream,结合 Stream 也能实现 Bigpipe 一样的效果,而且可以有效的提高 TTFB 韶光。
伪代码
复制代码
const stream1 = renderToNodeStream(<MyPage/>);const stream2 = renderToNodeStream(<MyTab/>); res.write(stream1)res.write(stream2)res.end()
如果每个 React 组件都用 renderToNodeStream 编译,并写入浏览器,那么流的上风就极其明显了,边读边写,都是内存操作,效率非常高。后端写入一个 React 组件,前端就 hydrate 绑定一下,如此循环往来来往,其做法和 Bigpipe 一模一样。
Next.js 同构开拓
Node.js 成熟的标志因此 MEAN 架构开始更换 LAMP。在 MEAN 之后,很多关于同构的探索层出不穷,比如 Meteor,将同构进程的非常彻底,利用 JavaScript 搞定前后端,首创性的提出了 Realtime、Date on the Wire、Database Everywhere、Latency Compensation,零支配等特性,其核心还是环绕 Full Stack Reactivity 做的,这里不展开。简言之,当数据发生改变的时候,所有依赖该数据的地方自动发生相应的改变。本身这些观点是很牛的,参与的开拓者也都很牛,但问题是过于超前了。熟习 Node.js 又熟习前真个人那时候还没那么多,以是前期开拓是非常快的,但一旦碰着问题,调试和解决的本钱高,过程是非常难熬痛苦的。以是至今发布了 Meteor 1.8 也是不温不火的情形。
Next.js 是一个轻量级的 React 运用框架。这里须要强调一下,它不但是 React 做事端渲染框架。它险些覆盖了 CSR 和 SSR 的绝大部分场景。Next.js 自己实现的路由,然后 react-loadable 进行按照路由进行代码分割,整体效果是非常不错的。Next.js 约定组件写法,在 React 组件上,增加静态的 getInitialProps 方法,用于 API 要求处理之用。这样做,相称于将 API 和渲染分开,API 得到的结果作为 props 传给 React 组件,可以说,这种设计确实很赞,可圈可点。
Nextjs 式的一键开启 CSR 和 SSR,比如下面这段代码。
复制代码
import React from 'react'import Link from 'next/link'import 'isomorphic-unfetch' export default class Index extends React.Component { static async getInitialProps () { // eslint-disable-next-line no-undef const res = await fetch('https://api.github.com/repos/zeit/next.js') const json = await res.json() return { stars: json.stargazers_count } } render () { return ( <div> <p>Next.js has {this.props.stars} </p> <Link prefetch href='/preact'> <a>How about preact?</a> </Link> </div> ) }}
在 scr/pages/.js 都是遵守文件名即 path 的做法。内部利用 react-router 封装。在实行过程中
loadGetInitialProps(),得到实行 getInitialProps 静态方法的返回值 props将 props 传给 src/pages/.js 里标准 react 组件的 props优点
静态方法,不用创建工具即可直接实行。利用组建自身的 props 传值,与状态无关,大略方便。SSR 和 CSR 代码是一份,便于掩护Next.js 的做法成为行业最佳实践并不为过,通过大略的用法,可有效的提高首屏渲染,但对付繁芜度较高的情形是很难覆盖的。毕竟页面里用到的 API 不会那么空想,后端支持力度也是有限的,其余前端自己组合 API 并不是每个团队都有这样的能力,那么要解此种情形就只有 2 个选择:1)在 SSR 里实现,2)自建 API 中间层。
自建 API 中间层是最好的办法,但如果未便利,集成在 SSR 里也是可以的。利用 Bigpipe 和 React 做好 SSR 组合,能够完成更强大的能力。限于篇幅,详细实践留在 QCon 环球软件开拓大会(广州站)上分享吧。
3、性能问题用 SSR 最大的问题是场景区分,如果区分不好,还是非常随意马虎有性能问题的。上面 5 种渲染办法里,预渲染里可以利用做事器端路由,此时无任何问题,就当普通的静态托管做事就好,如果在递进一点,你可以把它理解成是 Web 模板渲染。这里重点讲一下混搭法和纯 SSR。
混搭法常日只有大略要求,能玩的事情有限。一样平常是 Node.js 要求接口,然后渲染首屏,在正常情形性能很好的,TTFB 很好,整体 rt 也很短,利用于大略的场景。此时最怕 API 组装,如果是几个 API 组合在一起,然后在返回顾屏,就会导致 rt 很长,性能低落的非常明显。当然,也可以解,你须要加缓存策略,减少不必要的网络要求,将结果放到 Redis 里。其余将一些个性化需求,比如千人千面的推举放到页面中做
如果是纯做事器渲染,那么哀求会更加苛刻,有时 rt 有 10 几秒,乃至更长,此时要担保 QPS 还是有很大难度的。除了合并接口,对接口进行缓存,还能做的便是对页面模块进行分级处理,从布局,核心展示模块,以及其他模块。
除了上面这些业务方法外,剩下的便是 Node.js 自身的性能调优了。比如内存溢出,耗时函数定位等,cpu 采样等,推举利用成熟的 alinode 和 node-clinic。毕竟 Node.js 专项性能调优模块过多,不如直接用这种套装方案。
4、未来Node.js 在大前端布局里意义重大,除了基本构建和 Web 做事外,这里我还想讲 2 点。首先它冲破了原有的前端边界,之前运用开拓只分前端和 API 开拓。但通过引入 Node.js 做 BFF 这样的 API Proxy 中间层,使 API 开拓也成了前真个事情范围,让后端同学专注于开拓 RPC 做事,很明显这样明确的分工是极好的。其次,在前端开拓过程中,有很多问题不依赖做事器端是做不到的,比如场景的性能优化,在利用 React 后,导致 bundle 过大,首屏渲染韶光过长,而且存在 SEO 问题,这时候利用 Node.js 做 SSR 就是非常好的。
当然,前端开拓利用 Node.js 还是存在一些本钱,要理解运维等技能,会略微繁芜一些,不过也有办理方案,比如 Servlerless 就可以降级运维本钱,又能完成前端开拓。直白点讲,在已有 Node.js 拓展的边界内,降级运维本钱,提高开拓的灵巧性,这一定会是一个大趋势。
未来,API Proxy 层和 SSR 都真正的落在 Servlerless,对付前真个演进会更上一层楼。向前是 SSR 渲染,先后是 API 包装,攻防兼备,提效利器,自然是趋势。