Next.js之前端渲染模式

1,927 阅读15分钟

image.png

最近 Next.js 发了新的大版本,很多东西感觉很有意思,且新增了更符合开发者对代码结构的组织方式,我司也一直用的 Next.js ,但是停留在了 Next.js 10,于是最近在重新梳理了一下 Next.js 技术体系,也是为后续的升级计划做一些准备。

本文主要是分析 Next.js 中的渲染模式,也会结合我们实际开发中用到的渲染模式来进行分析。

在开始分析之前,先问一个问题:为什么要使用 Next.js 呢?

据我所知,一般在最开始我们开始使用它,还是因为它封装了 React 的服务端渲染功能,并形成了一整套技术体系,可以开箱即用,性能也很好,不需要自己再去实现并维护一个React服务端渲染的体系,毕竟这相当耗费时间精力,百分之九九以上的开发人员跟不上框架更新,文档也很难成体系,最后甚至成为了团队技术更新的累赘,再加上现在前端圈子里,封装了 React 服务端渲染功能的框架还比较少,所以它基本上会成为很多团队的首选方案。

以前也没有去深入对 Next.js 进行研究,随着最近不断去了解它,才发现它做的也不仅仅是实现了服务端渲染,还推广出新的渲染模式,以及利用各种方法充分的挖掘 web 的性能和体验。

因为 Next.js 13 版本还不是太成熟,有的功能还没实现,所以本篇文章将主要介绍 Next.js 12 中的渲染模式,当然渲染模式其实 13 并没有多少差别,只是其中的使用方式会更符合 hook 理念。

现代web常见的渲染模式

以前提到页面的渲染模式,我们脑海里一般会浮现 SSR-服务端渲染CSR-客户端渲染,以及 同构渲染,然后在我们印象中,他们的定义是这样的:

  1. SSR

SSR 也就是服务端渲染好 html,然后当浏览器收到 http 响应时,接收到的 html 文件上就有页面上需要显示内容,一般只需要浏览器渲染完成html时,就可以看到页面内容,不需要去等待 js 的下载和执行,一般来说响应时间会比较短,正常网速下,页面显示时间短,用户等待时间少。但是跳转页面时还是SSR,路由切换时间就很长,且页面公共元素不能复用。

  1. CSR

CSR 是当浏览器收到 http 响应时,页面仍然是白屏,需要等待一段时间加载完成关键资源,再去通过 js 代码渲染页面内容,一般来说,就算是正常网速,页面首次显示页面时间也比较长,与关键资源的体积大小有关,一般来说同等网速比 SSR 渲染时间慢个几倍都比较正常。但跳转页面时因为资源早就下载好了,渲染页面就很快,路由切换时间就很短,页面公共元素能复用。

  1. 同构渲染

同构渲染 就是首屏时 SSR 渲染,跳转其他页面时 CSR 渲染,这样既实现了首屏速度快,切换页面体验也很好。

当然,上面只是我之前的理解,并不是什么官方定义。

Next.js 中的渲染模式

从 Next.js 文档上,我们在 数据获取 目录可以看到获取数据的几种方式,其实也是 Next.js 的几种处理方式,根据不同的方式,就形成了最后不同的渲染模式。

主要获取数据的方式有以下几种:

  • getStaticProps
  • getInitialProps
  • getServerSideProps
  • 客户端获取

其实 Next.js 项目的首屏渲染都返回了包含页面内容的 html,其实也算是 SSR,只是不一定需要在服务端进行实时合成 html 内容,切换页面都是 CSR,因此首屏访问都会比较快。

Next.js 首屏渲染模式有以下几种:

  • 页面静态化(SSG):nextjs应用页面会默认静态化,除非使用 getServerSidePropsgetInitialPropsnext export 输出后,getInitialProps 也可以实现静态化,但不要这样使用,会导致页面每次直接访问都是打包的时候的数据。
  • 静态增量再生(ISG):使用 getStaticProps 来获取数据才能实现,且配合其返回的参数 revalidate 来控制,也可以配合 getStaticPaths 或者 接口通知的方式 来实现动态路由的静态增量。
  • 服务端动态渲染(SSR):使用 getServerSidePropsgetInitialProps 进行渲染的模式。

下面我们来探索每个渲染模式的含义。

静态化渲染(SSG)和静态增量再生(ISG)

什么是页面 静态化

打包阶段就生成了包含页面内容 html,如果有 getStaticProps ,则会在打包过程中执行,并把执行结果传递给页面 props 进行渲染页面,然后在接收到页面请求时,直接返回此 html,用户即可看到页面内容。

如果打包结束后,使用了 next export 命令,如果页面存在 getInitialProps,也会上诉所说的进行静态化处理,如果存在 getServerSideProps ,命令则报错,停止生成静态页面。

其实从 Next.js 官方文档中也可以看出来,框架的最重要的核心点之一就是 静态化,那是为什么呢?我们为何要选择 静态化

答案:当然是因为他的性能会更好!服务器只需要把 html 直接发送到浏览器,而不再需要在收到请求的时候去动态生成 html

页面静态化后,页面非主要内容也可以在浏览器端的时候与后端进行交互时获取,我们从 页面内容和用户状态的依赖性来分析性能、体验、是否适合静态化。

用户状态只一切外界依赖条件,比如cookie、请求参数、浏览器属性等

分析之前,我们给性能和体验用一个等级区分:

  • S —— 极好
  • A —— 好
  • B —— 一般
  • C —— 较差
  • D —— 很差

我们还需要先弄清楚页面内容与后端服务的关系,因为页面内容与接口依赖程度的不同,会导致页面的性能和体验会有所不同,页面内容和后端服务接口的依赖性一般可以分成五种情况:

  1. 页面内容无需依赖用户状态
  2. 页面首屏内容无需依赖用户状态
  3. 页面首屏内容低依赖用户状态,也就是用户状态只会影响首屏小部分内容显示,并不影响主要内容
  4. 页面首屏内容高依赖用户状态,也就是用户状态会影响首屏大部分内容显示,页面不能立即显示关键内容
  5. 页面内容完全依赖用户状态,也就是页面内容完全根据用户状态来进行显示

页面静态化的性能体验如何?

序号页面内容和用户状态的依赖性首屏性能体验是否适合静态化
1页面内容无需依赖SS适合
2首屏内容无需依赖SA 页面滚动下滑部分需要依赖时,体验稍差一下,仅仅页面加载完后不能立即滚动查看非首屏内容适合
3首屏内容低依赖SA 只是小部分非主要内容的话,并不会影响用户太多体验适合
4首屏内容高依赖A 影响页面的LCPC 首屏看不到页面主要内容,会让用户感觉页面有延迟的感觉,影响了不适合,更适合服务端渲染
5页面内容完全依赖C 影响页面的FCPD 长时间白屏会让用户觉得页面没有响应不适合,更适合服务端渲染

从上面的分析可以看出:

因为页面是直接访问到已经有内容的 html,所以前 3 种情况的页面,首屏性能体验都会很好,页面的 FP,FCP,LCP 时间基本上一致,当然如果页面有大图片的情况下,可能会影响LCP的加载。后面两种情况更适合服务端渲染。

上面描述了 静态化 的概念,再进行了性能体验的分析,那我们平时的业务是否应该选择静态化呢?

我们可以从一个 web app 内部常用页面来分析如何进行抉择:

  1. 协议类页面:协议一般来说确定后就不会更改,或者更改频率特别低,非常适合使用静态化
  2. 游戏、活动类页面:因为页面大部分元素都并不依赖用户状态,只是少部分变化类数据需要后端参与,完全只需要在客户端进行请求或者,也建议使用静态化
  3. 商品详情类:如果页面展示的大部分核心元素样式和商品本身及用户状态无关的的话,可以形成一个特定骨架,那么仍然可以使用静态化,否则也可考虑使用同构渲染模式
  4. 个人中心及其相关页面:大部分样式都是固定模板的,所以适合使用静态化
  5. app首页
    1. 如果固定了显示模块,商品、品牌等类容都是在客户端再去获取,当然可以使用静态化,加个骨架屏,体验会相当不错,建议使用静态化
    2. 如果是根据一个后端返回的json配置来显示的架子,这个json本身是可以频繁变动的,但如果每次修改后,提前一段时间进行发布,一般也能达成某确定时间的更新,这仍然是可以让首页不断进行更新的,且一般也不需要更新的太过于频繁,考虑使用静态化。
  6. 下单相关页面:页面内容大部分都是与用户的当前订单状态相关的,因此部分页面更适合使用服务端渲染或者同构渲染
  7. 订单详情页面:都依赖用户的状态,且订单状态很多样化,再加上更适合使用 同构渲染
  8. 订单列表/商品列表页面:列表类页面的骨架一般是固定的,一般也不需要进入页面直接显示搜索好的商品,因此也建议使用静态化

从上述分析结论来看,一个完整的电商 web 应用,百分之 90+ 的页面其实都是适合静态化的,并且因为静态化跳转页面时不会阻塞跳转,体验一般来说会更好。

因此,静态化渲染一般可以成为首选方案,如果你不清楚选择哪种模式,可以直接先使用使用它。

如果页面需要有一定动态更新能力,那么可以考虑使用 静态增量再生功能,静态增量再生功能有以下几种方式:

  1. 设置更新时间,如果 第二次访问页面的时间第一次访问时间 < 设定的更新时间,则进行生成静态页面,等待页面生成完成后替换旧的页面,如果在生成完成前或者失败,都访问旧的页面,因此对不需要每次进入页面都必须重新与后端进行交互的页面来说,是一个相当不错的选择,比如博客类页面,就算是排行榜页面,一两分钟改变一次,对业务来说也没啥影响,也能减轻高并发对服务端的压力。
  2. 指令方式,通过api接口去动态下发指令更新。
  3. 动态路由,可以根据首次访问的时候去动态生成页面,可配合前两种方式进行。

服务端动态渲染(SSR)

getServerSidePropsgetInitialProps 都能实现在服务端实时合成页面内容,首屏渲染其实没啥差别,两者的区别主要在于:

  • 输出到客户端的代码有所差别

getServerSideProps 代码不并不会暴露在客户端,而 getInitialProps 会出现,在从应用其他页面跳转进入时,使用 getServerSideProps 的页面是让 nodejs 服务器去 执行 getServerSideProps 方法,而使用 getInitialProps 的页面是在客户端直接调用 getInitialProps,不过两者都是调用完成后才会跳转到对于的页面。

  • 是否支持静态化

getServerSideProps 不支持静态化,getInitialPropsexport 模式下支持静态化,但也有严重bug。

Next.js 渲染模式之间的性能对比

getServerSidePropsgetInitialProps 服务端渲染的性能体验其实是比静态化差的,下面我们分析一下 getStaticProps / getServerSideProps / getInitialProps 的优劣:

  • getStaticProps:首屏渲染直接返回构建好的 html ,不需要服务端做其他操作;跳转页面时只用加载 client 目录对应页面的js资源;nodejs 服务器只需要转发资源即可,压力很低。
  • getServerSideProps:首屏渲染时需要执行 getServerSideProps 函数,并且需要调用 renderToString 来生成 html ; 跳转页面时,需要 提前nodejs 服务器发起请求,等待服务器执行 getServerSideProps 后的返回结果后,然后再在客户端生成 html 内容,再进行切换页面内容。安全性上可以隐藏 getServerSideProps 的执行过程。每次进入页面时,nodejs 服务器都需要处理一些事项,压力会比较高。
  • getInitialProps:首屏渲染时需要执行 getInitialProps 函数,并且需要调用 renderToString 来生成 html ; 跳转页面时,需要 提前 调用 getInitialProps 函数,等待 getInitialProps 执行后的返回结果,然后再在客户端生成 html 内容,再进行切换页面内容。首次进入页面时,nodejs 服务器需要处理一些事项,也会造成一定的压力。

从上面我们分析得出一个对比表格:

序号指标getStaticPropsgetServerSidePropsgetInitialProps
1首页性能SAA
2跳转性能SBB
3安全性BAC
4nodejs服务器压力中高
6Next.js官方推荐超级推荐推荐不推荐

Next.js 构建分析

按照官方的说明:

next build 生成用于生产版本的应用程序。它包含以下内容:

  • 默认(不使用前面讲的三个函数)或者使用 getStaticProps 的页面的 HTML 文件。
  • 用于全局样式或单独作用域样式的 CSS 文件
  • 用于 Next.js 在服务器渲染动态内容的 JavaScript
  • 通过 React 在客户端进行交互的 JavaScript

它们都输出在 .next 文件夹内:

  • .next/static/chunks/pages – 此文件夹中的每个 JavaScript 文件都与同名的路由相关。例如,路由 /about 会有对应的 .next/static/chunks/pages/about.js 文件。
  • .next/static/media – 静态导入的图像 next/image 在此处进行哈希和复制
  • .next/static/css - 应用程序中所有页面的全局 CSS 文件
  • .next/server/pages – 从服务器预呈现的 HTML 和 JavaScript 入口点。每个页面对应的 .nft.json 文件是在启用输出文件跟踪时创建的,并且包含依赖于给定页面的所有文件路径。
  • .next/server/chunks – 在整个应用程序的多个地方使用的共享 JavaScript 块
  • .next/cache – 是 Next.js 服务器的构建缓存和缓存图像、响应和页面的输出。使用缓存有助于减少构建时间并提高加载图像的性能。

.next 目录的所有 JavaScript 代码都被编译过,浏览器包进行了体积优化压缩,以便于可以实现最佳性能并支持所有现代浏览器

从上面描述可以得出:

  • 如果不使用 getServerSidePropsgetInitialProps,都会生成对于的 html
  • 只使用 next build 构建的产物,一般只能使用 next start 来启动应用程序

但如果代码中没有使用到 getServerSideProps,且又不想使用 next start 进行启动服务,那么可以 使用 next export 来进行静态导出,也就是可以使用其他 web 服务来启动,比如 nginx 。

那么执行了 next export,对应用本身会有哪些影响呢?

  1. 含有 getInitialProps 的页面,会执行 getInitialProps 后生成 html 文件,首次访问将都是build 构建时候的产物,也就是通过刷新页面,页面内容如果不在客户端渲染后改变页面内容,页面永远都是构建时的内容,将会失去 getInitialProps 的意义,但又会在应用内部切换页面请求执行 getInitialProps ,以至于页面会进行更新,但一刷新就还原到页面的初始状态。这种明显会是一种bug,因此不建议有getInitialProps页面的时候去 export
  2. 执行 next export 意味着并没有 Next.js 服务端程序去维护页面,也就是将会失去 静态增量再生(ISG) 功能。

所以建议不要去 export,除非你的应用没有使用 getServerSidePropsgetInitialProps,且不需要 静态增量再生(ISG) 功能。

总结

  1. 常见的渲染模式有两种:SSRCSR
  2. 一般前端应用需要服务端渲染时,一般会选择 同构渲染
  3. Next.js 中主要新增了一种渲染模式,也是其最推崇的渲染模式:页面静态化
  4. Next.js 针对服务端动态渲染提供了两种模式,一种类似常规的 SSR,另一种对其安全性进行了提升,并减少了客户端的代码。

本篇文章没有去细讲 静态增量再生,也没有去讲 动态路由静态化 的结合,大有兴趣大家可以点击去官方文档看一下,如果理解了 Next.js 的渲染模式,再去理解这些,应该会很容易。

最近还看了很多关于页面渲染流程的资料,再集合 Next.js 的输出产物,进行了一些分析,预计最近也会整理出一篇关于 Next.js 是怎么优化页面渲染流程的文章。

有问题欢迎大家提出,感谢!

参考资源: