开发了1年Next App Router项目,我劝你不要用App Router

0 阅读7分钟

我开发了接近1年的月访问量800万+的境外电商项目,踩了很多坑,挑几个讲一下。

使用的版本是v14.2.3,在这个版本里React.cache默认对get请求生效,Next Fetch Cache默认自动缓存。

我的技术很一般,如果大佬有解决方案,还望不吝赐教!是真的很想知道解决这些问题的正确方案是什么。

第一个坑,全局数据怎么存

要保证的第一个需求就是,用户切换了产品以后,之前的选项还在,比如数量、颜色等等。

比如现在的路由结构是这样的:

├── pdp
│   ├── [slugName]
│   │   └── [productId]
│   │       ├── [skuId]
│   │       │   ├── layout.tsx
│   │       │   └── page.tsx
│   │       ├── layout.tsx
│   │       └── page.tsx

按照以前Page Router的SSR的理解,在当前Sku的layout下包一个Provider就行了。但是在App Router里完全不行,一旦sku发生改变,[skuId]这一层全更新了,状态也保留不了,必须将状态上移到[productId]这一层,只有不变的这一层layout才不会更新。

如果你的路由结构没多个嵌套,比如你只有一层[productId],那一切换product,全局状态就丢了,只能再上移,问题有些时候不能再上移了,比如你是[...slug]这种动态路由,我是存localstorage里自己判断解决的这个问题。

还有一个问题就是在[productId]这一层你是拿不到[skuId]

第二个坑,server端数据如何跨组件共享

client component跨组件共享值可以用React Context,server component更新获取数据的那一层server怎么共享值?也没有找到官方的解决方案,官方不外乎都是数据共享,比如在多个组件里请求相同的API接口,因为按照官方逻辑同一个树渲染里是有React.cache缓存的,所以不会重复请求。但是如果你要对数据做一些处理怎么办,不可能每个请求的组件里都处理吧?

我找到2种方案:

  1. 将处理值的方法和请求方法合成一个方法,再用React.cache包一次。
  2. React.cache的原理做一个类似于React Context的包,我写了一个

第一种的方法是略显的不那么React,没有那种从上至下的数据流感觉。

第二种方法的问题是,如果客户端的server component更新时,获取数据的那一层server component没有更新,那么到客户端里的东西都是undefined,这个也好理解,数据是由最上层的server component存到React.cache里的,而这个cache只在同一棵树的渲染过程中有效,当后续server component更新,之前存有数据的cache已经消失了。

API重复请求

最好笑的是Next官方推App Router就说性能各种好了,结果同一个API会向服务端请求2次,“请随意fetch,缓存交给我”,结果最基础的功能有问题,会消耗双倍资源。

GitHub Issue,v15正式版应该修复了吧,我还没验证。

内存泄漏

服务器图表内存一直是锯齿形,不得不设置自动重启,找问题找了很久,代码都快注释完了,我也想不到是Nodejs的问题。

最后另一个同事电脑无法复现,原来是他Node没升级,还是装的20.12,而我们和服务器上都装的20.17,最后Node也不敢升级了,把Docker里版本锁到20.12,暂时解决这个问题,为什么叫暂时解决,其实还是泄漏,只是泄漏的很慢,以前可能半个小时服务器就得重启,现在是24小时重启。

这个问题是由Node里的fetch实现undici导致的,不用Next我是抠破头也想不通Node会导致Next坏掉。

这是undici相关PR,在上个月的Node v20.18里更新了依赖undici版本,实测确实修复了。

Connect Timeout 连接超时

在New relic里后台日志,各种UND_ERR_CONNECT_TIMEOUT,抠破头也找不到为什么,接口很随机,并不是接口问题,甚至第三方CMS接口都会出现这个问题,找了半天也没找出结果,就想会不会又是Node坏掉了,结果还真又是Node的问题,在CPU密集型任务时触发(Next在SSR占用大量CPU),这是undici相关PR

这个PR在上周的Node v20.18.1里更新,感谢Next,让我无比关心Node的版本更新。

CDN问题

CDN穿透率一直很高,但是没有去研究,这两天是另外一个项目组的App Router项目暴出来的问题,才发现Next实现的server component根本CDN根本不好缓存,我先说如果是Page Router是什么行为。

Page Router

/a => /b
a页面跳转b页面,实际拿到的是b页面数据,请求到了b-content.json,通过json数据更新页面。

/c => /b
c页面跳转b页面,实际拿到的是b页面数据,请求到了b-content.json,通过json数据更新页面。

这种情况CDN很好缓存,只用缓存b-content.json这个文件就行了。

到App Router是什么情况

/a => /b
a页面跳转b页面,发rsc请求,/b?_rsc=4yi2o

/c => /b
c页面跳转b页面,发rsc请求,/b?_rsc=vq4k7

关键来了,从不同的路径跳相同页面,请求的参数不一样,这个不只表现在参数上,连请求头里的参数都不一样,这个行为导致CDN根本无法缓存页面数据,如果从10个不同的页面跳/b页面,那么就有10种/b的数据。

我可以理解Next是要需要判断layout是否需要更新,问题是我们站点有几千个产品,这个排列组合一下这数量谁受的了啊,为什么Next不能返回整个页面全量的server component payload,再由客户端判断?

现在只能通过魔改Next源码,让它发一样的rsc=id和请求头内容,这样CDN才能对一个页面进行缓存了,还不知道有没有问题。这个项目组只有一个server component就是layout来请求数据,其他全是client component,Next也要更新rsc。

相关discussion,这人10万个产品,不知道他们怎么解决的。

CDN缓存过期后,页面始终是老版本。

Next的Revalidation机制也很奇怪,如下图所示,当一个用户请求过来,发现数据过期了,它不是同步验证返回新数据的页面,而是先返回老数据页面然后异步更新数据,等下下个用户访问的时候再拿到新数据。

image.png

这个导致的问题是什么呢?

Next服务器设置的5分钟重新校验,现在用户第一次请求/a这个路径,到达服务器后,服务器返回页面,页面被CDN缓存10分钟。

在这10分钟内,/a的请求都到了CDN,10分钟后CDN缓存失效,到达Next服务器,Next服务器发现数据过期,返回老数据异步更新数据,CDN又缓存老数据10分钟。

唯一解决这个问题的方法就是把Next服务器的Revalidation关了,关了以后任何请求都是重新请求API,再结合上面的问题细品。

不能设Next缓存不然CDN有老数据 ⬇️

CDN穿透请求到Next服务器 ⬇️

Next发2次请求双倍消耗 ⬇️

_rsc没法缓存又到Next服务器 ⬇️

Next服务器不能设置缓存因为CDN会有老数据 ⬇️

真蚌埠住了啊。

总结

如果大佬有解决方案,还望不吝赐教

要我说,如果是小项目,随便用,反正也吃不了什么资源,如果是电商项目,建议别用,保不准又有什么问题。

server component确实方便了一些,但是带来的代价还不可估量。

如果商业项目硬要用,建议用Vercel部署,我们的k8s上只有几十个pod,但是Vercel serverless function可以并发到10万+个,我都怀疑它们是不是采取的空间换时间策略,而且它们的缓存还是服务器之间共享的。