先说结论:这次看起来像两个问题,其实最后都指向同一个根因。
真正的根因是:详情页用了 Vant 的 showToast,它会把 Vant 的全局 reset CSS 带进来。
进入详情页后,这份 Vant CSS 会留在当前 SPA 会话里。再返回首页时,首页已经处在“加载过 Vant reset”的环境里,所以和“直接刷新进入首页”的样式不一样。
最后临时处理是:先注释掉详情页复制代码成功/失败时的 showToast,页面就不抖了。
现象
本地 pnpm dev 看起来没问题。
但部署到 Vercel 后,直接刷新博客首页,会看到页面布局短暂跳动一下。
一开始看到的现象有两类,看起来像两个不同的问题:
现象 1:刷新首页会抖一下
- 本地不明显
- Vercel 上刷新
/blog更容易看到 - 关掉文章列表后就不抖
现象 2:详情页返回首页后,首页样式和刷新进入时不一样
- 从
/blog点进详情页 - 在详情页点“全部文章”返回
- 左侧标签栏的
li高度从大约 34px 变成 30px
一开始很容易怀疑是 SSR 和客户端水合不一致。
第一轮误判:以为是 NuxtLink 预取
先从最容易造成水合差异的地方排:
- 关掉日签,因为它依赖日期。
- 关掉日历,因为它也依赖当前日期。
- 关掉左侧标签和归档侧栏。
- 关掉文章列表。
结果发现:
文章列表一关,刷新就不抖了。
于是继续缩小范围:
- 换成纯文本标题列表:不抖
- 换回卡片组件:会抖
- 不用组件,直接在页面里写同样的卡片 DOM:还会抖
- 去掉卡片外框、阴影、padding:还是会抖
- 把标题里的
NuxtLink换成普通span:不抖 - 把
span换成普通<a>:不抖 - 换回
NuxtLink,但加no-prefetch:不抖
这一步很容易让人以为:问题就是 NuxtLink 的预取。
NuxtLink 默认会为页面跳转做优化。
当链接进入视口附近时,Nuxt 可能提前预取目标页面需要的资源,这样用户点击时会更快。
这本来是好事。
但博客首页有一个特点:首屏一下子出现很多文章链接、标签链接、归档链接。
页面刚刷新时,这些链接同时进入视口,预取逻辑也会跟着启动。
在本地开发环境里,资源都在本机,加载很快,不一定看得出来。
到了 Vercel 线上,网络、缓存、资源 chunk 加载顺序都更真实,页面首屏渲染后又触发一批预取请求,就可能造成短暂的布局变化或视觉抖动。
所以这个问题不是“NuxtLink 不能用”,而是:
首屏批量链接不一定都适合默认预取。
当时我也按这个方向处理过:给列表页这些链接加 no-prefetch。
但后面继续验证发现,把 no-prefetch 去掉,只要注释掉详情页的 showToast,页面也不会抖。
所以 NuxtLink 预取不是最终根因。
它更像是排查过程里的一个干扰项:预取会改变资源加载时机,但真正造成样式变化的是被加载进来的那份全局 CSS。
真正根因:Vant reset 改了全局基础样式
继续查第二个现象:刷新进入首页和“详情页返回首页”不是同一个样式状态。
固定复现路径是:
- 刷新进入
/blog - 点击一篇文章进入详情页
- 在详情页点击“全部文章”返回
- 首页左侧标签栏高度和刚刷新进入时不一样
更细一点看,左侧标签栏的 li 高度会从 34px 变成 30px。
继续查资源差异,发现详情页里复制代码按钮用了 Vant 的 showToast:
await navigator.clipboard.writeText(codeEl.textContent ?? '')
showToast('已复制')
项目里配置了 @vant/nuxt:
modules: ['@vant/nuxt', '@nuxt/content']
所以 showToast 会被自动导入。详情页用到它以后,构建出来的详情页资源里就会包含 Vant Toast 相关 CSS。
Vant 的样式里有一些全局 reset,大概像这样:
body {
font-family: var(--van-base-font);
margin: 0;
}
a {
text-decoration: none;
}
ol,
ul {
list-style: none;
margin: 0;
padding: 0;
}
这些 CSS 一旦在 SPA 会话里加载进来,返回首页时不会自动卸载。
于是就出现了两个状态:
- 直接刷新进入首页:还没加载 Vant reset
- 从详情页返回首页:已经加载过 Vant reset
左侧标签链接本身没有写死 line-height,所以当 body 字体从浏览器默认字体变成 Vant 的 --van-base-font 后,行盒高度跟着变了。
这就是为什么同一个标签,刷新时看着是 34px,返回后变成 30px。
这个发现也解释了前面的首页刷新抖动:
只要详情页相关资源被提前加载,或者进入过详情页,Vant reset 就可能改变当前会话里的全局基础样式。看起来像是列表、卡片、NuxtLink 在抖,实际是底层的字体和 reset 变了。
最后怎么改
最终先做最小改动:注释掉详情页复制代码里的 Vant Toast。
await navigator.clipboard.writeText(codeEl.textContent ?? '')
// showToast('已复制')
这样详情页就不会因为复制提示把 Vant 全局 reset 带进来,返回首页时左侧标签栏高度也不会再变。
后面如果还想保留“复制成功”提示,更适合自己写一个很轻的本地 toast,样式放在详情页自己的 scoped style 里,不再引入 Vant。
这次学到什么
这次最容易误判的地方是:no-prefetch 确实能改变现象,所以它看起来像修复。
但它只是改变了资源加载时机,没有解释为什么样式会变。
真正要看的不是“哪个操作触发了变化”,而是“变化发生后多了什么资源”。
这次多出来的是 Vant 的全局 CSS reset。
它不是只影响详情页,而是影响整个 SPA 会话。
所以以后遇到这种“刷新进入”和“从别的页面返回”样式不一致的问题,要优先查:
- 返回之前的页面有没有加载新的 CSS chunk
- 新 CSS 里有没有
body、html、a、ul这种全局选择器 - 有没有组件库的 reset/base style 被按需带进来
- 当前页面有没有依赖默认字体、默认行高、默认列表样式
这比单纯盯着当前页面的模板和 scoped style 更容易找到根因。
评论区