面试官:你们 Nuxt 项目 SEO 怎么做的?
我:服务端同步请求,保证爬虫能拿到完整 HTML。
面试官:那首屏加载卡顿怎么办?
我:🤯……
SSR 渲染的"鱼与熊掌"之困:
| 方案 | SEO | 用户体验 |
|---|---|---|
| ✅ 异步请求 | ❌ 爬虫只看到 JS 代码 | ✅ 页面秒开 |
| ✅ 同步请求 | ✅ 完整 HTML 给爬虫 | ❌ 用户卡到怀疑人生 |
有没有一种方式:搜索引擎来的时候装乖孩子(同步),用户来的时候当个灵活胖子(异步)?
当然有!而且 Nuxt 3 自带解决方案,一行逻辑就能搞定 👇
🔑 核心武器:import.meta.server + nuxtApp.isHydrating
要实现"看人下菜碟",关键在于判断当前代码在谁的地盘上执行。
❌ 为什么不能用路由 from 判断?
很多同学第一反应:用 route.query.from 判断是不是从站内跳转来的。
天真了! 爬虫(Googlebot、百度蜘蛛)访问页面时,压根没有"上一页"的概念,它们就是直接访问。用 from 判断,爬虫会直接被归到"首屏用户"那类,结果你还是区分不了。
所以,我们要判断的不是"有没有上一页",而是 "代码此刻在服务端还是客户端" 。
✅ 正确姿势:一行代码定乾坤
const shouldWait = import.meta.server || nuxtApp.isHydrating;
这句代码就是整篇文章的灵魂,我们来拆解一下:
import.meta.server:Vite 注入的编译时标记。代码在 Node.js 服务端跑时为true,在浏览器跑时为falsenuxtApp.isHydrating:Nuxt 提供的运行时标记。客户端首屏注水(Hydration)阶段为true,SPA 跳转后为false
🚀 完整实现:智能同步/异步切换
<script setup>
const route = useRoute()
const nuxtApp = useNuxtApp()
// 🎯 核心判断逻辑
const shouldWait = import.meta.server || nuxtApp.isHydrating
const { data, pending } = await useAsyncData(
`data-${route.path}`,
() => $fetch('/api/your-endpoint'),
{
// 爬虫/首屏 → 阻塞等待(同步)
// 客户端跳转 → 不阻塞(异步)
lazy: !shouldWait,
// 确保服务端一定会执行这段代码
server: true,
}
)
</script>
<template>
<div>
<div v-if="pending" class="skeleton">
⏳ 加载中...
</div>
<div v-else>
<h1>{{ data.title }}</h1>
<p>{{ data.content }}</p>
</div>
</div>
</template>
🔬 两种场景模拟
场景 A:搜索引擎爬虫来访 🕷️
爬虫请求 /article/123
↓
Nuxt 在 Node.js 环境运行
↓
import.meta.server === true
↓
shouldWait === true → lazy === false
↓
⏸️ 阻塞等待 API 返回数据
↓
📦 渲染完整 HTML
↓
🚀 发送给爬虫(内含真实数据,SEO 满分!)
场景 B:用户点击链接跳转 🏃
用户点击 <NuxtLink to="/article/456">
↓
客户端路由切换,不经过服务端
↓
import.meta.server === false
nuxtApp.isHydrating === false
↓
shouldWait === false → lazy === true
↓
⚡ 页面立即跳转,不等待 API
↓
🔄 后台异步请求数据,pending 状态下显示骨架屏
↓
📊 数据返回,更新视图(丝滑体验!)
⚠️ 避坑指南:小心 Payload 导致 Hydration 报错
这是最容易翻车的地方,90% 的 Nuxt SSR 报错都跟它有关。
原理
Nuxt 在服务端获取数据后,会序列化到 HTML 中:
<script id="__NUXT_DATA__" type="application/json">
{ "data": { "title": "文章标题" } }
</script>
客户端注水时,Nuxt 会从 __NUXT_DATA__ 中读取数据,不会重新请求。
什么时候会翻车?
// ❌ 错误写法
const { data } = await useAsyncData('key', () => $fetch('/api'), {
server: false, // 服务端不跑 → HTML 里没有 payload
lazy: true, // 客户端异步请求
})
结果:客户端注水时,发现 data 是 null,但模板期望有数据 → Hydration Mismatch ❌
正确姿势
// ✅ 正确写法
const { data } = await useAsyncData('key', () => $fetch('/api'), {
server: true, // 服务端执行,payload 写入 HTML
lazy: !import.meta.server, // 仅服务端阻塞
})
🧩 封装成组合式函数(推荐)
每次写一堆判断太累了,直接封装成可复用的 useSmartFetch:
// composables/useSmartFetch.ts
export function useSmartFetch<T>(
key: string,
url: string,
options?: { transform?: (data: any) => T }
) {
const nuxtApp = useNuxtApp()
// 如果在 hydrating 且 payload 已有数据,无需再次阻塞
const hasPayload = !!nuxtApp.payload.data[key]
const shouldBlock = import.meta.server || (nuxtApp.isHydrating && !hasPayload)
return useAsyncData<T>(
key,
() => $fetch(url),
{
lazy: !shouldBlock,
server: true,
...options,
}
)
}
使用:
<script setup>
const { data, pending } = await useSmartFetch('articles', '/api/articles')
</script>
清爽!一行代码搞定智能同步/异步切换 🎉
📊 场景决策矩阵
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 🏠 首页/着陆页 | 强制同步 lazy: false | SEO 是第一优先级 |
| 📄 内容详情页 | 智能判断(本文方案) | 爬虫和用户都要照顾 |
| 🔍 搜索列表页 | 智能判断 | 平衡体验 |
| 👤 用户中心 | 全异步 lazy: true | 不需要 SEO |
| 🛠️ 后台管理 | 全异步 lazy: true | 不需要 SEO |
💎 总结
Nuxt 3 处理 SEO + 性能平衡的最佳实践就三句话:
- 用
import.meta.server判断服务端 → 爬虫来了乖乖同步 - 用
nuxtApp.isHydrating判断首屏 → 用户首次访问也不拉胯 - 处理好 Payload 防 Hydration 报错 → 服务端必须
server: true