Nuxt3项目实战中的性能优化与SSR避坑指南

Air-梦媛 框架 阅读 1,248
赞 14 收藏
二维码
手机扫码查看
反馈

优化前:卡得不行

项目上线三个月,数据一拉,Lighthouse 跑分移动端才 32。首屏加载时间平均 5.6 秒,部分低端安卓机甚至干到 8 秒以上。用户进来点两下就跳出,我看着都心疼。这个 Nuxt 2 的老项目,当初图快,用 generate 打包静态页,现在页面一多,build 时间直接破十分钟,部署也成了心理阴影。

Nuxt3项目实战中的性能优化与SSR避坑指南

最离谱的是首页——一个看起来挺简单的资讯聚合页,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 提供了很多能力,但默认配置往往是“通用但不极致”。真正要爽,还得自己动手调。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论