Replies: 5 comments 1 reply
-
Vary Header 相关经验
举个具体例子: 在这种情况下,CDN 会为同一个 URL 维护多个缓存版本: 但是这里有一个重要的问题:默认情况下, 所以,更好的做法是在 middleware 中只保留关键的 cookie: // middleware.ts
export function middleware(request: NextRequest) {
const theme = request.cookies.get('theme')?.value || 'light'
const language = request.cookies.get('language')?.value || 'en'
// 1. 创建一个新的响应
const response = NextResponse.next()
// 2. 设置规范化的 Cookie 头
// 只包含影响页面渲染的 cookie
response.headers.set(
'Cookie',
`theme=${theme}; language=${language}`
)
// 3. 告诉 CDN 基于 Cookie 头缓存
response.headers.set('Vary', 'cookie')
return response
}这样做的好处:
// 不好的例子 - 可能导致过多的缓存变体
Cookie: theme=dark; language=en; sessionId=123; tracking=abc
Cookie: theme=dark; language=en; sessionId=456; tracking=xyz
// 这样会产生两个不同的缓存版本,尽管页面内容是一样的
// 好的例子 - 只关注必要的 cookie
Cookie: theme=dark; language=en
// 不管其他 cookie 如何变化,只要 theme 和 language 相同,就使用同一个缓存版本
// 可以准确计算缓存变体数量
const themeCount = 2 // light, dark
const languageCount = 3 // en, zh, es
const totalVariants = themeCount * languageCount // 6个变体
// 添加缓存状态监控
response.headers.set('X-Cache-Status', 'HIT/MISS')
response.headers.set('X-Cache-Key', `${theme}-${language}`)
// 可以添加调试信息
response.headers.set('X-Cache-Debug', JSON.stringify({
theme,
language,
url: request.url,
timestamp: new Date().toISOString()
}))需要注意的限制:
// 需要确保规范化后的 Cookie 不会太大
const cookieValue = `theme=${theme}; language=${language}`
if (cookieValue.length > 4096) {
console.warn('Cookie value too large')
}
// 确保缓存键的组成部分是确定的
const cacheKey = `${request.url}_${theme}_${language}`.toLowerCase()
// 当主题或语言配置更新时,需要有机制刷新缓存
response.headers.set(
'Cache-Control',
'public, s-maxage=3600, stale-while-revalidate=86400'
)使用建议:
这样的实现可以确保:
|
Beta Was this translation helpful? Give feedback.
-
variant segment rewrite如果要把这个方案做完善。现有的实现存在哪些问题需要重构的?
进展前置重构:
|
Beta Was this translation helpful? Give feedback.
-
相关实践问题合集【500 类错误】 经过 【NextAuth 集成失效】 基本可以确认和环境变量失效是同一类问题,均由于强制静态化后丢失 cookie、headers 等导致集成失效。设定 在 1.52.0 中,我们引入 【首页路由重定向不正确】 在使用 db 的 docker 镜像中,会出现 【环境变量 ( 原本环境变量都通过 RSC 的 props 参数进行传递,页面静态化后,RSC 的 props 将无法动态化,因此环境变量将会全部失效。目前的临时解法是重新将页面变成动态,设定 最终解法是再多一个 client 端请求来获取 serverConfig:( d4b2f36 )。如果是 Vercel 部署场景下,还是可以通过 RSC 来获取最新的FeatureFlag,而 docker 模式下则会做一次请求后进入正确状态。 【发现页 助手详情语种不正确】 【当访问 chat 路径时出现异常 404 响应】 当访问 chat 路径时出现异常 404 响应,具体表现为:
部分客户端(主要集中在Chrome),部分用户出现问题。 找到问题了:宝塔面板反向代理的坑。 宝塔面板的"禁用缓存"不彻底,我直接在反向代理增加了自定义配置才能完全的禁用缓存 refs: #5804 (comment) |
Beta Was this translation helpful? Give feedback.
-
|
目前拿
详细数据: Vercel 部署模式v1.51.3: 平均 2.3s v1.52.11: 平均 160ms Zeabur 部署模式v1.51.3:平均 2.1s v.1.52.9: 平均 215ms 自部署服务器一台 2C8G ,在 HK 的服务器: 纯 IP + 端口模式访问: v1.51.3: 平均 3.7s v1.52.9 不禁用浏览器缓存(这是大家浏览器默认的配置): 平均 73 ms HTTPS 域名 + Cloudflare 小黄云代理访问: v1.51.3:平均 3.3s v1.52.9 禁用浏览器缓存:首次 5.2s ,后续平均 279ms ,全部平均 1.26s 不禁用浏览器缓存: 平均 289 ms |
Beta Was this translation helpful? Give feedback.
-
|
一图胜千言:
|
Beta Was this translation helpful? Give feedback.











Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
背景
本 RFC 讨论如何提升应用的页面打开速度
设计思路
现状回顾
首先回顾下现有的实现。 LobeChat 目前的应用结构,是完全基于 SSR/RSC 的研发范式来的。它的好处有两点:
第一点:首屏感知
SSR 能带来更好的首屏感知,一般 SPA 的首屏最多加个 load 就完事了,但是通过 SSR + 水合的方式,能做到首屏直出应用的完整骨架屏。
以 https://lobechat.com/chat 为例,SSR 首屏直出是这样的:
如果点 devtools 去看的话,首屏返回的 html 直接就是 loading 的样子:
这就意味着当服务端返回 html 时,用户看到的就是已经等待加载数据的样子。而如果换到 SPA 的话,这个骨架屏大概率要等到 JS 加载完之后才能出现,也会遇到俗称的瀑布加载的问题。所以在首屏角度来看,采用 SSR的体验会明显好于纯 SPA。
第二点:按需下发页面资源
在 SSR 之上,新的 RSC 带来了更强的服务端渲染能力,能带来更好的移动端首屏体验(详见:#252 )。
上面这个就是从 SSR Layout 切到 RSC Layout 之后带来的性能提升,因为只用加载一套移动端 layout 的 js ,bundle 一下子就小了很多。而 SSR还是要同时带上 PC 和移动的两份 JS。
选择现有方案的原因已经讲明,那接下来需要回答的问题就是,为什么现在 SSR/RSC 的实现,会比 SPA 要慢这么多?比如我们的一位用户跟我反馈:
经过仔细研究,我发现根因也是 SSR。Next 的 SSR 有两种形式,一种是 Static Render,另一种则是 Dynamic Render,根据 官方文档 中的说明,当应用中有使用以下任何一种方法时,nextjs 都会自动把页面渲染模式切换为 Dynamic Render :
而我们为了移动端 / 桌面端不同页面布局的适配,使用了
headers来获取用户的 ua;为了避免亮暗色主题、多语言、LTR 导致的 FOUC,我们使用了cookies。这就会导致页面默认进入了动态渲染。而从 SPA 时期过来的我们,在做这个方案的时候只考虑用户看到的第一眼的问题(访问速度的问题在本地开发时是完全感知不到的),却不曾想到这种看似良好的体验,早已在暗中标记好了价格。
动态渲染最大的一个缺陷是,必须要回源到服务器做计算生成(nextjs 会在请求层面将 Cache Control 设为不做任何缓存,必须回源),没法复用 CDN 静态缓存,所以带来的问题有两方面:
这就是目前 LobeChat 整体页面打开缓慢的根本原因。我们部署的 Cloud 实例,由于在 Vercel 上,计算耗时相对还好,能控制在 1~2s 左右,而如果用户是自部署的情况,可能就会出现耗时几秒甚至十几秒的情况。
解决思路
其实知道了根本原因,解决的思路就很清晰了:想方法做缓存。
因为根据我们的实际情况来说,不同设备、不同语言只需要算一次 HTML,后续复用这套 html 即可,不需要每次都重新计算。
所以理论上只要针对不同设备、语言、主题提供一套预先渲染好的 html ,只要访问请求命中组合,就直接返回 CDN 缓存的内容,页面就能做到接近秒开。
思路有了,但是具体该怎么做?期间我探索了两种方式,Vary Headers 和 variant path segment 静态化。
Vary Header 方案
一开始想从 CDN cache 角度找方案,自然而然就找到了 Vary Header。但经过一番尝试,发现这方案不太行。
原因是 Nextjs 会在框架层直接覆写 Vary 强制变成:
RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch,导致 Vary 设置失效。社区也有对应的 issue 提出:而后咨询了 Vercel 的 Lee ,他说 Vary 这个方案存在的一个明显的问题是并非所有的 CDN 供应商都遵循 Vary 规范,就会导致缓存不一定真正有效。而是推荐了另外一种他们在 v0 中的实现思路,就是 variant segment rewrite + 静态化。
variant segment rewrite
这个实现静态化的方案非常巧妙,主要通过以下几个关键点实现:
在
middleware中读取 header、cookie 状态并组合成 variant segment,然后加到路由最前面做重写:这样就可以做到最终的请求是:
通过新增一个路由段[variant],然后在这里的 layout.tsx 添加
generateStaticParams函数实现静态路由的参数预生成:RouteVariants.serializeVariants函数将不同的参数组合序列化成字符串生成的路径段例如:
/zhCN_0_light、/en-US_1_dark。通过
RootLayout组件处理动态内容:由于在这个 Layout 中已经没有了 cookie、header 等动态内容,因此页面就可以完全静态化。已经测试了 POC 方案,效果显著:
基本能做到首次打开 1s 以内,第二次打开 500ms 以内。
因此接下来会基于这个方案做更进一步的梳理,争取在 2 月底之前完成上线。
Beta Was this translation helpful? Give feedback.
All reactions