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 为主进行讲解。

jsp加载ttfb耗时年夜前端时期若何做好 C 端营业下的 React SSR Docker

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 包装,攻防兼备,提效利器,自然是趋势。