React在中后台业务里已经很好落地了,但对于C端(给用户使用的端,比如PC/H5)业务有其特殊性,对性能要求比较苛刻,且有SEO需求。另外团队层面也希望能够统一技术栈,小伙伴们希望成长,那么如何能够完成既要、也要、还要呢?

\n

本次分享主要围绕C端业务下得React SSR实践,会讲解各种SSR方案,包括Next.js同构开发,并以一次优化的过程作为实例进行讲解。其实这些铺垫都是在工作中做的Web框架的设计而衍生出来的总结。这里先卖个关子,自研框架基于Umi框架并支持SSR的相关内容留到广州QCon上讲,感兴趣的同学可以来5月的QCon全球软件开发大会广州站聊。下面开始正题。

\n

曾和小弟讨论什么是SSR?他开始以为React SSR就是SSR,这是不完全对的,忽略了Server-side Render的本质。其实从早期的cgi,到PHP、ASP,jsp等server page,这些动态网页技术都是服务器端渲染的。而React SSR更多是强调基于React技术栈进行的服务器端渲染,是服务器端渲染的分类之一,本文会以React SSR为主进行讲解。

\n

1、为什么要上SSR?

\n

对于SSR,大家的认知大概是以下3个方面。

\n

•    SEO:强需求,被搜索引擎收录是网站的基本能力。
\n  •    C端性能:至少要保证首屏渲染效率,如果秒开率都无法保证,那么用户体验是极差的。
\n  •    统一技术栈:目前团队以React为主,无论从团队成长,还是个人成长角度,统一技术栈的好处都是非常明显的。

\n

诚然,以上都是大家想用SSR的原因,但对笔者来说,SSR的意义远不止如此。在技术架构升级的过程中,如果能够同时带给团队和小伙伴成长,才是两全其美的选择。目前我负责优酷PC/H5业务,在优酷落地Node.js,目前在做React SSR相关整合工作。玉伯曾讲过在All in Mobile的时代的尴尬——对于多端来说是毁灭性的灾难。\u0008押宝移动端在当时是正确的选择,但在今天获客成本过高,且移动端增速不足,最好的选择就是多端在产品细节上做
\nPK,PC/H5业务的生机也正在于此。

\n

然而历史包袱如此的重,有几方面原因。1)页面年久失修;2)移动端在All in Mobile时代并没有给多端提供技术支持,PC/H5是掉队的,需要补齐App端的基本能力;3)技术栈老旧,很多页面还是采用jQuery开发的,对于团队来说,这才是最痛苦的事儿。

\n

其实所有公司都是类似的,都是用有限资源做事,希望最少的投入带来最大化的产出。可以说,通过整合SSR一举三得,将Node.js和React一同落地,顺便将基础框架也落地升级,这样的投入产出是比较高的。

\n

2、从CSR到SSR演进之路

\n

SSR看起来很简单,如果细分一下,还是略微负责的,下面和我一起看一下从CSR到SSR演进之路。

\n

客户端渲染 (CSR)

\n

客户端渲染是目前最简单的开发方式,以React为例,CSR里所有逻辑,数据获取、模板编译、路由等都是在浏览器做的。

\n

Webpack在工程化与构建方便提供了足够多便利,除了提供Loader和Plugin机制外,还将所有构建相关步骤都进行了封装,甚至连模块按需加载都内置,还具备Tree-shaking等能力,外加Node cluster利用多核并行构建。很明显这是非常方便的,对前端意义重大的。开发者只需要关注业务模块即可。

\n

\n

常见做法是本地通过Webpack打包出bundle.js,嵌入到简单的HTML模板里,然后将HTML和bundle.js都发布到CDN上。这样开发方式是目前最常见的,对于做一些内部使用的管理系统是够的。

\n

\n

CSR缺点也是非常明显的,首屏性能无法保障,毕竟React全家桶基础库就很大,外加业务模块,纵使按需加载,依然很难保证秒开的。

\n

为了优化CSR性能,业界有很多最佳实践。在2018年,笔者以为React最成功的项目是CRA(create-react-app),支付宝开发的Umi其实也是类似的。他们通过内置Webpack和常见Webpack中间件,解决了Webpack过于分散的问题。通过约定目录,统一开发者的开发习惯。

\n

与此同时,也产生了很多与时俱进的最佳实践。使用react-router结合react-loadable,更优雅的做dynamic import。在页面中切换路由时按需加载,在Webpack中做过代码分割,这是极好的实践。

\n

\n

以前是打包bundle是非常大的,现在以路由为切分标准,按需加载,效率自然是高的。

\n

\n

Umi基于react-router又进步增强了,约定页面有布局的概念。

\n

export default {\n  routes: [\n    { path: '/', component: './a' },\n    { path: '/list', component: './b', Routes: ['./routes/PrivateRoute.js'] },\n    { path: '/users', component: './users/_layout',\n      routes: [\n        { path: '/users/detail', component: './users/detail' },\n        { path: '/users/:id', component: './users/id' }\n      ]\n    },\n  ],\n};\n

\n

这样做的好处,就有点模板引擎中include类似的效果。布局提效也是极其明显的。为了演示优化后的效果,这里以Umi为例。它首先会加载index页面,找到index布局,先加载布局,然后再加载index页面里的组件。下图加了断点,你可以很清楚的看出加载过程。

\n

\n

在 create-react-app(cra)和Umi类似,都是通过约定,隐藏具体实现细节,让开发者不需要关注构建。在未来,类似的封装还会有更多的封装,偏于应用层面。笔者以为前端开发成本在降低,未来有可能规模化的,因为框架一旦稳定,就有大量培训跟进,导致规模化开发。这是把双刃剑,能满足企业开发和招人的问题,但也在创新探索领域上了枷锁。

\n

预渲染(Prerending)

\n

SPA(单页面应用)的主要内容都依赖于JavaScript(bundle.js)的执行,当首页HTML下载下来的时候,并不是完整的页面,而是浏览器里加载HTML并JavaScript文件才能完成渲染。用户在访问的时候体验会很好,但是对于搜索引擎是不好收录的,因为它们不能执行JavaScript,这种场景下预渲染(Prerending)就派上用场了,它可以帮忙把页面渲染完成之后再返回给爬虫工具,我们的页面也就能被解析到了。

\n

CSR是由bundle.js来控制渲染的,所以它外层的HTML都很薄。对于首屏渲染来说,如果能够先展示一部分布局内容,然后在走CSR的其他加载,效果会更好。另外业内有太多类似的事件了,比如用less写css,coffee写js,用markdown写博客,都是需要编译一次才能使用的。比如Jekyll/Hexo等著名项目,它们都非常好用。那么基于React技术,也必然会做预处理的,Gatsby/Next.js都有类似的功能。将React组建编译成HTML,可以编译全部,也可以只编译布局,对于页面性能来说,预渲染是非常简单的提升手段。其原理JSX模板和Webpack stats结合,进行预编译。

\n

•    编译全部:纯静态页面。
\n  •    只编译布局:对于SPA类项目是非常好,当然多页应用也可以只编译布局的。

\n

生成纯HTML,可以直接放到CDN上,这是简单的静态渲染。如果不直接生成HTML,由Node.js来接管,那么就可以转换为简单的SSR。

\n

无论CSR还是静态渲染,都不得不面对数据获取问题。如果bundle.js加载完成,Ajax再获取的话,整个过程还要增加50ms以上的交互时间。如果预先能够得到数据,肯定是更好的。

\n

\n

类似上图中的数据,放在Node.js层去获取,并注入到页面,是服务器端渲染最常用的手段,当然,服务器端远不止这么简单。

\n

服务器端(SSR)

\n

纯服务器渲染其实很简单,就是服务器向浏览器写入HTML。典型的CGI或ASP、PHP、JSP这些都算,其核心原理就是模板+数据,最终编译为HTML并写入到浏览器。

\n

第一种方式是直接将HTML写入到浏览器,具体如下。

\n

\n

上图中的renderToString是react SSR的API,可以理解成将React组件编译成HTML字符串,通俗点,可以理解React就是当模板使用。在服务器向浏览器写入的第一个字节,就是TTFB时间,然后网络传输时间,然后浏览器渲染,一般关注首屏渲染。如果一次将所有HTML写入到浏览器,可能会比较大,在编译React组件和网络传输时间上会比较长,渲染时间也会拉长。

\n

第二种方式是就采用Bigpipe进行分块传输,虽然Bigpipe是一个相对比较”古老“的技术,但在实战中还是非常好用的。在Node.js里,默认res.write就支持分块传输,所以使用Node.js做Bigpipe是非常合适的,在去哪儿的PC业务里就大量使用这种方式。

\n

以上2种方法都是服务器渲染,在没有客户端bundle.js助力的情况下,第一种情况除了首屏后懒加载外,客户端能做的事儿不多。第二种情况下,还是有手段可以用的,比如在分块里写入脚本,可以做的的事情还是很多的。

\n

    res.write(\u0026quot;\u0026lt;script\u0026gt;alert('something')\u0026lt;/script\u0026gt;\u0026quot;)\n

\n

渐进混搭法(Progressive Rehydration)

\n

渐进混搭法是将CSR和SSR一起使用的方式。SSR负责接口请求和首屏渲染,并客户端准备数据或配合完成某些生命周期的操作。

\n

\n

首先,在服务器端生成布局文件,用于首屏渲染,在布局文件里会嵌入bundle.js。当页面加载bundle.js成功后,客户端渲染就开始了。通常客户端渲染过程都会在domReady之前,所以优化效果是极其明显的。

\n

Bigpipe可以使用在分块里写入脚本,在React SSR里也可以使用renderToNodeStream搞定。React 16现在支持直接渲染到节点流。渲染到流可以减少你的内容的第一个字节(TTFB)的时间,在文档的下一部分生成之前,将文档的开头至结尾发送到浏览器。当内容从服务器流式传输时,浏览器将开始解析HTML文档。渲染到流的另一个好处是能够响应。 实际上,这意味着如果网络被备份并且不能接受更多的字节,则渲染器会获得信号并暂停渲染,直到堵塞清除。这意味着您的服务器使用更少的内存,并更加适应I / O条件,这两者都可以帮助您的服务器处于具有挑战性的条件。

\n

最简单的示例,你只需要stream.pipe(res, { end: false })。

\n

// 服务器端\n// using Express\nimport { renderToNodeStream } from \u0026quot;react-dom/server\u0026quot;\nimport MyPage from \u0026quot;./MyPage\u0026quot;\napp.get(\u0026quot;/\u0026quot;, (req, res) =\u0026gt; {\n  res.write(\u0026quot;\u0026lt;!DOCTYPE HTML\u0026gt;\u0026lt;HTML\u0026gt;\u0026lt;head\u0026gt;\u0026lt;title\u0026gt;My Page\u0026lt;/title\u0026gt;\u0026lt;/head\u0026gt;\u0026lt;body\u0026gt;\u0026quot;);\n  res.write(\u0026quot;\u0026lt;div id='content'\u0026gt;\u0026quot;); \n  const stream = renderToNodeStream(\u0026lt;MyPage/\u0026gt;);\n  stream.pipe(res, { end: false });\n  stream.on('end', () =\u0026gt; {\n    res.write(\u0026quot;\u0026lt;/div\u0026gt;\u0026lt;/body\u0026gt;\u0026lt;/HTML\u0026gt;\u0026quot;);\n    res.end();\n  });\n});\n

\n

当MyPage组件的HTML片段写到浏览器里,你需要通过hydrate进行绑定。

\n

// 浏览器端\nimport { hydrate } from \u0026quot;react-dom\u0026quot;\nimport MyPage from \u0026quot;./MyPage\u0026quot;\nhydrate(\u0026lt;MyPage/\u0026gt;, document.getElementById(\u0026quot;content\u0026quot;))\n

\n

至此,你大概能够了解React SSR的原理了。服务器编译后的组件更多的是偏于HTML模板,而具体事件和vdom操作需要依赖前端bundle.js做,即前端hydrate时需要做的事儿。
\n可是,如果有多个组件,需要写入多次流呢?使用renderToString就简单很多,普通模板的方式,流却使得这种玩法变得很麻烦。

\n

React SSR里还有一个新增API:renderToNodeStream,结合Stream也能实现Bigpipe一样的效果,而且可以有效的提高TTFB时间。

\n

伪代码

\n

const stream1 = renderToNodeStream(\u0026lt;MyPage/\u0026gt;);\nconst stream2 = renderToNodeStream(\u0026lt;MyTab/\u0026gt;);\n\nres.write(stream1)\nres.write(stream2)\nres.end()\n

\n

\n

如果每个React组件都用renderToNodeStream编译,并写入浏览器,那么流的优势就极其明显了,边读边写,都是内存操作,效率非常高。后端写入一个React组件,前端就hydrate绑定一下,如此循环往复,其做法和Bigpipe如出一辙。

\n

Next.js同构开发

\n

Node.js成熟的标志是以MEAN架构开始替换LAMP。在MEAN之后,很多关于同构的探索层出不穷,比如Meteor,将同构进程的非常彻底,使用JavaScript搞定前后端,开创性的提出了Realtime、Date on the Wire、Database Everywhere、Latency Compensation,零部署等特性,其核心还是围绕Full Stack Reactivity做的,这里不展开。简言之,当数据发生改变的时候,所有依赖该数据的地方自动发生相应的改变。本身这些概念是很牛的,参与的开发者也都很牛,但问题是过于超前了。熟悉Node.js又熟悉前端的人那时候还没那么多,所以前期开发是非常快的,但一旦遇到问题,调试和解决的成本高,过程是非常难受的。所以至今发布了Meteor 1.8也是不温不火的情况。

\n

Next.js是一个轻量级的React应用框架。这里需要强调一下,它不只是React服务端渲染框架。它几乎覆盖了CSR和SSR的绝大部分场景。Next.js自己实现的路由,然后react-loadable进行按照路由进行代码分割,整体效果是非常不错的。Next.js约定组件写法,在React组件上,增加静态的getInitialProps方法,用于API请求处理之用。这样做,相当于将API和渲染分开,API获得的结果作为props传给React组件,可以说,这种设计确实很赞,可圈可点。

\n

Nextjs式的一键开启CSR和SSR,比如下面这段代码。

\n

import React from 'react'\nimport Link from 'next/link'\nimport 'isomorphic-unfetch'\n\nexport default class Index extends React.Component {\n  static async getInitialProps () {\n    // eslint-disable-next-line no-undef\n    const res = await fetch('https://api.github.com/repos/zeit/next.js')\n    const json = await res.json()\n    return { stars: json.stargazers_count }\n  }\n\n  render () {\n    return (\n\n      \u0026lt;div\u0026gt;\n        \u0026lt;p\u0026gt;Next.js has {this.props.stars} \u0026lt;/p\u0026gt;\n        \u0026lt;Link prefetch href='/preact'\u0026gt;\n          \u0026lt;a\u0026gt;How about preact?\u0026lt;/a\u0026gt;\n        \u0026lt;/Link\u0026gt;\n      \u0026lt;/div\u0026gt;\n\n    )\n  }\n}\n\n\n

\n

在scr/pages/*.js都是遵守文件名即path的做法。内部使用react-router封装。在执行过程中

\n

  • \n
  • loadGetInitialProps(),获得执行getInitialProps静态方法的返回值props\n
  • 将props传给src/pages/*.js里标准react组件的props\n

\n

优点

\n

  1. \n
  2. 静态方法,不用创建对象即可直接执行。\n
  3. 利用组建自身的props传值,与状态无关,简单方便。\n
  4. SSR和CSR代码是一份,便于维护\n

\n

Next.js的做法成为行业最佳实践并不为过,通过简单的用法,可有效的提高首屏渲染,但对于复杂度较高的情况是很难覆盖的。毕竟页面里用到的API不会那么理想,后端支持力度也是有限的,另外前端自己组合API并不是每个团队都有这样的能力,那么要解此种情况就只有2个选择:1)在SSR里实现,2)自建API中间层。

\n

自建API中间层是最好的方式,但如果不方便,集成在SSR里也是可以的。利用Bigpipe和React做好SSR组合,能够完成更强大的能力。限于篇幅,具体实践留在QCon全球软件开发大会(广州站)上分享吧。

\n

3、性能问题

\n

用SSR最大的问题是场景区分,如果区分不好,还是非常容易有性能问题的。上面5种渲染方式里,预渲染里可以使用服务器端路由,此时无任何问题,就当普通的静态托管服务就好,如果在递进一点,你可以把它理解成是Web模板渲染。这里重点讲一下混搭法和纯SSR。

\n

混搭法通常只有简单请求,能玩的事情有限。一般是Node.js请求接口,然后渲染首屏,在正常情况性能很好的,TTFB很好,整体rt也很短,使用于简单的场景。此时最怕API组装,如果是几个API组合在一起,然后在返回首屏,就会导致rt很长,性能下降的非常明显。当然,也可以解,你需要加缓存策略,减少不必要的网络请求,将结果放到Redis里。另外将一些个性化需求,比如千人千面的推荐放到页面中做懒加载。

\n

如果是纯服务器渲染,那么要求会更加苛刻,有时rt有10几秒,甚至更长,此时要保证QPS还是有很大难度的。除了合并接口,对接口进行缓存,还能做的就是对页面模块进行分级处理,从布局,核心展示模块,以及其他模块。

\n

除了上面这些业务方法外,剩下的就是Node.js自身的性能调优了。比如内存溢出,耗时函数定位等,cpu采样等,推荐使用成熟的alinode和node-clinic。毕竟Node.js专项性能调优模块过多,不如直接用这种套装方案。

\n

4、未来

\n

Node.js在大前端布局里意义重大,除了基本构建和Web服务外,这里我还想讲2点。首先它打破了原有的前端边界,之前应用开发只分前端和API开发。但通过引入Node.js做BFF这样的API Proxy中间层,使API开发也成了前端的工作范围,让后端同学专注于开发RPC服务,很明显这样明确的分工是极好的。其次,在前端开发过程中,有很多问题不依赖服务器端是做不到的,比如场景的性能优化,在使用React后,导致bundle过大,首屏渲染时间过长,而且存在SEO问题,这时候使用Node.js做SSR就是非常好的。

\n

当然,前端开发使用Node.js还是存在一些成本,要了解运维等技能,会略微复杂一些,不过也有解决方案,比如Servlerless就可以降级运维成本,又能完成前端开发。直白点讲,在已有Node.js拓展的边界内,降级运维成本,提高开发的灵活性,这一定会是一个大趋势。

\n

未来,API Proxy层和SSR都真正的落在Servlerless,对于前端的演进会更上一层楼。向前是SSR渲染,先后是API包装,攻防兼备,提效利器,自然是趋势。

\n

作者简介

\n

狼叔(网名i5ting),现为阿里巴巴前端技术专家,Node.js 技术布道者,Node全栈公众号运营者,曾就职于去哪儿、新浪、网秦,做过前端、后端、数据分析,是一名全栈技术的实践者。目前负责BU的Node.js和基础框架开发,即将出版Node.js《狼书》3卷。同时,狼叔将作为QCon全球软件开发大会(广州站)的讲师,分享「C端服务端渲染(SSR)和性能优化实践」,感兴趣的同学可以关注下。

\n

大前端时代,如何做好C 端业务下的React SSR?\n相关推荐

  1. 大前端时代的挑战与机遇(深圳场)正式开放报名

    2017年,以饿了么为代表的一些企业开始提出大前端的概念.2018年,InfoQ 举办了首届全球大前端技术大会,在大会中将前后端分离.跨平台和 PWA 等技术设立了专场,这次大会具有重要的意义,它预示 ...

  2. GMTC 大前端时代前端监控的最佳实践

    摘要: 今天我分享的内容分成三个部分: 第一部分是"大前端时代前端监控新的变化", 讲述这些年来,前端监控一些新的视角以及最前沿的一些思考. 第二部分"前端监控的最佳实践 ...

  3. GMTC 大前端时代前端监控的最佳实践 1

    摘要: 今天我分享的内容分成三个部分: 第一部分是"大前端时代前端监控新的变化", 讲述这些年来,前端监控一些新的视角以及最前沿的一些思考. 第二部分"前端监控的最佳实践 ...

  4. 大前端时代,从前端小工到架构师的进阶锦囊!

    随着大前端的演进与火爆,前端工程师的责任越来越大,未来对前端岗位的要求也越来越高. "大前端时代" 来临带来了什么样的变革和机遇? 由于前端的责任越来越大,未来对前端开发人才的要求 ...

  5. 大前端时代的乱流:带你了解最全面的 Flutter Web

    Flutter Web 稳定版本发布至今也有一年多了,经过这一年多的发展,今天就让我们来看看作为大前端时代的乱流,Flutter Web 究竟有什么不同之处,本篇分享主要内容是目前 Flutter 下 ...

  6. 02.Web大前端时代之:HTML5+CSS3入门系列~H5结构元素

    Web大前端时代之:HTML5+CSS3入门系列:http://www.cnblogs.com/dunitian/p/5121725.html 1.结构元素 可以理解为语义话标记,比如:以前这么写&l ...

  7. 04. Web大前端时代之:HTML5+CSS3入门系列~HTML5 表单

    Web大前端时代之:HTML5+CSS3入门系列:http://www.cnblogs.com/dunitian/p/5121725.html 一.input新增类型: 1.tel:输入类型用于应该包 ...

  8. 大前端时代搞定PC/Mac端开发,我有绝招

    如果你是一位前端开发工程师,对"跨平台"一词应该不会感到陌生.像常见的前端框架:比如React.Vue.Angular,它们可以做网页端,也可以做移动端,但很少能做到跨PC.Mac ...

  9. 大前端时代安全性如何做

    目录 背景 爬虫手段 解决方案 制定出Web 端反爬技术方案 关键步骤 App 端安全的解决方案 之前在上家公司的时候做过一些爬虫的工作,也帮助爬虫工程师解决过一些问题.然后我写过一些文章发布到网上, ...

最新文章

  1. java中next的用法_关于java iterator的next()方法的用法
  2. html点击按钮删除session,Asp.net中安全退出时清空Session或Cookie的实例代码
  3. 返回指定大小的数组_python中数组和矩阵的基础以及应用
  4. PHP 项目中单独使用 Laravel Eloquent 查询语句来避免 SQL 注入
  5. 自定义构建基于.net core 的基础镜像
  6. 1024到了,默默给自己点个赞!
  7. 【转载】飞鸽传书2013官方下载
  8. python库下载安装报错_Python 各种库的安装
  9. 更改TFS项目中的SharePoint网站端口
  10. linux使用中的问题 --- (防火墙iptables -F)
  11. Linux 系统启动与服务管理
  12. 这应该是史上最强的物理学科普(雄文)
  13. PAT甲级 1125
  14. Python绘制箱形图全解
  15. STM32---ADC模数转换详解
  16. Windows常用操作—热键(快捷键)
  17. python 标准输入设备,实时获取MIDI设备的输入(Python)
  18. 关于百度站长工具中站点属性LOGO提交申请详解说明
  19. 色彩大全 Android 颜色值
  20. Python爬虫之验证码处理

热门文章

  1. 部署企业中第一个站点
  2. 怎样消除公司的信息孤岛
  3. 算法题复健之路 第四天 第五天 第六天
  4. css实现简单的头像遮罩效果
  5. 寻觅反思,追求卓越——毕业工作所感
  6. 使用私服管理jar时,下载jar出现 lastUpdated问题 maven
  7. Ubuntu同时使用中英文man手册
  8. 什么是RPC?什么是Restful ?它们有什么区别?
  9. ipad 导入电脑中文件
  10. Python中 ‘int‘ object is not subscriptable 问题的可能解决方法