静态网站生成器

Next.js原本是一个SSR框架, 后来开始支持静态生成, 并逐渐将静态生成发展成了它最主要的特性.
默认配置足够好, 技术选择自由度大, 项目结构足够干净.
可以用来混合静态生成和SSR网站.

  • 由于Next.js页面路由, 很难重构出合理的文件结构.
  • Page文件的代码里同时包含前端渲染和后端数据获取两项职责, 对代码可读性有害.
  • getServerSideProps 里暴露了http模块的细节, 导致SSR的功能与API服务器的功能重合.
  • API服务器的路由设计延续了页面路由的设计, 导致路由失去灵活性.
  • next dev 会重写 tsconfig.json, 对配置TypeScript构成障碍:
    • https://github.com/vercel/next.js/issues/8128
  • build时, 即使是服务器部分的代码也会被打包,
    因此无法在Next.js项目内使用Node.js的Worker之类的依赖代码文件名的模块.
  • /public 目录中的静态文件缓存时间为0, 由于这些静态文件散落在不同的路由上, 最终只能通过匹配扩展名来手动设置缓存.

Next.js对公开环境变量的支持是基于替换的, 只有硬编码才能替换成功.
并且对NEXT_PUBLIC开头的环境变量的替换是在构建期间完成的, 这意味着相关的环境变量不能被用作配置项.
https://github.com/vercel/next.js/discussions/17641

建议用React Context替代 NEXT_PUBLIC 开头的环境变量:
将需要公开的环境变量通过各种getProps函数发送给相关的页面, 然后在Page组件里设置相关的React Context.
遗憾的是, 不能在 _app_document 里注册Context,
因为它们没有自己的getProps函数, 这使得每个页面都需要手动完成一次注册.

当项目的依赖项里有dotenv时, 它不会自动加载以NEXT_PUBLIC开头的环境变量.
https://github.com/vercel/next.js/discussions/12754

Next.js有两种缓存设置方法:

  • 对于SSR页面, 可以直接在页面里调用 ctx.res.setHeader 设置 Cache-Control.
  • 对于其他情况, 可以在 next.config.js 里配置 headers().

不推荐在SSR页面上使用 next.config.js 设置缓存, 推荐直接调用 ctx.res.setHeader 的原因:

  • ctx.res.setHeader 可以一并设置页面位于 /_next/ 下的 .json 文件.
  • next.config.js 只是JavaScript, 不能导入其他格式的脚本, 例如TypeScript文件.
  • next.config.js 只能根据URL进行匹配, 不能根据SSR的结果为不同的分支设置不同的缓存.

Next.js的身份验证有多个解决方案.

专为Next.js设计的身份验证解决方案.
由于该项目的抽象级别特别高, 并且有大量用户留在v3版本, 是否值得使用还有待时间检验.

next-connect是Next.js版本的connect, 可以与passport一同使用以构建具有身份验证功能的API端点.
会话对passport来说是可选的.

基于无状态的身份验证.
类似于JWT, 用户身份数据直接存储在cookie里, 不保存在后端.

Next.js 12新增的特性, 通过ServiceWorker的Request和Response类实现了类似Express的中间件,
中间件会根据URL的层级结构由浅入深逐级执行.

该特性允许用户在单个文件里为特定路由下的页面设定统一的行为.

Next.js最基础的功能之一, 静态生成.

SSG需要页面提供 getStaticPropsgetStaticPaths.

用户在浏览其他页面时, Next.js会自动预取(prefetch)通过Link组件指向的SSG页面的props.
预取让SSG与SSR在性能上产生了决定性的差异:
SSR的导航速度永远不可能比预取的SSG页面更快, 即使在SSR有缓存的情况下也一样.

适合:

  • 更新频率很低的静态站点.
  • 上线后就不会更新的静态页面.
  • 完全静态的网站: 使用next export导出为静态HTML

不适合:

  • 动态页面.

自v9.5加入的功能.

ISR可以在不重新build整个Next.js项目的情况下更新网站.
要使用ISR, 需要在SSG的基础上, 在 getStaticProps 方法的返回值里加上 revalidate 属性.

在ISR下, 访问页面时:

  • 如果缓存不存在, 则根据 getStaticPaths 方法返回的 fallback 属性选择行为.
    一般情况下, 推荐使用 fallback: blocking (在运行时按需构建页面, 并且阻塞用户请求, 对SEO有利).
    如果页面生成很慢, 则建议使用 fallback: true 以显示"加载中", 防止用户陷入长时间的等待.
  • 如果缓存存在, 且缓存时间小于revalidate时, 返回缓存的页面.
  • 如果缓存存在, 且缓存时间第一次大于revalidate, 返回缓存的页面, 同时在后台重新生成页面.
    如果需要删除页面, 则 getStaticProps 方法需要返回 notFound: true,
    为了让页面可以尽快被删除, 必须将 revalidate 设置为一个较小的值.

当前, ISR的页面删除行为存在一些bug:

  • https://github.com/vercel/next.js/issues/21453
  • https://github.com/vercel/next.js/issues/25470
  • https://github.com/vercel/next.js/issues/25907

ISR自身具有stale-while-revalidate的缓存行为, 并且会在响应头 Cache-Control 里包含 s-maxage.
如果在Next.js之上还有一个缓存层, 则会导致页面的缓存时间变成双倍:
缓存层等到缓存(s-maxage)过期后, 才会访问Next.js,
而Next.js会先返回旧的页面, 这就导致同一个旧页面会存在 s-maxage * 2 的时间.

如果Next.js能在未来的版本里取消过期后的页面的 Cache-Control, 就可以解决此问题.

getStaticPaths 只可用于动态路由的页面,
因此静态路由的页面实际上不能使用 fallback 跳过编译时构建.

这无疑是一种失败的设计.
不过, 跳过编译时构建的ISR实际上非常接近带有缓存的SSR, 因此可以通过"SSR+页面缓存"解决.
请阅读与"SSR+页面缓存"的不同一节.

相关问题: https://stackoverflow.com/questions/69141392/how-to-enable-isr-for-static-routing-in-next-js

至少在v11版本里, ISR页面在 Cache-Control 里设置的 stale-while-revalidate 不包含值, 不符合语法.

如果ISR页面是动态生成的, 则Link组件在生产中的默认预取行为会造成后台突然需要生成大量页面.
解决方法是设置 =prefetch={false}=, 此时预取只对悬停链接有效.

ISR实际上非常像"SSR+页面缓存", 主要区别在于:

  • ISR可以利用编译时的静态构建.
    如果编译时产生静态构建的速度比运行时快得多, 则可以为运行时省下很多计算资源.
  • ISR也是SSG.
    这意味ISR页面像SSG页面一样, 支持预取(prefetch), 从而加速导航到ISR页面的速度.
  • 对于跳过编译时构建的页面, 可以先显示载入中页面(fallback: true).
    这对SEO只会产生负面效果, 因此不会有多少人使用.
  • ISR的缓存机制是stale-while-revalidate, 会在缓存更新完毕之前, 返回旧缓存.
    最大的区别之一, 因为传统的缓存机制在失效后会等待新页面的生成, 而不是先返回旧缓存.
    在这种缓存机制下, 即使在新页面生成过程中发生错误, 网站仍然是可用的,
    这允许CMS系统, 数据库等后端组件临时下线.
    然而, 如果"SSR+页面缓存"使用的缓存机制也是stale-while-revalidate, 则两者没有区别.
  • ISR会为页面添加 Cache-Control 头, 包含 s-maxagestale-while-revalidate.

该功能目前正在开发中:
https://github.com/vercel/next.js/discussions/11552#discussioncomment-646963

ISR包含SSG的所有优点, 只要静态页面本身有可能更新, 总是应该选择ISR而不是SSG.

适合:

  • 页面很多导致编译时间很长的网站.
  • 会更新的静态页面.

不适合:

  • 页面很多导致存储成本很高的大型网站.
  • 动态页面.

Next.js最基础的功能之一, 服务端渲染.

SSR需要页面提供 getServerSideProps.

适合:

  • 页面很多导致存储成本很高的大型网站.
  • 动态页面.

不适合:

  • 静态页面: SSR页面无法得到像SSG那样的优化.
  • 页面生成成本很高的页面.

https://github.com/dunglas/react-esi

建立在ESI规范上的React/Next.js组件缓存库, 对很多云缓存供应商有效, 也支持开源的Varnish.

从历史渊源上看, ESI技术是为像PHP这样可以拼装HTML的服务端语言设计的.
React SSR之所以能够缓存, 是因为把组件的输出结果视作了可缓存的单元, 这的确符合ESI的意图.

考虑到以下因素, 这种方案在很多场合下并不值得实施:

  • 为了适应ESI, 必须将组件的渲染函数作为端点暴露给缓存供应商
    (react-esi默认使用 /_fragment 作为HTTP端点).
  • SSR组件渲染的性能并不是一个迫于解决的问题, 它已经足够快.
  • 组件缓存不兼容Hooks API的状态.
  • 需要为动态获取数据的组件实现专门的getInitialProps方法(和Next.js的函数具有相同的签名和功能, 但服务的对象不同).

虽然可以在 getServerSideProps 里实现"数据获取"的缓存, 但这实际只缓存了整个过程的一半:
不仅是"数据获取", "生成SSR页面"也会消耗计算资源的, 因此只在数据获取阶段使用缓存不能解决问题.

在应用层实现SSR页面缓存的障碍在于, Next.js本身不提供此功能:
在Next.js里, 用于渲染SSR结果的renderToHTML方法并不属于公开的API, 这直接导致了ssr-caching等关键示例被修改.
因此, 所有使用renderToHTML方法的SSR缓存方案都是没有保障的, 并且[[https://github.com/vercel/next.js/issues/25621][事实上已经在版本更新中引发问题]].

除非使用Redis这样的独立缓存服务, 否则应用层页面缓存对使用负载均衡的站点来说效果较差, 因为节点之间的缓存不共享.

https://github.com/Kikobeats/cacheable-response

一个框架无关的SSR缓存中间件, 支持stale-while-revalidate缓存淘汰策略.
它可以与Next.js的自定义服务器一同工作, 并且是官方推荐的做法.

它使用Keyv作为缓存适配器, 实际上stale-while-revalidate缓存淘汰策略也是由Keyv(@keyvhq/memoize)提供的.

审查过@keyvhq/memoize的代码, 发现这个包对stale-while-revalidate的实现基本上是完美的.
如果硬要挑些问题的话, 问题都出在人们喜欢在memoize侧实现缓存策略这一常见模式上:

  • memoize绕过 Keyv.get 直接调用了后端的 Keyv.store.get.
    这导致缓存项目只会在过期失效后触发revalidate, 而不会删除项目
    (Keyv.get 会在取得项目后检查项目是否过期, 如果过期就会删除).
    如果用户需要删除缓存项目的功能, 实际上必须要手动调用Keyv的内部方法取得项目的key, 然后手动删除, 这往往是效率最低的做法.
  • Keyv的项目存储方式是将数据放进一个包含元数据的对象里, 然后将对象序列化, 这也导致取出数据时需要进行反序列化.
    尽管一些后端不需要序列化/反序列化, 这种同时返回元数据和数据的做法仍然会对性能造成影响, 因为后端可能分别存储这两样东西.
  • 这个库没有考虑到原生支持TTL的后端会在超时后删除项目的问题.
    Keyv支持原生TTL的方式是直接在set时将TTL发送给后端, 让后端自行决定是否支持原生TTL行为
    (Keyv自身会将TTL打包进元数据, 所以就算后端不支持, 也会在调用 Keyv.get 时执行过期删除,
    对于那些原生TTL行为的后端来说, 这是冗余的, 因为后端通常会早于 Keyv.get 调用就完成删除).
    然而, 这种行为在stale-while-revalidate里就成为了问题, 因此支持原生TTL的后端实际上不能正确执行stale-while-revalidate.
    这本身是一个实现上的bug, 因为memoize直接调用了 Key.set, 而不是调用 Key.store.set, 可能在未来版本里修复.

缓存层应该通过HTTP响应头来配置缓存, 而不是手动配置,
(手动配置的问题在于, 保持应用程序的配置和缓存层的同步很麻烦, 部署应用程序的人也可能没有权限配置反向代理服务器).

支持根据HTTP响应头来缓存, 同时提供世界上最好的性能.

https://vercel.com/blog/serverless-pre-rendering

SPR是Vercel平台提供的一项CDN功能, 提供stale-while-revalidate缓存服务.
SPR实际上适用于任何HTTP服务, 而不仅仅是Next.js.

Cloudflare默认情况下不会缓存HTML页面, 需要手动设置相关规则.

https://github.com/rjyo/next-boost

一个替代Next.js的自定义服务器, 以提供基于SQLite和文件系统的持久化缓存.
该库不依赖于Next.js的内部API, 需要缓存的页面是通过向内部创建的Next.js服务器发送HTTP请求得到的.

该项目可以有效改善SSR页面的性能.
根据自述文件的说法, 缓存的SSR性能比SSG还要快.

经过代码审查后, 发现整个库从上至下的每一个由同一作者rjyo编写的模块都存在各种各样的缺陷.
尽管作者声称next-boost在生产中投入使用, 也很难相信它是一个足够可靠的库,
该作者对next-boost的使用明显是极度单一和片面的, 否则不可能在发布相关模块超过半年后仍然没有发现和解决这些问题.
由于错误和缺陷是如此之多, 并且它的技术营销很大程度上是一个骗局, 完全有理由将该作者的所有作品拉入黑名单.

此外, 该项目是否真的有必要存在也是一个问题,
因为它的功能与反向代理式页面缓存几乎没有区别, 并且性能也差一个数量级.

此项目并不会自动处理Next.js的内部URL, 也不会识别页面的类型.
在默认情况下, 所有页面都会被缓存.

如果想要获得比较好的体验, 就得手动配置需要缓存的URL.
然而, Next.js的内部URL是由Next.js掌控的,
因此想要精确定义到某个页面的缓存变得很困难, 也缺乏可维护性:
页面并不只有它被浏览器直接导航的路径, 还包括它的json文件等其他内容,
这些文件的路径有可能在未来的版本里发生变化.

于是, 用户只能从以下两种情况中选择其一:

  1. 1.
    由于不能精确设置缓存, 只好缓存整个 /_next 路径.
  2. 2.
    不使用缓存.

此外, next-boost也不支持LRU缓存, 不能限制缓存占用的磁盘空间量.

虽然程序读取了配置文件 .next-boost.js, 但是却没有使用它:
https://github.com/rjyo/next-boost/issues/48

  • https://github.com/rjyo/next-boost/issues/32
  • https://github.com/rjyo/next-boost/issues/44

据作者称随机504错误是由锁的超时机制导致的, 但手动尝试后发现并不能通过超时来复现永久504错误.

理论上, 出现504错误需要发生两件事:

  1. 1.
    缓存过期或首次生成页面, 导致没有fallback可用
  2. 2.
    页面生成失败(无法在10秒内生成).

作者还错误地认为将缓存保存在 /tmp/hdc (默认的位置)下是诱发504问题的原因, 这一愚蠢的推卸被实际的测试结果推翻:
即使缓存不在 /tmp 之下, 也仍然会出现永久504错误.
实际上造成问题的原因是next-boost的缓存更新过程会出现死锁.

next-boost-hdc-adapter使用setInterval进行定时purge, 但是没有正确处理purge的异步调用和出错的情况, 这可能在purge途中产生死锁.

这是next-boost项目底层使用的缓存模块, 启发自python-diskcache.
该项目使用better-sqlite3来使用SQLite3数据库.

对小于等于10KB的值, 会直接作为BLOB保存进数据库.
对大于10KB的值, 会直接作为文件保存在文件系统上, 仅在数据库里记录文件名.

缓存默认保存在 /tmp/hdc.

该项目直接保存了缓存过期的时间而不是缓存更新的时间.
这导致不能通过修改配置文件来改变缓存的过期时间, 如果已经存在一个TTL很大的缓存, 除非手动删除缓存, 否则无法让它更快过期.

该项目将key设置为数据库的主键, 这是错的, 因为key可能在删除后重新使用.

该项目保存了filename字段, 而实际上该字段可以通过 hash(key) 得到.

Gatsby的npm下载量于2020年底被Next.js彻底超越.

  • Gatsby的插件系统鼓励过度封装.
  • 官方插件的代码质量普遍较差, 缺乏足够的测试.
  • 基于钩子的配置模式.
  • 糟糕且落后的文档, 甚至在教程部分就能轻易发现错误.
  • 配置文件不支持TypeScript.
  • 只能全量更新.

Gatsby的数据层是建立在将GraphQL同时作为接口和DSL之上的.
Gatsby的源插件面向GraphQL接口进行开发, 源插件的用户使用GraphQL进行数据获取.

然而, 这项设计是在技术选型时把酷当成好的结果, 它本身没有任何存在的必要性, 还大大降低了项目的灵活性.
GraphQL唯一的作用就是方便Gatsby建立基于源数据插件的生态, 为用户制造供应商锁定.

运行开发服务器 gatsby develop
生成静态页面 gatsby build
本地运行生产代码 gatsby build && gatsby serve

src/pages 由组件组成的页面
src/components 可重用的组件
src/styles CSS样式

GraphQL可查询节点是通过Gatsby的数据源插件添加的.
例如文件系统插件就会添加文件的名称, 创建日期等信息.

一个数据源插件的gatsby-node.js文件暴露sourceNodes函数,
在该函数内调用createNode创建节点.

该插件实际上遍历了otions.ppath设置下的所有文件,
将这些文件转换成以id属性作为唯一值的GraphQL File节点.

转换器插件将从数据源插件里得到的原始数据进一步转换.
在数据源插件就能够提供足够的数据的时候, 是不需要转换器插件的.

一个转换器插件的gatsby-node.js文件暴露onCreateNode函数,
在该函数内通过检查node.internal的属性来决定是否需要创建新的节点(通常检查mime类型).
和数据源插件一样, 通过调用createNode创建新节点.

这个转换器插件系列是由官方维护的, 代码质量差得惊人.
由于gatsby将简单的事情搞复杂, gatsby的博客用户基本上都需要依赖于这个插件提供的功能.
实际上这个插件的功能非常小,
以至于需要给转换器再加上一大堆以gatsby-remark开头命名的插件才足以使用.

代码质量差得惊人.
它事实上调用了gatsby-plugin-sharp(其作用是利用sharp模块生成小尺寸的图像),
因此使得大图像在输出时以小尺寸进行显示, 而无需由写markdown文档的人亲自处理图像.

在概念上足够简单的渐进式静态站点生成器.
与Gatsby的主要不同是没有复杂, 烦人且多余的GraphQL和数据源/转换器插件.
可以混合静态生成和SSR网站.

缺陷:

  • 和Gatsby一样, 配置文件不支持TypeScript.
  • 考虑到用户数量和发布频率, 以及Next.js的存在, react-static似乎没有被使用的意义.