关于 Next.js 最新的特性, Partial Pre-Rendering 和 SSR 之间的问题

268 天前
 lazyczx

我以前写过 React ,现在想学 Next.js 。跟着官方教程在学它的那个教程

学到 chapter 11: Partial Pre-rendering 越学越迷茫,我不懂它说的某些功能的表现是怎么样的,虽然我看得懂它教程里说的内容,这个框架是挺美好的,但是实际用起来真的很奇怪。

  1. 教程里说使用 unstable_noStore() ,给一个服务端组件的 API 层,可以禁用 cache ,这样可以让一些用户间不共享的信息,在用户请求的时候可以实时更新。问题是我就算吧这个 noStore 注释了,每次我刷新页面的时候,对应的 API 方法又重新调了。这就和我理解的冲突了,不是说服务端渲染好的页面,只要不是 dynamic 的(用了 noStore 就是 dynamic ),就不会那么快实时更新吗?而且我怕只看 API 方法重新调不合适,说不定它框架里做了从调用上看不出来的缓存。。然后我跑去 vercel 上 update 了数据库,好家伙,一刷新页面,修改了的数据立马反映出来了。。那是不是说明这个服务端根本不是静态渲染的,而是默认动态的,那我加不加这个 noStore 不是一摸一样吗?我去外网和官方文档上找,它说这个就是加了个 http header 。。具体有用没用好像是看网页客户端的,但是我这个不是服务器请求服务器吗?哪来的网页客户端,不是 nodejs ,或者这里是因为 nodejs 模拟了浏览器内核的缘故吗?

  2. 这个 Next.js 是不是有什么问题?我在测试这个缓存过没过期的时候,随便搜就搜到了这个帖子,它贴的官方文档说 dynamic 是 5s ,static 是 30s ,然后我又觉得这个缓存不起效可能和我一直在点刷新有关,我是不是应该在应用内点跳转链接,然后机缘巧合之下,我在等 30s 之后,开始点页面内的 navigation ,好家伙每点一下,API 方法就被调用一次。。下面是我弄的计算方法执行时间的 log:

    cost time: 773ms
     Fetching revenue data...
     cost time: 699ms
     Fetching revenue data...
     cost time: 213ms
     Fetching revenue data...
     cost time: 231ms
     Fetching revenue data...
     Fetching revenue data...
     cost time: 216ms
     Fetching revenue data...
     cost time: 215ms
     Fetching revenue data...
     Fetching revenue data...
     cost time: 246ms
     Fetching revenue data...
     cost time: 647ms
    

    而且我在 vercel 的[云服务上也复现]( https://nextjs-dashboard-kappa-amber-64.vercel.app/dashboard )了,有兴趣的可以等个三十秒,然后疯狂点那个 home 的按钮实时,每点一下就来一个网络请求,但是不知道这个网络请求是干嘛的,但是我本地的 dev 确实打这些 log 了。而且刚刚刷新,进去网站的时候,你就算疯狂点,也只会加载一次( api 方法也只调了一次),等个 30s 就这样了,是不是有什么问题。

写的有点长了感觉,但是这个 next.js 我是真越学越晕啊,各位如果有兴趣可以试一试,另球球前端大佬指点(我又来白嫖网友力量了嘿嘿 OWO ),在下先行拜谢!

2162 次点击
所在节点    程序员
28 条回复
lazyczx
268 天前
另外,我把代码仓库 public 了,想要看问题 2 的兄台走[这里]( https://github.com/dribble312/nextjs-dashboard)
lazyczx
268 天前
写的太长了,而且看的教程也是那么长的英文,估计很难得会有大佬有时间帮忙指点这种根基性的问题了,如果没有本贴权当一些记录和发泄了,相信有一天关于这些问题,会豁然开朗!
SmiteChow
267 天前
请求是请求,渲染是渲染,两码事
epiloguess
267 天前
这个问题比较复杂,我一点一点回答你。
先讨论正式版,再讨论部分预预渲染
1.next dev 和 build 的渲染逻辑是不一样的,如果你把 data.ts 中 dashboard 相关的那三个组件的 noStore 注释掉,然后 build 一下,你会发现 /dashboard 生成的是静态页面,因为页面没有动态函数而且数据都 cached 了,但 dev 的时候情况可能不一样,数据可能没有全部 cache 完,或者说每次刷新的时候都会重新请求相关数据,这样其实更符合逻辑。注意,你加不加 suspense 都不会影响静态渲染
2.在正式版中,如果你给那三个组件中任意一个加上了 noStore ,整个 dashboard 页面,包括那三个组件都会退出静态渲染,这一点也并不难理解,回顾官方定义,再考虑一下目前渲染路线其实就两种,组件又在页面之中,next 在 build 的时候遇到 noStore 就知道下一步该选什么渲染路线。
> unstable_noStore 可用于以声明方式选择退出静态渲染并指示不应缓存特定组件。

3.你希望达成什么?我想应该是不能够每次刷新都去查询数据,最好可以手动 revalidateTag 。
有两个函数,可能可以帮到你,react cache 和 unstable_cache
https://nextjs.org/docs/app/building-your-application/caching#react-cache-function
https://nextjs.org/docs/app/api-reference/functions/unstable_cache

你可以现在就试试,记得随便一个组件上加上 noStore,然后在 RevenueChart 组件上创建一个函数,
import { unstable_cache} from 'next/cache';

const getCachedRevenue = unstable_cache(
async () => fetchRevenue(),
['Revenue']
);
组件内部开头删掉
const revenue = await fetchRevenue()
然后这样写,
const revenue = await getCachedRevenue()

先 build,start,然后进 dashboard ,在刷新的时候,你会发现,Revenue 组件会保持不变,另外两个出现了骨架屏,可以换个浏览器或者进隐私窗口,一样是秒开。
4.关于你的第二点,没看太明白,这个 30s,5 分钟之类的,都是客户端缓存,你在开发者工具的网络选项卡把选一下禁用缓存,第三点的效果不会变,多看看文档这一节,https://nextjs.org/docs/app/building-your-application/caching#overview
5.最后再来说一下部分预渲染,

根据,https://nextjs.org/docs/app/api-reference/functions/unstable_noStore
在 unstable_cache 内使用 unstable_noStore 不会选择退出静态生成。相反,它将根据缓存配置来确定是否缓存结果。

这句话不太好懂,不过我实测的结果就是,如果你是正式版,那么无论你任何地方使用了 noStore ,在 build 的时候,路线都会变成动态,而如果你是 canary,想要使用部分预渲染,最好还是不要在 unstable_cache 缓存的函数内使用 noStore,参考 https://nextjs.org/docs/messages/ppr-caught-error
> 确保您没有将选择动态渲染的 Next.js API 包装在 try/catch 块中。
尽管官方建议,可以在 try...catch 前插入 noStore, 但后面实现缓存函数不太方便,所以我个人建议,可以在组件的第一行,也就是 const revenue = await getCachedRevenue()的上面一行,使用 noStore ,第二行用 cache,逻辑也比较清晰。


同时,根据,https://nextjs.org/docs/app/api-reference/next-config-js/partial-prerendering
你需要 npm install next@canary ,然后改 config (这个我看有注释),记得每个组件都要加上 noStore ,不然参考第一条,就会被当成静态内容一次生成哦(关于哪些内容被生成静态的了,你可以去开发者工具的网络选项卡,预览第一个送过来的 Document ),然后可以 build 了哦。

祝好!

其他参考(不分先后,我也没看(完),不过很值得看):
- https://github.com/vercel-labs/next-partial-prerendering
- https://github.com/orgs/vercel/discussions/4696
- https://codedrivendevelopment.com/posts/rarely-known-nextjs-features
- https://github.com/vercel/next.js/pull/56930
- https://stackoverflow.com/questions/76829076/in-next-js-13-app-router-how-can-i-use-data-caching-when-not-using-fetch-but
- https://github.com/vercel/next.js/discussions/54075
epiloguess
267 天前
这里补充一下,为什么 unstable_noStore 在正式版和 canary 里表现不一致,可能是因为这个 function 就是为了后续的功能开发的,当前版本可以通过配置路由段,或者 fetch 一个空数据加上 no-store 来退出静态渲染
Xu3Xan89YsA7oP64
267 天前
这个教程我也做了,我没复现你这个问题。noStore 是判定是否为 dynamic 的边界,你试试把所有 noStore 都注释了,可能是另一个 noStore 的边界包含了你注释的位置
lazyczx
267 天前
@epiloguess 太感谢了,你是我的神!!!

抱歉看到之后过了很久才回复,因为我一边看你的回复,一边自己实验了相当一段时间!

1. 那个 cache 真的很强大,被 cache 包裹的 api 方法调用就像是被锁到了另外一个空间,同页面的别的组件都不会影响到它,引起它的的 revalidation 。我看到一个 noStore() 标注的方法执行的时候,别的 API 方法也跟着被调用,但是作为 cache fallback 的方法不被调用。我觉得这个就是 ppr 了对吗? canary 的支持只不过让这个实现更加简单了一点而已:不需要 cache 方法包裹,而是没有使用 noStore 就默认为 cache 。

2. 如果调用了 noStore 的方法没有 cache 作为 fallback ,而是直接调用了的话,会导致整个 page 直接变成 dynamic 的,此时就算别的 api 调用没有调用 noStore ,也会每次刷新都自动调方法。如果所有方法都没有“有效的” noStore ,则在 start 启动的时候直接变成 static 页面,而在 dev 的时候,方法会调用,但是请求出不去,整个方法的执行都在毫秒级完成。

3. 我说的那个一直点,一直调请求的,还是会时不时复现,有段时间我一直没有成功复现,然后我同时打开了 local 的页面和 vercel 上托管的页面,立马就出现这个问题了,我甚至都怀疑是不是它们两个之间互相有什么影响,但是我不知道是什么原因,照理说域名都不一样,怎么会有互相影响呢。。但是我发现,如果页面是 static 的,肯定就不会有问题。但是我猜大概从表现的危害上来说也可以接受吧,毕竟页面都是 dynamic 的,让它每点一下就调一次又怎么样了呢,但是原因还是不清楚。。关于这个,我想到个问题,如果页面是 dynamic 的话,是不是最好把后端接口调用放在客户端(如果没有保密需求的话),换言之前端服务器做这种 proxying (不包括缓存)的事情,对前端服务器压力大吗?因为我想如果是 user 之间不共享的数据,那每个 user 都要从你前端服务器 proxy 出去,应该有很大的吞吐量,框架要处理这些,会不会在上面浪费很多性能。

4. 关于第 5 点,我不知道你是怎么测试的,但是我测试的情况下,如果 API 方法里使用了 noStore ,然后把这个方法作为 cache 的 fallback 的话,确实不会退出静态生成。build 的时候显示的 tree 也显示页面仍然是 static ,如果不用 cache 的话,tree 马上变为动态 λ 的了。我感觉正式版里只要用 cache 了,noStore 就跟没有调用一样,好像 cache 这个缓存,会直接接管这整个方法调用的结果,然后选择是否缓存。

再次感谢大佬,very informative answer ,让我学到很多。

其实好久没写前端了,你说起 build start 的时候,我猛然想起,以前做前端项目的时候都是每次改代码,每次 build start 的哈哈。
lazyczx
267 天前
@shizhibuyu2023
noStore 都注释了的话,就变成 static 页面了,static 页面确实永远都不会出现这个问题,这一点我在本地测试了。

但是我感觉我碰到的这个表现,这个和时间有关就很扯哈哈,就有的时候刚点进去,navigation 切了几下,就不会调接口了。然后等了一段时间,就算在 home 狂点那个 home 也会一直调接口,但是刚刚进去页面的时候不会这样。你点过我部署的那个页面了吗?你不会遇到这个问题吗?就进去之后先在 home 和 invoices 之前切几下,可以看到切几下之后不管怎么切,network 都没有新的请求了,然后等个一分多钟,再去点 home ,每点一下,就出来一个新请求,这是在动态页面的情况下。主要是和时间有关,太奇怪了。
lazyczx
267 天前
@shizhibuyu2023

不过就算是 dynamic 的,也有不复现的概率,我有几次就死活没法复现。有段时间就是会出现这个问题,我不确定是什么原因,总之很奇怪。

喏,又复现了:

https://imgse.com/i/pFTauGt

而且这是在 dynamic 的情况下,意味着每次点这个 home 就调一次数据库:
cost time: 773ms
Fetching revenue data...
cost time: 699ms
Fetching revenue data...
cost time: 213ms
Fetching revenue data...
cost time: 231ms
Fetching revenue data...
Fetching revenue data...
cost time: 216ms
Fetching revenue data...
cost time: 215ms
Fetching revenue data...
Fetching revenue data...
cost time: 246ms
Fetching revenue data...
cost time: 647ms
但是页面刚打开的时候却没有这种问题,很奇怪。
lazyczx
267 天前
我整了个 discussion 到 next 的 github community 碰碰运气

https://github.com/vercel/next.js/discussions/63889
epiloguess
267 天前
前提:canary 但是 config 里面注释掉 ppr = 正式版

1.
> 我看到一个 noStore() 标注的方法执行的时候,别的 API 方法也跟着被调用,但是作为 cache fallback 的方法不被调用。我觉得这个就是 ppr 了对吗? canary 的支持只不过让这个实现更加简单了一点而已:不需要 cache 方法包裹,而是没有使用 noStore 就默认为 cache 。

ppr 指的是部分预渲染,相当于默认一切都是静态的,只要 suspense 里面没有 no store,但是这里有坑,最后说。

在正式版/canary without ppr 中,
这个 noStore 是不是 cache 的 callback,都没有关系,只要你这个路线中,任意一个地方出现了 noStore,next 在 build 的时候,就等价于路由段配置中的 force-dynamic 或者 fetch 中的 no-store,相当于退出静态渲染,改用动态。

当你刷新的时候,为什么另外两个组件都去获取数据了,有 cache 的却没反应呢,因为 unstable_cache 它 cache 的函数的返回值。

所以这跟 ppr 有什么关系?重要的是要把 ppr 和 cache 分开。

你加 cache,只是为了优化,降低它查询数据库的频率。

2.
有一点非常值得注意的是,在 nextjs 中的 fetch,和 unstale_cache 表现是不一样的,

fetch 缓存的是 fetch 请求的返回值,也就是说,为什么在没有 unstable_cache 的时候,你会看到你写的测试时间的方法被调用,因为整个请求数据+计算时间的函数都会被执行,只不过 await sql`` 的时候,直接从缓存中给你值了(这里也有坑)

而 unstable_cache 缓存的是给定函数的返回值,(请求数据+计算时间)这个函数的返回值被缓存了,里面的计算时间自然不会被执行了。

3.
> 但是我发现,如果页面是 static 的,肯定就不会有问题。但是我猜大概从表现的危害上来说也可以接受吧,毕竟页面都是 dynamic 的,让它每点一下就调一次又怎么样了呢,但是原因还是不清楚。

static 的时候,直接就是 html 了,肯定不会调用你写的计时方法。
你需要确保的是,如果函数都加上 unstable_cache 了,你这个问题应该就不会出现了吧。


4.
> 关于第 5 点,我不知道你是怎么测试的,但是我测试的情况下,如果 API 方法里使用了 noStore ,然后把这个方法作为 cache 的 fallback 的话,确实不会退出静态生成。build 的时候显示的 tree 也显示页面仍然是 static ,

你这个是在什么条件下测试的?

> 如果不用 cache 的话,tree 马上变为动态 λ 的了。
在正式版中,任意组件任意位置存在 noStore ,是的,路线就会退出静态渲染,跟 cache 没什么关系。

> 我感觉正式版里只要用 cache 了,noStore 就跟没有调用一样,好像 cache 这个缓存,会直接接管这整个方法调用的结果,然后选择是否缓存。

正如我前面所说,这事儿跟 cache 没什么关系,cache 只是为了降低 动态组件 查询数据库的频率,在静态路线里写不写 cache 没什么意义,是同一个结果

5.一些坑以及一些猜测
深入理解 nextjs 的缓存,
我 fork 了你的项目,RevenueChart 没有 cache,另外两个有,build,start
当你刷新页面的时候,你会发现 RevenueChart 在获取数据,你的终端上出现 fetching data,另外两个数据立刻就有了
这中间发生了什么?

> unstable_noStore can be used to declaratively opt out of static rendering and indicate a particular component should not be cached.
> unstable_noStore 优于 export const dynamic = 'force-dynamic' ,因为它更细粒度并且可以在每个组件的基础上使用

unstable_noStore 是用来配合部分预渲染,实现细粒度的控制组件的渲染方式,当你在另外两个组件中声明 noStore 的时候,就成了一个动态组件
> unstable_noStore is equivalent to cache: 'no-store' on a fetch
> no-store - Next.js 在每次请求时从远程服务器获取资源,而不查看缓存,并且不会使用下载的资源更新缓存。

但是这并意味着我们就没有 data cache 了

> Next.js 有一个内置的数据缓存(data cache),可以在传入的服务器请求和部署中保留数据获取的结果。这是可能的,因为 Next.js 扩展了本机 fetch API 以允许服务器上的每个请求设置自己的持久缓存语义。


---以下为猜测---
假如我们的组件声明了 noStore,组件内部 fetch 了一个资源,没有理由这个 fetch 会退出 nextjs 的 data cache.

我们声明了 noStore,只表达了这是个动态组件,它不是网页静态的一部分,当你访问/刷新网页的时候,你应该永远从服务器获取组件的内容(这里先不提客户端缓存)


因为 @vervel/postgre 的 sql 也是基于 fetch 的
这一点很奇怪,完全想不通,但是参考
https://github.com/orgs/vercel/discussions/4696
https://nextjs.org/docs/messages/ppr-caught-error
> Database Error: NeonDbError: Error connecting to database: Route /dashboard needs to bail out of prerendering at this point because it used revalidate: 0. React throws this special object to indicate where. It should not be caught by your own try/catch. Learn more: https://nextjs.org/docs/messages/ppr-caught-error
> As a convenience, it is not necessary to set the cache option if revalidate is set to a number since 0 implies cache: 'no-store' and a positive value implies cache: 'force-cache'.
> 为方便起见,如果 revalidate 设置为数字,则无需设置 cache 选项,因为 0 隐含 cache: 'no-store' 且正值意味着 cache: 'force-cache'

sql 的内部实现可能用的是 fetch(``, { next: { revalidate: 0 } })

一个悲伤的事情是,正式版的静态渲染和 revalidate 0 配合工作良好,ppr 却不行,这也就意味着,对于带有数据库查询的组件,你并不能 ppr 它们变成完全静态的

当然,这个问题是可以被解决的,配合 router handle,你其实可以用 fetch 获取某些数据库的信息(安全吗?)

---接下来让我们回到刷新后的渲染过程---

为了解决 sql 的 revalidate0,我们引入了 unstable_cache 。

当我们刷新页面的时候,服务器上已经有了那两个组件 cache 的 RSCP,直接就发送过来了,内容是立即出现的
而 RevenueChart 没有 cache,并且由于 sql 的 revalidate0 ,需要重新查询数据库,终端上出现 fetching data

为什么第一次慢后面快?所以给你一种查询没有发出去的错觉?
猜测 1:可能确实没发出去,毕竟 revalidate0 是我的猜测
猜测 2:发出去了,第一次慢是因为网络问题,vercel 在国外,tcp 慢启动,第二次第三次就快了

这一点其实也很好验证,你一边刷新(可以用插件自动刷新),一边在 vercel 数据库中新建一个数据就可以判断了,交给你了,等你反馈

---客户端缓存/路由器缓存---

> 当用户在路线之间导航时,Next.js 会缓存访问过的路线段并预取用户可能导航到的路线(基于视口中的 <Link> 组件)。
> 导航之间不会重新加载整页,并且会保留 React 状态和浏览器状态。
> 会话:缓存在整个导航过程中持续存在。但是,它会在页面刷新时被清除

这一点也很好验证,当你刷新的时候,RevenueChart 会重新获取数据,当你点击左边导航随便一个再点回来,不会触发重新获取,终端上也不会有 fetching data


---最后---

目前你这个例子有一点小,还都是获取 db ,可能无法明显看出 ppr 的优势

对于 ppr 和 sql revalidate 0 的问题,如果真的有想要完全静态的组件还带有 db 查询,
我的建议是 unstable_cache 梭哈,不设置过期时间,算是一种半静态吧,除了第一次比较慢,后面其他用户第二次访问就很快了
epiloguess
267 天前
忘了发 fork 地址了抱歉,https://github.com/epiloguess/nextjs-dashboard
epiloguess
267 天前
更正:
1. 我看到一个 noStore() 标注的方法执行的时候,别的 API 方法也跟着被调用,但是作为 cache fallback 的方法不被调用。
第一点讨论的有点多余,因为我把 fallback 看成 callback 了

2.但是这并意味着我们就没有 data cache 了
“并”改成“并不”
lazyczx
267 天前
@epiloguess

关于第一点,我确实测试的是非 canary ,我以为这个 canary 是 unstable 的,就没想这个是正式版 haha 。。

---

关于和 PPR 有什么关系:我当时就是感觉如果假设页面上只有两个调 API 的组件,其中一个 A 组件调 API 的时候用 cache ,另一个 B 调 API 的时候用 dynamic ,那就相当于 PPR ,因为 A 组件在不 revalidation 的情况下就一直是静态的了,而 B 组件会变化,这和我心里想的部分预渲染的定义相符。可能到教程里的例子,就是把除了 noStore 的,其他的 API 调用都用 cache 包起来,就相当于实现了 canary 里的 PPR 的功能了,即默认其他的都是静态。

也可能是 cache 的文档里有 revalidation 这个术语,让我想起了教程里 chapter 10 有这句话(看,这里也有 revalidation ):

> Partial Prerendering leverages React's [Concurrent APIs]( https://react.dev/blog/2021/12/17/react-conf-2021-recap#react-18-and-concurrent-features) and uses [Suspense]( https://react.dev/reference/react/Suspense) to defer rendering parts of your application until some condition is met (e.g. data is loaded).
>
> The fallback is embedded into the initial static file along with other static content. At build time (or during **revalidation**), the static parts of the route are *prerendered*, and the rest is *postponed* until the user requests the route.

​ 所以我以为它们也许是 通 的。

---

> 你需要确保的是,如果函数都加上 unstable_cache 了,你这个问题应该就不会出现了吧。

这个我觉得应该是不会了,毕竟我当时测试的时候,加上了 unstable cache 之后,如果没有设定 revalidation 的话,不管怎么操作,都不会 rerender 了,就像是一个动态页面里独立出来了一个静态的玩意儿。所以用了这个应该是绝对不会出来这个问题。

---

> \> 关于第 5 点,我不知道你是怎么测试的,但是我测试的情况下,如果 API 方法里使用了 noStore ,然后把这个方法作为 cache 的 fallback 的话,确实不会退出静态生成。build 的时候显示的 tree 也显示页面仍然是 static ,
>
> 你这个是在什么条件下测试的?

是非 canary 下测试的呀,就是 noStore 只加在 revenue api 方法上,然后用 `cache(()=>getRevenue(), ['revenue'])`。发现 router 树 dashboard 后面的符号是 圆圈。而且用页面的时候 revenue 也没有被调用(除非有 revalidation )。

> 在正式版/canary without ppr 中,
> 这个 noStore 是不是 cache 的 callback,都没有关系,只要你这个路线中,任意一个地方出现了 noStore,next 在 build 的时候,就等价于路由段配置中的 force-dynamic 或者 fetch 中的 no-store,相当于退出静态渲染,改用动态。

我刚刚去把版本换成 canary without ppr 测试了一下,我用了 noStore 在 API 方法里,然后使用 cache 把 API 包裹起来,生成树还是静态的(注意我没有使用你上面回答里的推荐做法,即把 noStore 放在组件里,放在 cache 方法上方,不知道是不是这里有误解。。)。

```
Route (app) Size First Load JS
┌ ○ / 226 B 98.9 kB
├ ○ /_not-found 871 B 87.8 kB
├ ○ /dashboard 293 B 92.2 kB
├ ○ /dashboard/customers 139 B 87 kB
├ ○ /dashboard/invoices 1.65 kB 95.3 kB
└ ○ /dashboard/invoices/create 172 B 93.8 kB
+ First Load JS shared by all 86.9 kB
├ chunks/23-51b06a4d0afaaa6e.js 31.3 kB
├ chunks/fd9d1056-f593fbcc7b74c7aa.js 53.6 kB
└ other shared chunks (total) 1.91 kB


○ (Static) prerendered as static content
```

而且我测试的时候又遇到个怪问题(为什么我老是遇到怪问题。。。):我在用 canary 版本 build 我做到后面 chapter 的代码的时候,发现 /dashboard/invoices 这个 route 变成 dynamic 的了,然后我慢慢注释一些我觉得可疑的东西,想试试是什么把它变成 dynamic 的。

结果我发现是这行代码。。。(下面有 CONFUSION 的):

```tsx
export default async function Page({
searchParams,
}: {
searchParams?: {
query?: string;
page?: string;
};
}) {
// ! CONFUSION: why once use this line, '/dashboard/invoices' became dynamic ??
const query = searchParams?.query || '';
// const currentPage = Number(searchParams?.page) || 1;
// const totalPages = await fetchInvoicesPages(query)
return (
<div className="w-full">
<div className="flex w-full items-center justify-between">
<h1 className={`${lusitana.className} text-2xl`}>Invoices</h1>
</div>
<div className="mt-4 flex items-center justify-between gap-2 md:mt-8">
<Search placeholder="Search invoices..." />
<CreateInvoice />
</div>
{/* <Suspense key={query + currentPage} fallback={<InvoicesTableSkeleton />}>
<Table query={query} currentPage={currentPage} />
</Suspense>
<div className="mt-5 flex w-full justify-center">
<Pagination totalPages={totalPages} />
</div> */}
</div>
);
}
```

我真是惊了个呆 o_o ....,这个不是我自己定义的一个 prop 吗。为什么会把 route 变成 dynamic 的。。。

关于上面两点,我 commit 了代码( commit message is: canary test ) push 了,你可以跑跑看。(另外我发现我之前没 push 数据库配置。。。我打开你 fork 的仓库跑起来直接数据出不来。。现在我 push 上去了)

---

> \> Next.js 有一个内置的数据缓存(data cache),可以在传入的服务器请求和部署中保留数据获取的结果。这是可能的,因为 Next.js 扩展了本机 fetch API 以允许服务器上的每个请求设置自己的持久缓存语义。

这部分不懂,动态组件是 PPR 的概念对吧?意思是动态组件也可以调用这个内置的数据缓存吗?如果是这样的话,那 sql 不也是基于 fetch 的吗?意思是通过恰当的配置,可以让一个动态组件每次读的是缓存的数据?

动态组件的情况下,并不意味着我们不能用缓存吗?

> https://github.com/orgs/vercel/discussions/4696
>
> Your response are cached because `@vercel/postgres` uses `fetch` for `sql` and `fetch` is cached by default in Next.js ( https://nextjs.org/docs/app/building-your-application/caching#data-cache). You can use [Segment Config Options]( https://nextjs.org/docs/app/building-your-application/caching#segment-config-options) to control how caching works.
>
> I have created an example at https://github.com/vercel-support/167147-postgres-cache-nextjs/tree/main/app/api where you can compare [the default (cached) response]( https://167147-postgres-cache-nextjs-nsnjr8w00.preview.vercel-support.app/api/default) with [one that's fully dynamic]( https://167147-postgres-cache-nextjs-nsnjr8w00.preview.vercel-support.app/api/no-cache) .

没仔细看它里面贴的网站,但是他这里说 sql fetch 是默认缓存的(因此造成了不修改请求代码的情况下会返回 stale data ),这好像和下面的 revalidate 0 冲突啊,revalidate 0 不是 no-cache 的意思吗?

> 一个悲伤的事情是,正式版的静态渲染和 revalidate 0 配合工作良好,ppr 却不行,这也就意味着,对于带有数据库查询的组件,你并不能 ppr 它们变成完全静态的

我 pull 了你的仓库,试了一下,确实是你说的这样。。

只要调了 sql ,而且没有 noStore 就会报错,除非使用 cache 。

我这里也报关于 validate 0 的问题,我也认为应该是 ppr 和它冲突了。

---

其实我就是想说 callback 的,我自己打错了,我看到你说这句,我回去看我写的那么多个 fallback 我也懵逼了,然后在 Suspense 组件那里看到同名 prop 了,疑似被记忆注入替换了 hoho 。
lazyczx
267 天前
@epiloguess

想说点无关的话,希望不会 bother

不知道是不是因为喜欢潜水久了,突然有解决不了的问题,然后被逼的开始和人交流,带来的感觉真的很不错,感觉自己现在甚至有点像什么人来疯那种心态了(开玩笑 hh )。之前也交流过后端的问题在 v2 ,也有经验者帮忙,感觉到自己真的处于一个互帮互助的社区,这种感觉真的很棒!而且很多问题问 gpt 感觉问不出所以然(可能语言组织还不够),尤其是这种比较新的问题( chatGPT 3.5 数据库才更新到去年 4 月份,然后国内 gpt 又感觉会有很多幻觉),还是交流起来开心哈哈哈。。虽然打字组织语言和想法真的挺累的,但是感觉这也是在锻炼自己薄弱的环节,这感觉也很好!!
epiloguess
266 天前
@lazyczx

只是一个假设,方便测试,使用 canary 但是不使用 ppr,表现出的行为应该和正式版的行为是一样,如果每次测非 ppr 都要切换回正式版就很麻烦了。


---

> 后备与其他静态内容一起嵌入到初始静态文件中。在构建时(或重新验证期间),路线的静态部分被预渲染,其余部分被推迟,直到用户请求路线。

主要的问题在与,你要深刻理解 nextjs 中服务端组件的渲染机制

动态组件,在客户端的服务端组件,客户端组件本身
ppr 和静态+cache+dynamic
静态导出/静态渲染
这些概念不要搞混,这些并不相等

> https://nextjs.org/docs/app/building-your-application/rendering/server-components
> 使用静态渲染,路线在构建时渲染,或者在数据重新验证后在后台渲染。结果被缓存并可以推送到内容交付网络 (CDN)。此优化允许您在用户和服务器请求之间共享渲染工作的结果。

在没有 ppr 之前,路线只有静态渲染,动态渲染,流式渲染
这就意味者,一旦一条路线被确认为动态渲染,这条路线中的所有内容都不会在构建或者重新验证后在后台渲染,从而被缓存为 RSCP 和 html ,推送到 CDN,共享渲染结果
而是会在请求的时候进行渲染,在请求的时候才利用 data cache (或者 unstable_cache)加速渲染结果

这也是为什么我们需要 ppr,尽可能提前渲染更多的内容,加速体验


> 在渲染过程中,如果发现动态函数或未缓存的数据请求,Next.js 将切换为动态渲染整个路由。下表总结了动态函数和数据缓存如何影响静态或动态渲染路由:

这里是理解的重点,动态函数和未缓存的数据请求
动态函数中就包括你后面的迷惑点 searchParams ,虽然它是你传的 props,但是只有在请求的时候才能被获取,页面也会因为变成 dynamic
未缓存的数据请求,包括,
> https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#opting-out-of-data-caching
The cache: 'no-store' is added to fetch requests.
The revalidate: 0 option is added to individual fetch requests.
The fetch request is inside a Router Handler that uses the POST method.
The fetch request comes after the usage of headers or cookies.
The const dynamic = 'force-dynamic' route segment option is used.
The fetchCache route segment option is configured to skip cache by default.
The fetch request uses Authorization or Cookie headers and there's an uncached request above it in the component tree.

其中数据库查询的背后用的就是 revalidate0,这一点,我给出那个 github 上的 discussion,那个信息已经过时,unstable_cache 在那之后发生了一次重构,本身也很合理,没理由一个数据库操作自带 cache,改成用 revalidate0 很合理

官方文档这一部分也值得多阅读几次
> https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#fetching-data-on-the-server-with-third-party-libraries

---
> 加上了 unstable cache 之后,如果没有设定 revalidation 的话,不管怎么操作,都不会 rerender 了,就像是一个动态页面里独立出来了一个静态的玩意儿

unstable cache 的缓存是存放在服务端的,在.next/cache 目录下,完全重新部署就会没有的,只是作为组件内 db 操作无法 ppr 的权宜之计,这一点后面再说,实际上,如果你这么使用,本质上和不用 ppr 没有区别,单就这个组件而言,表现出的效果是一样的


---

> 我刚刚去把版本换成 canary without ppr 测试了一下,我用了 noStore 在 API 方法里,然后使用 cache 把 API 包裹起来,生成树还是静态的(注意我没有使用你上面回答里的推荐做法,即把 noStore 放在组件里,放在 cache 方法上方,不知道是不是这里有误解。。)。

这一点我之前已经提到过了,不过就连我自己都忘了,借助这一点我们就可以 ppr 数据查询操作了,太棒了!
> https://nextjs.org/docs/app/api-reference/functions/unstable_noStore
> 在 unstable_cache 内使用 unstable_noStore 不会选择退出静态生成。相反,它将根据缓存配置来确定是否缓存结果。

让我们简单回顾一下
1.在正式版/canary without ppr 中,如果我们在页面中任意位置声明了 noStore ,路线就会退出静态渲染,因为这相当于在路线中出现了 未缓存的数据请求,等价与 force-dynamic 或者 no-store
但是,如果不声明 noStore ,尽管我们猜测 db 操作背后是 revalidate0,它不会让路线退出静态渲染,
关于这一点,或许我们就不应该在正式版中使用 noStore,这个 function 如我之前所说,是用来在 ppr 中细粒度的控制组件渲染方式的
2.在 ppr,
如果我们在 suspense 边界内部声明了 noStore,根据约定,suspense 内部应该是一个动态组件,内容不应该被 ppr
如果我们的组件没有声明 noStore,却使用了 db 操作,我们猜测 db 操作背后是 revalidate0 ,nextjs 会表现出和正式版不一样的操作,它会很困惑,不知道我们到底要做什么,我觉得这里就是一切困惑的来源。ppr 无法和 revalidate0 共存,可以提 issue 。
好在官方可能考虑过这一点了,如果你在 unstable_cache 缓存的函数中声明 noStore,那么在 nextjs 看来,组件仍然是可以被 ppr 的,不会受到 nosStore 和 revalidate0 影响了

---


> 我真是惊了个呆 o_o ....,这个不是我自己定义的一个 prop 吗。为什么会把 route 变成 dynamic 的。。。

上面已经提到了,searchParams 属于动态函数

> 另外我发现我之前没 push 数据库配置。。。我打开你 fork 的仓库跑起来直接数据出不来。。现在我 push 上去了)

你需要 clone 我 fork 的版本然后加上.env 啊,不然没有数据啊,这一点我忘了提了


---

> 这部分不懂,动态组件是 PPR 的概念对吧?意思是动态组件也可以调用这个内置的数据缓存吗?如果是这样的话,那 sql 不也是基于 fetch 的吗?意思是通过恰当的配置,可以让一个动态组件每次读的是缓存的数据?

> 动态组件的情况下,并不意味着我们不能用缓存吗?

没错,是这样的,前面说过了,要分清楚动态组件和服务端组件的区别

或许你应该把 ppr 换个角度,看成缓存了更多静态内容的动态页面

动态组件也要经过服务器才能查询 db ,或者 fetch 相关数据,不可能把 db 相关操作放到客户端来进行

这也就意味着他们可以使用
1. fetch 带来的持久缓存
2. 每次 req-res 周期中存在的 rect Request Memoization
3.unstable-cache

但是动态组件并不能使用 全路由缓存
> https://nextjs.org/docs/app/building-your-application/caching#opting-out-2
> 您可以选择退出完整路由缓存,或者换句话说,为每个传入请求动态渲染组件,方法是:


---

不会觉得 bother

我其实也是最近一个月才接触 nextjs,hh ,没有做过这个项目,因为我学东西喜欢从 doc 看起,doc 中其实 canary,ppr 的内容很少,几乎没有。很高兴多学习了一些知识,也加深了自己对 nextjs ,data fetch ,cache,render 的理解。

码字组织语言有助于提高自己的逻辑表达能力,不过 v 站这个排版效果真是让我一言难尽哦
lazyczx
266 天前
@epiloguess

概念讨论:

因为很多文档我还没有读过(不过我后面会认真读的,尤其是你发出来的文档! owo ),所以我这里就是大胆秀一下我的理解,如果有不对的地方,希望能够获得宝贵的指正!

动态组件是 PPR 开始才有的,之前是只有动态 route 。它是在静态渲染页面里面动态的部分,每次 request 都会重新渲染。

在客户端的 RSC 和 RSC 本身,这个有点混淆不懂。。我感觉 RSC 这个东西(我没看过文档,根据感受瞎猜一下 hh )就是把以前应该在客户端的操作挪到服务端,由服务端做好之后,直接把结果( render 好的服务端组件)发回给客户端。而且最重要的是它可以和 客户端组件 配合,客户端组件变化的时候可以把变化传回给服务端,然后服务端组件 render 之后再返回给客户端,隐藏了数据的处理。至于在客户端的服务端组件,好像服务端组件,如果在客户端就无法使用很多客户端才能提供的东西,比如 cookies 。

静态导出应该是整个网站都是静态站吧,每次想要更新网站的内容,只能 build ,而静态渲染因为有 revalidation ,所以不一定要 build 。

在没有 ppr 之前,路线只有静态渲染,动态渲染,流式渲染

静态渲染是在构建时和 revalidation 时渲染一次。

动态渲染是 request 请求发过来就渲染一次。

流式渲染是哪个部分先渲染好了,就先发过去哪个部分,让 HTML 不间断地更新,ready ,实现方法是分别在一个 <Suspense> 中包裹每个 RSC ,并且把 data fetching 扔到每个 RSC 内自己做,自己管理自己是否 ready 。

---

另外,你说的这个:

> 好在官方可能考虑过这一点了,如果你在 unstable_cache 缓存的函数中声明 noStore,那么在 nextjs 看来,组件仍然是可以被 ppr 的,不会受到 nosStore 和 revalidate0 影响了

我去 canary with ppr 测试了:

会报错的组合:

1. 有 cache

noStore: 在 cache 内 or 完全没有

2. 无 cache

noStore: 没有

不报错的组合:

1. 有 cache

noStore: 在 cache 外,在 cache 前(**这里就是 PPR 动态组件使用 cache !**,这时候可以使用 Suspense 在 PPR 做 Stream 其他动态的内容,我在 revenue 那里使用 noStore 再使用 unstable_cache 缓存了 api 返回值,然后又使用 Suspense 包裹了一个空调了一下 revenue API 的组件,发现 revenue API 每次刷新只被调了一次,说明 PPR 里的缓存起效了!)

2. 无 cache

noStore: 在 API 方法内外均可,只要在 API 方法里 try catch 之前即可。

ppr 下,有 revalidate 0 ,next 会不知道我们想要什么( next: 为什么你使用了 revalidate 0 却不显示声明 noStore 啊啊啊迷惑死,大概它是这样的心理 = =),因此必须在 cache 外(如果有 cache )使用 noStore ,明确告诉要使用 ppr 。

因此我认为,应该要在 cache 语句之前声明 noStore 才有用,在 cache 内部使用 noStore 没用,因这样好像影响不到 cache callback 的外部的组件。

---

排版问题,确实,v2 为啥不在回复里应用 markdown 引擎。。应该不麻烦吧这个。。难道就是鼓励简短的无结构化的回复??
epiloguess
266 天前
@lazyczx 你可能需要 update 一下你的 canary 到 50 版本

这样的话,在 unstable_cache 缓存的函数中使用 noStore ,内容就会被静态渲染了,你去开发者工具里查看第一个接受的 document 就能看到

官方是没有动态组件/dynamic component 这个概念的,抱歉误导了你,我用这个词指的是那些在 ppr 中需要动态渲染的组件

我在整理这个帖子的内容,稍微回复你这个回复的其他内容
lazyczx
266 天前
@epiloguess

> 这样的话,在 unstable_cache 缓存的函数中使用 noStore ,内容就会被静态渲染了,你去开发者工具里查看第一个接受的 document 就能看到

这个是在 PPR 开启的情况下吗?

你的意思是没有使用 "unstable_cache 前的 noStore()" 的情况下吗?

那就是变成我说的会报错的 组合 1 了。

我刚刚升级到 50 版本 ( from 49 )测试了,还是会报错呀。

如果你说的是 ppr 未开启的情况下,那肯定是会开启静态渲染了,因为 cache 里的 noStore 似乎没有直接作用于 next route (不知道我的表述对不对)。

使用 unstable_cache 的话,也没有在 unstable_cache 前使用 noStore ,在 PPR 下会直接报错( validate 0 和 unstable_cache 的冲突);不在 PPR 下的话,本来就会静态渲染呀,我好像没有否认过这一点。

和这点有关的我先前的回复:

> 我刚刚去把版本换成 canary without ppr 测试了一下,我用了 noStore 在 API 方法里,然后使用 cache 把 API 包裹起来,生成树还是静态的(注意我没有使用你上面回答里的推荐做法,即把 noStore 放在组件里,放在 cache 方法上方,不知道是不是这里有误解。。)。
epiloguess
266 天前
@lazyczx

我发现一些奇怪的事情,我的评价是远离 canary...

以下为一些发现,都指的是 ppr 开启的情况下
---

###第一个 bug.
先不考虑 unstable_cache 。
1. noStore 的位置没有影响。
只要 suspense 边界内有 noStore,这个组件就不会被预渲染。
在没有 unstable_cache,开启 ppr,路由会被部分渲染,从 dashboard 上面那个标题就能看出来

2.db 操作不是 revalidate 0 ,这一点跟原来想得不一样

在没有 unstable_cache,没有 noStore 的情况下,开不开 ppr,路由都会被渲染成静态的

3.非常非常有意思的一点,
假如现在有两个组件,第一个组件满足条件 1(只有 noStore),第二个组件满足条件 2(没有 noStore,没有 unstable_cache)

然后,会报错!!!直接黑人问号???

这时候你先后注释掉一个组件,你会发现,每一个都可以 build,不是 static,就是 partial,合一起就不行了。

我直接 hhhhhhhhhhhhhhh,要疯了
这个 bug 直接误导我们 db 操作是 revalidate0

---

### 第二个 bug
在第一个 bug 的基础上,我们想探索出,如何让两者共存
目标是,两个 db 查询的组件,一个是动态渲染的,一个是构建时渲染的
动态渲染的那个同样先不考虑 unstable_cache,因为那属于优化体验的事情.
直接考虑第二个组件,目前是什么都没有,没有 cache,noStore
这时候我们走进了第二个坑,准确来说就我一个
> 在 unstable_cache 内使用 unstable_noStore 不会选择退出静态生成。相反,它将根据缓存配置来确定是否缓存结果。
接着我们尝试在第二个组件中,在 unstable_cache 缓存的函数内使用 noStore,我说行,你说不行,那到底行不行?
答案是,有时候行,有时候不行

如果你意外让你的本地缓存里出现了你想要缓存的数据,那就行.

首先,删掉你的.next,然后 build,直接 errrrrrrrrrrrrrooor(感觉自己有点精神不稳定了)

第二部,注释掉 ppr,删掉.next(如果有),然后 build 一下,访问一下 dashboard 不出意外,第一个组件是动态渲染的,第二个组件是秒加载的,
值得注意的是,这时候 dashboard 路线是动态的,第二个组件在 build 的时候被执行过一次,得益于 unstable_cache(谢谢你啊我的 cache!!!!),你第二次(你以为你是第一次,实际不是)访问的时候,数据是秒加载的


这一点很容易验证,你在第二个组件首行加上终端输出就会看到,每次刷新都会有终端输出


最后,加上 ppr,然后 build,成功了????恩?恩?



---
一些多余的总结,包括你已经知道了的,
在 ppr 中,noStore 用来声明动态渲染组件,unstable_cache 可以用来缓存函数返回值,降低数据库压力
在 ppr 中,在 unstable_cache 中使用 noStore 目前会报错,应该在 cache 外使用


> 在 unstable_cache 内使用 unstable_noStore 不会选择退出静态生成。相反,它将根据缓存配置来确定是否缓存结果。
这句话好像只对非 ppr 有效

非 ppr 时,在 unstable_cache 中使用 noStore 确实不会退出静态生成
比如说,我们把第一个组件注释掉,在第二个组件中如此使用,生成的是 static 的
不过,为什么要在 unstable_cache 中使用 nostore 呢?明明不使用 unstable_cache,和 unstable_noStore,一样可以生成 static 啊(终端不会再有东西蹦出来了)


梳理一下应对方案和注意事项
假设你正在构造一个 ppr 的页面,除非你想让页面完全 static,那你所有的 db 组件可以什么都不用管,(不过那你还要 ppr 干啥呢)
就算你只有一个想动态渲染组件,这个组件也应该加上 noStore,不然它会 static

现在似乎没有办法在开启 ppr 的时候,只静态渲染某个 db 组件
---

最后的最后

我已经不想思考为什么了,好在我错的够多,聪明如我,已经在之前给出过答案了.

> 我的建议是 unstable_cache 梭哈,不设置过期时间,算是一种半静态吧,除了第一次比较慢,后面其他用户第二次访问就很快了

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1028274

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX