Nuxt3项目实战中的性能优化与SSR避坑指南
优化前:卡得不行
项目上线三个月,数据一拉,Lighthouse 跑分移动端才 32。首屏加载时间平均 5.6 秒,部分低端安卓机甚至干到 8 秒以上。用户进来点两下就跳出,我看着都心疼。这个 Nuxt 2 的老项目,当初图快,用 generate 打包静态页,现在页面一多,build 时间直接破十分钟,部署也成了心理阴影。
最离谱的是首页——一个看起来挺简单的资讯聚合页,hydrate 完成后整整卡顿 1.2 秒,页面假死。滚动延迟、按钮点击没反应,体验差到我自己都不想用。
找到瘼颈了!
先上 Chrome DevTools 的 Performance 面板跑了一圈,发现两个大问题:
- JS 解析和执行占了主主线程将近 4 秒
- 首屏 hydration 过程中,Vue 在 diff 一堆根本不需要的服务端组件
然后开 Lighthouse,重点看 “Reduce JavaScript payloads” 和 “Avoid enormous network payloads” 这两项。果然,vendor.js 直接干到 1.8MB(gzip 后还有 420KB),首页加载了十几个不相关的页面组件,比如 /admin/user-list 和 /settings/billing —— 这些根本不该出现在首页的 chunk 里。
再查 network,发现 API 请求是串行的:layout 获取用户信息 → page 再发内容请求。等得人抓狂。而且所有接口都没有缓存策略,刷新一次打一次。
动刀:代码分割 + 异步组件
第一个动手点就是组件懒加载。虽然 Nuxt 支持自动 code splitting,但默认只按页面拆。我们的 _slug.vue 页面里引入了一堆复杂组件,比如评论区、推荐流、打赏模块,全都是同步 import 的。
改法很简单,但效果惊人:
// 优化前:全量引入
import CommentSection from '~/components/CommentSection.vue'
import DonationModal from '~/components/DonationModal.vue'
import RelatedPosts from '~/components/RelatedPosts.vue'
export default {
components: {
CommentSection,
DonationModal,
RelatedPosts
}
}
// 优化后:异步加载
export default {
components: {
CommentSection: () => import('~/components/CommentSection.vue'),
DonationModal: () => import('~/components/DonationModal.vue'),
RelatedPosts: () => import('~/components/RelatedPosts.vue')
}
}
这一改,首页 JS 体积直接砍掉 35%。而且这些组件只有在用户滑到对应区域或点击按钮时才加载,体验顺滑多了。
这里注意我踩过好几次坑:如果组件有 SSR 上下文依赖(比如需要服务端预取数据),记得在 nuxt.config.js 里加 transpile:
// nuxt.config.js
build: {
transpile: [
'some-external-component-library'
]
}
接口合并 + 缓存策略
原来首页要打三个 API:用户信息、文章详情、推荐列表。每个都独立 await,导致瀑布流请求。后来我把它们合并成一个聚合接口,省了两次 TCP 握手。
同时给接口加上合理的缓存头:
// plugins/api.js
const api = $axios.create({
baseURL: 'https://jztheme.com/api'
})
// 对特定 GET 请求启用内存缓存
const cache = new Map()
export const cachedRequest = async (url, options = {}) => {
const key = url + JSON.stringify(options.params)
if (cache.has(key)) {
return cache.get(key)
}
const promise = api.get(url, options)
cache.set(key, promise)
// 5分钟过期
setTimeout(() => cache.delete(key), 300000)
return promise
}
在页面中使用:
async asyncData({ $api }) {
try {
const data = await $api.cachedRequest('/homepage-aggregate', {
params: { slug: 'xxx' }
})
return { ...data }
} catch (err) {
return { articles: [], user: null }
}
}
接口请求数从 3 次降到 1 次,总耗时从 1200ms 降到 400ms 左右。而且弱网环境下优势更明显。
SSR Hydration 优化
这个问题最隐蔽。页面结构和服务端渲染输出明明一致,但 Vue 还是要花时间做 diff。后来发现是因为服务端用了 new Date() 或随机 ID,导致客户端和服务器不一致,被迫 fallback 到 full re-render。
解决方法是在可能产生差异的地方加 v-no-ssr 或确保两端完全一致:
<!-- 不要用 -->
<div>{{ Math.random() }}</div>
<div>{{ Date.now() }}</div>
<!-- 要用 -->
<client-only>
<div>{{ formattedLocalTime }}</div>
</client-only>
另外还关掉了非必要页面的 SSR,在 nuxt.config.js 里配置:
// nuxt.config.js
render: {
ssr: false // 全局关闭 SSR(适用于纯静态站点)
}
或者按页面控制:
// pages/some-heavy-page.vue
export default {
mode: 'spa' // 只对这个页面关闭 SSR
}
这一步让 TTI(Time to Interactive)从 5.1s 降到 2.3s。
图片懒加载 + WebP
首页一堆高清图,全都在 onload 前就发起请求,阻塞主资源。简单粗暴上了懒加载:
<img
v-lazy="https://jztheme.com/uploads/${post.cover}.webp"
:alt="post.title"
width="300"
height="200"
>
配合 vue-lazyload 插件:
// plugins/lazyload.js
import Vue from 'vue'
import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload, {
preLoad: 1.3,
error: '/placeholder.webp',
loading: '/loading-spinner.svg',
attempt: 1
})
再加上 nginx 自动转 webp(Accept 头判断),图片体积平均减少 60%。首屏关键资源竞争明显缓解。
构建层面:Gzip + Preload
nuxt.config.js 加了几行配置:
// nuxt.config.js
export default {
build: {
optimization: {
splitChunks: {
chunks: 'all',
maxSize: 250000, // 250KB 分块上限
}
}
},
render: {
httpCompression: true,
resourceHints: true
},
loading: false // 关掉自带的 loading bar,自己实现骨架屏
}
顺便把 moment.js 换成 dayjs,体积从 260KB 干到 12KB。这种替换属于“改一行,收益巨大”的典型。
优化后:流畅多了
改完之后重新跑 Lighthouse,移动端分数从 32 干到了 78。首屏加载时间压到 1.1 秒左右,TTI 控制在 2.3 秒内。最重要的是用户反馈变少了——没人再说“点不动”了。
当然还有一些小问题:比如低版本 Android 的 font-display 支持不好,导致 FOUT 明显;动态组件 unload 后内存释放不够彻底,长时间驻留会缓慢上涨。但整体已经可以接受了。
性能数据对比
- 首屏加载时间:5.6s → 1.1s (下降 80%)
- vender.js 体积:1.8MB → 890KB(gzip 后 210KB)
- 首页请求数:14 → 6
- Lighthouse PWA 分数:32 → 78
- build 时间:10min → 4min(通过 cache-loader + hard-source-webpack-plugin)
以上是我的优化经验,有更优的实现方式欢迎评论区交流
这个项目不是完美方案,比如没有上 Nuxt 3,也没有搞微前端拆分。但对大多数中小型项目来说,这几招已经够用了。特别是异步组件 + 接口合并 + 图片懒加载,基本是必做的三板斧。
折腾了半天发现,很多时候性能瓶颈不在框架本身,而在我们怎么用它。Nuxt 提供了很多能力,但默认配置往往是“通用但不极致”。真正要爽,还得自己动手调。

暂无评论