无加载不是速度,是连续感
先做一个思想实验。
方案 A:点击链接后,页面白屏 300ms,然后新页面完整出现。
方案 B:点击链接后,立刻显示骨架屏,800ms 后内容填进来。
哪个更快?A。
哪个感觉更快?A。
骨架屏不是为了让页面"看起来在加载",它是在承认"我在加载"。而这个承认本身,就是一种延迟信号。
真正的「无加载」是什么
无加载的意思是:用户从未看到"等待"这个状态。
实现它需要三个条件同时成立:
- 页面在点击之前就已经准备好了
- 切换过程不出现空白帧
- 没有任何 loading 指示器
条件一:提前准备
SSG(静态生成)把这件事做到了极致:所有页面在构建时就变成了 HTML 文件。
点击链接时,浏览器请求的是一个已经存在的静态文件——不是"等服务器查询数据库再渲染"。
但即使如此,仍然有一次网络往返。所以还需要预取:
<!-- Nuxt 的 NuxtLink 默认开启 prefetch -->
<NuxtLink to="/archive">归档</NuxtLink>
Nuxt 会在链接进入视口时预取目标页面的 payload。用户真正点击时,数据已经在内存里了。
关键细节:外链和友链要显式关闭预取,否则会预取一堆无关域名。
<NuxtLink to="/links" no-prefetch>友链</NuxtLink>
条件二:不出现空白帧
即使数据准备好了,页面切换仍然会有一次 DOM 重建。
如果没有过渡,你会看到:旧页面消失 → 空白 → 新页面出现。
那"空白"可能只有 16ms(一帧),但眼睛能看见。
解法是 out-in 模式的页面过渡:
// nuxt.config.ts
app: {
pageTransition: { name: 'page', mode: 'out-in' },
}
.page-enter-active,
.page-leave-active {
transition: opacity 200ms ease-out, transform 200ms ease-out;
}
.page-enter-from {
opacity: 0;
transform: translateY(4px);
}
.page-leave-to {
opacity: 0;
}
200ms 的淡入淡出。旧页面淡出的同时新页面淡入,中间没有空白帧。
为什么是 200ms? 低于 150ms 感知不到,高于 300ms 开始觉得慢。200ms 是"有过渡但不等待"的窗口。
条件三:删掉 loading 指示器
这一条最反直觉。
大部分项目会加一个顶部进度条(NuxtLoadingIndicator):
<!-- 删掉这个 -->
<NuxtLoadingIndicator />
进度条的存在本身就在告诉用户:"可能要等一下"。
当你的页面跳转真的在 200ms 内完成时,进度条会闪一下——比没有更糟。
所以我把 NuxtLoadingIndicator 完全删掉了,靠 view transition 自然过渡。
一个真实的对比
改之前(有骨架屏 + 进度条):
点击 → 进度条出现 → 骨架屏 → 内容填入 → 进度条消失
总时长:约 600ms,用户全程在"等待"
改之后(SSG + 预取 + 过渡):
点击 → 淡出 → 淡入
总时长:约 200ms,用户感知为"瞬间"
什么情况下做不到
诚实地说,这套方案有三个前提:
一、内容必须是静态的。 如果页面需要实时数据(股票、聊天),SSG 帮不上忙。
二、页面数量要可控。 全量预渲染 10 万个页面,构建要几小时。
三、首屏没法这样优化。 用户第一次访问仍然要下载 HTML + CSS + JS。这是物理限制。
对一个个人博客来说,这三个前提全都成立——所以它能做到真正的无加载。
首屏怎么办
首屏的唯一优化方向是减小体积。
我的做法:
- 没有第三方脚本(统计默认关闭)
- 字体分片,实际加载约 180KB
- 搜索索引懒加载(首次搜索才请求)
- 零运行时状态库(没有 Pinia)
结果:首屏 JS 约 41KB(gzip 后)。
一句话
无加载不是把加载藏起来,是让加载根本不需要发生。
这两者的区别在于:前者是障眼法,后者是架构。
评论暂未开放