使用 Next.js 改造大型电商站点经验分享

在调研使用Next.js开发电商类网站时,读到 Jonne Kats 的这篇文章 ,觉得很有价值,于是略译出来。

Jonne 公司团队 使用 既有技术栈(ASP.NET + React)开发一些新大流量项目 ,发现有性能瓶颈,尝试迁移 Next.js 后成功 改善了性能,并总结了多条 项目经验 ,我以为是很有价值。

Rebuilding A Large E-Commerce Website With Next.js (Case Study)

我们公司为客户开发电商应用 已经 有很多年,这些年也经历了行业技术的演变,从原初 的服务端渲染的传统网站,到 很强前端交互的 JS 单页型应用。

最初我使用是的纯 ASP.NET,后来 前端交互开始增加,我们选用了React。虽然,ASP.NET 混合 React看起来增加了复杂性,但是想到这种混合 能满足用户需求,我们依然觉得很高兴。直到我们遇到 大流量用户。

基于各种原因,我们使用混合技术栈开发的 大流量项目 上线后,Core Web Vitals标示出性能问题。性能对于电商网站成功至关重要,有研究报告发现,网站性能改善一点点收益都很大,如响应速度提高 0.1秒,流量转换高达 10%。

为了缓解性能问题,我们想尽了各种办法,包括添加服务器,设立反向代理来添加页面缓存,这些新部署甚至牺牲了部分 应用功能 。但即使 部署复杂 ,牺牲功能 和 加大成本 ,收效依然不理想。

很显然 我们对这些改造 是不满意的,直到我们发现了Next.js。

Next.js 支持静态页面部署到像 Vercel 或 Netlify 这样的CDN上,访问延迟很低,再加支持 后端渲染,非常适用开发 电商应用注1

新工具挑战

Next.js 很清新,但一定有挑战性注2。第一,社区很新,没有太多的社区经验;第二,即时可见的开发体验大大提高开发效率同时,也加速引入bug的速度。因为当我们只关注代码开发效率时,会忽视了代码的可维护性;第三,随着代码量的增加,JS的无类型 也会损害 代码库的品质,bug的数量 和 代码生产效率成正比。

面对这些挑战,我们积累了以下一些经验 :

  1. Modularize Your Codebase
  2. Lint And Format Your Code
  3. Use TypeScript
  4. Add Automated Tests
  5. Plan For Performance And Measure Performance
  6. Add Performance Checks To Your Quality Gate
  7. Aggressively Manage Your Dependencies
  8. Use A Log Aggregation Service
  9. Next.js’s Rewrite Functionality Enables Incremental Adoption

1 给你的源码库设计一个好的概念模型 |Modularize Your Codebase

项目 目录结构 是需要一定的组织设计的,但是目前还没有一致的最佳实践。Next.js的 项目生成工具(create-next-app)会自动生成一个 直观简易的项目目录,如果你不加设计的直接使用,那么有可能后期会出现 “大泥球” 困难。

/public
  logo.gif
/src
  /lib
    /hooks
      useForm.js
  /api
     content.js
  /components
     Header.js
     Layout.js
  /pages
     Index.js

我们一开始也直接使用这个目录结构,只是在 /components 内为一些 大组件 创建子目录。这种直观设计 对于小型项目 是够用的,但是,随着项目演化,我们发现项目中 组件关系很难记住。我们甚至发现有一些组件 已经无用了!这预示这种目录设计随代码的增长一定会出现 “大泥球” 的现象 ,因为你很难 推理 组件之间的依赖关系 。

大泥球是指一个随意化的杂乱的结构化系统,只是代码的堆砌和拼凑,往往会导致很多错误或者缺陷。

为此,我们重新设计了目录结构,在目录上加了一层 功能模块 (functional modules )的组织。

/src
  /modules
    /catalog
      /components
        productblock.js
    /checkout
      /api
        cartservice.js
      /components
        cart.js

上面的示例中,/modules 目录 分出两个子模块 —— 产品列表(catalog)和 下单流程(checkout)。

这种设计 明显提高了 代码的组织性:从目录名你就知道了代码将实现的功能,和从哪里去修改代码。

这种设计 也更能明了代码之间的依赖关系。产品列表 和 下单流程 有自己的互不依赖的组件,也有通用的组件(common)。在分支设计上 catalog 和 checkout 分别从 common的公共分支分出来。

/src
  /modules
    /common
      /atoms
      /lib 
    /catalog
      /components
        productblock.js
    /checkout
      /api
        cartservice.js
      /components
        cart.js
    /search
  /project
    /layout
      /components
    /templates
      productdetail.js
      cart.js
  /pages
    cart.js

在 模块 之外,我们还设计 一个 并立 的 project 模块 管理 全局的布局模板,和通用页面模板(page templates)。设计大概是这样:

lessons learned ecommerce nextjs 3

在 project 目录里,我们保存一些全局的 layout,和通用的页面模板。在 Next.js设计 里,保存在/pages目录的是页面路由,一个文件对应一个路由项。而我们的经验是,/pages 页面一般都 会动态的参数化组装,所以 /project 有 一些 page templates,用来组装 路由页面。

review:Jonne 这个 目录设计有创新性,但不得不说已经落后了。不但是因为 Next.js 13的新路由,还有就是他的会话结构意识还没有。

2 添加品质自动化审查 | Lint And Format Your Code

我们学到的第二条经验是,多人项目一定要使用 自动化格式工具,像linter。人工约定(所谓代码规范),和人工审查是不够的。

linter 是一种 代码缺陷防范检测工具,同时支持格式化检查,能让你的代码保持一定的品质(包括Web性能指标 Core Web Vitals)。我们使用的是 ESLint + prettier。有了这种自动化格式工具,我们可以卸下代码规范的思想包袱,专心推理,有格式问题让 linter 来提示。

Next.js自 11起内置支持 ESlint,包括内置了 React的规则,和Next.js自己特定的扩展规则,完全开箱即用。

3 添加程序行为强类型约束 |Use TypeScript

我们学到的第三条经验是,体验了 强类型审查(TypeScript)的价值。

我们发现,经过了一段时间,一些有被重构(refactored)过的组件,有 prop已经 没有被用了 ,并且也遇到过一些组件prop类型不匹配的bug。这些都是因为没有使用强类型检查 而导致的。

一开始 ,我们没有觉得项目引入 TS有什么意义 ,甚至觉得增加了多余的抽象 。不过 我们有一个同事对TS比较熟,坚持让我们试一试。幸好,原来 Next.js 开箱即用 TS!并且支持“试用”——可部分代码使用TS,不必一次性全面升级。

让人惊艳的是,当我们开始引入TS后 就立即发现了 组件调用 存在着类型问题。TS另一大利好是,开发测试反馈周期更短,在IDE上就发现问题,不必启动浏览器。我们还发现,TS还有助于代码重构,因为你可以很容易查看 组件间的依赖关系(包括调用 参数信息)。总体来说,TS的好处是:

第一,降低的 bug的数量;第二,代码更易重构;第三,代码更易分析阅读;

4 添加自动化测试和持续集成工作流 | Add Automated Tests

我们学到的第五条经验是,引入自动化测试 ,并且引用持续集成工作流。

如果项目 不引入单元测试(或其他类型测试),随着代码库的演化,你很难保证对某个模块(尤其是通用模块)的重构 不会出现问题。我们经验里,引入 端到端(E2E)测试 可以有效防范这些危险。即使是很小的项目,引入简单的冒烟测试(注:冒烟测试类似端到端测试的快速简版)也是很有必要的。

测试 工具我们选择 Cyress 。此外,我们还引入持续集成工作流,结合Netlify 和 Vercel 创建 一个临时的集成发布环境 ,可以很方便 在其上对每个 推送请求(Pull request) 进行 E2E 测试 。我们使用 Github Action 和这个 脚本

此外,根据项目的类型和性质,我们可能需要添加 组件测试(Enzyme)和 单元测试(JEST),在更细致的粒度上对程序品质进行保卫。

5 认识页面的渲染性能特性并实施策略 | Plan For Performance And Measure Performance

我们学到的第五条经验是,为不同类型页面选择不同的渲染策略改善性能,另外还要对最终打包到的 bundle 进行策略性调优。

Next.js 同时支持 页面的静态(编译时)生成(SSG)和运行时服务端动态渲染(SSR)。编译时静态渲染一定是性能是最优的,但不是所有页面都适用,例如 产品详细页面,库存信息(stock)会变化,不可能编译时能知道的,如果每次库存变更都编译一次 显然 是不现实的。

另外,大规模静态页面的项目 ,SSG 也会损害 编译性能,加载性能(bundle大小),针对这个问题 Next.js 还有一种 增量式渲染( Incremental Static Regeneration ), 编译只生成一个模板,具体的页面在 具体请求时才真正渲染。

有了这些策略工具,当我们设计一个页面时,为了性能选择最佳的渲染方式。尽量选择 SSG,若满足不了 再考虑 ISR,和性能损耗最大的 SSR。

6 在持续集成工流中添加性能自动检测 |Add Performance Checks To Your Quality Gate

第六条经验是,将性能指标的测试(lighthouse) 像E2E测试 那样纳入持续集成的自动化工作流中。

工作中,我们发现即使 Next.js 有很多性能调优策略,像SSG,性能依然不稳定。我们试过好多次,给项目添加新功能后,性能指标的分数(lighthouse score)坠降。为此,我们使用 Github Action 为 lighthouse score 添加 了自动化测试 。我们使用是这个脚本 https://github.com/treosh/lighthouse-ci-action

7 谨慎对待为项目添加的依赖 |Aggressively Manage Your Dependencies

第七条经验是,主动有意识地对 引入新抽象工具(及工具版本)依赖 进行利害判别。

大项目会引入大量的复杂性需要专业(抽象过)的工具(package),在有没必要使用第三方工具,使用哪一个,以及哪一个版本上 都有额外的工作,因为项目增加新依赖是有代价的。我们试过很多次 为项目 添加新依赖,或更新了版本 而产生bug和性能下降。

所以 ,在为项目 安装 新NPM package前,你必须先问自己几个问题:

  • 这个包质量如何 ?
  • 对最终bundle的 大小有什么影响?
  • 这个包真的有必要吗,有没有其他替补方案?
  • 这个包 是否还活跃开发?

理论上 增加依赖一定的会对性能有影响的,所以无必要不增依赖,尽量让编译结果轻盈。

注:这个 VSCODE插件 可以自动检测 引入包的大小。

Next.js 新版跟进

跟上技术趋势,怎么说都是对的。Nextjs 和 React 还在不断演化,新版利好是无疑的。利好包括 新功能,新概念模型 ,和新的安全方面的修正。Next.js官方 提供 了自动化的升级工具 Codemods

更新所有依赖

同样道理,其它工具依赖也应该跟进。Github的 dependabot 帮助我们完成这项任务,它会为每个 依赖更新 产生一个 推送请求(Pull Requests)。更新依赖有利好,但也必须特别注意,因为有可能产生错误(break things),所以必须对更新进行严格测试。

知识:推送请求 和 新版本审查 A pull request is a request to merge changes into a project's code repository It is a way to submit and review changes, also called patches, before integrating them with the main branch.

A pull request notifies the repository maintainer or manager, who is responsible for approving the updates.

8 添加日志监控 | Use A Log Aggregation Service

为确保 应用程序 的功能正常,及尽早发现问题,我们发现 非常有必有在系统中集成一种 日志聚合的服务(log aggregation service)。

Vercel 的控制台有提供 app 运行的即时日志,但这是活动的一次性的,并且不支持自定义编程,既不能保存日志,也不能在别的地方查看到这些日志 。我们需要额外的工具。

我们有过样的的经验,就是有些异常反馈得很慢。例如,我们在某些页面使用 swr (Stale-While-Revalidate )缓存策略后存在bug,这些页面上线后 被 我们发现数据没有被正确的刷新,而当我们查看 Vercel 的日志 后发现这些页面在渲染时报错了。如果我们有 一个 日志聚合服务,那么发现异常会更加迅速。

日志聚合功能 还可以用来 监视 Vercel 的云主机套餐的超额事件。Vercel 的控制面板 可以查看套餐使用情况,但是 日志聚合功能 可让我们 在达到某种阀值时自动收到通知。

Vercel 官方提供了很多即用的 日志聚合 集成工具 ,流行的有:Datadog, Logtail, Logalert, 和Sentry。

知识:stale-while-revalidate是什么

stale-while-revalidate 是HTTP的响应头cache-control的一个扩展,它允许浏览器发现一个过期缓存(stale)时返回这个已经过时的响应缓存,同时背后默默重新发起一次请求(revalidate)。这样隐藏了服务器(或网络)的响应延时。平衡了「新鲜性」和「快速响应体验 」之间的矛盾。

9 按需逐步改造 |Next.js’s Rewrite Functionality Enables Incremental Adoption

最后一条经验,对于一些旧项目,可以使用Next.js的Rewrite 功能 实现逐步的改造。

我们遇到的大多数客户,对站点全面改造 都是抗拒的,他们更感兴趣的是对性能痛点进行针对性的“治疗”。这也是我们所学到的,不必全盘推翻原有的代码,只是针对某些性能关键的页面进行改造。例如,我们曾试过改造产品详细(product detail),和产品分类(category),大大提高了站点性能。

Next.js的Rewrite 功能 可以实现这个目标。

lessons learned ecommerce nextjs 2

小结

要做出一个有商业价值的成品 ,需要多方面的努力,多方面的知识,技能和经验。商业软件品质靠 高价值功能,优越性能和 高效成本控制。

我以为 Jonne团队的这些经验 都是直接或间接围绕提高 开发出有品质的 商业网站的,并不是仅仅是改善性能这一条。

例如:

  • 1 先进的目录结构(会话概念模型)改进开发体验,更好的控制 维护成本 ;
  • 2 品质审查、3 强类型编程 和 4 自动化测试 是保证程序功能和安全和正确性;
  • 5 渲染策略是直接 改善性能,而6 性能检测 和 7 审慎对待新依赖 则 间接的;
  • 8 日志监控 属于 间接的保证 程序功能 的安全和正确性;
  • 9 逐步改造 应该属性于 改进开发体验;

  1. Jonne 没有具体指出 Next.js优于 ASP.net的点,因为都是使用 React做前端,都支持后端渲染。
  2. Next.js 高效率居然还会引入问题?!Jonne 提出这些问题 其实和 Next.js 无关,面对规模项目 ,那是团队智力和经验的水平产生的。
裸男
Nakeman.cn 2023 Build by Gatsby and Tailwind, Deploy on Netlify.