Vue 3 SPA 首屏优化:从 3s 到 1.2s 的 5 个实践

最近给 sotool.top 做了首屏性能优化,LCP 从 3s+ 降到 1.2s。这篇文章记录 5 个实际有效的优化手段,以及踩过的坑。

背景

sotool 是一个 PDF 工具箱,首页包含 17 个工具入口、FAQ、对比表格、推荐位等,首屏 HTML 超过 30KB。

Vite 打包后:

  • index.html 加载 app.js(约 180KB gzipped)
  • app.js 里引入了 vue、vue-router、pdf-lib、html2pdf.js、mammoth、xlsx 等
  • 首页虽然不直接用到 pdf-lib,但它在 vendor chunk 里
  • 移动端首屏 LCP 经常 3s+

优化 1:手动 chunk 拆分

Vite 默认把所有第三方库打包到一个 chunk 里,首页加载了不需要的代码。

我们配置了 manualChunks:

css 复制代码
// vite.config.ts
build: {
  rollupOptions: {
    output: {
      manualChunks: {
        vendor: ['vue', 'vue-router', 'lucide-vue-next'],
        'pdf-lib': ['pdf-lib'],
        'pdfjs-dist': ['pdfjs-dist'],
        mammoth: ['mammoth'],
        xlsx: ['xlsx'],
        'html2pdf-jspdf': ['html2pdf.js', 'jspdf'],
      },
    },
  },
}

效果: 首页只加载 vendor chunk(约 60KB gzipped),pdf-lib 等库按需加载。

坑点: 拆得太细会导致 HTTP 请求过多。Vite 6 支持 HTTP/2,但移动端 HTTP/2 多路复用不一定生效,chunk 数量控制在 5-8 个比较安全。

优化 2:modulePreload 过滤

Vite 默认会为每个 chunk 生成 <link rel="modulepreload"> 标签。首页的 HTML 里preload了 pdf-lib、html2pdf 等不需要的 chunk。

javascript 复制代码
// vite.config.ts
build: {
  modulePreload: {
    resolveDependencies(filename, deps, { hostId, hostType }) {
      // 首页不 preload 大型 PDF 工具 chunk
      if ((hostType === 'html' && hostId.includes('index')) || hostId.endsWith('index.html')) {
        return deps.filter(dep => !dep.includes('pdf-lib') && !dep.includes('html2pdf'))
      }
      return deps
    },
  },
}

效果: 首页的 preload 请求从 12 个降到 4 个,减少了不必要的网络竞争。

优化 3:路由懒加载

Vue Router 的路由组件全部用 import() 懒加载,首页不加载任何工具页面。

javascript 复制代码
// router/index.ts
{
  path: '/merge',
  name: 'Merge',
  component: () => import('@/views/Merge.vue'),
}

坑点: 首次点击工具时会有 200-500ms 的 chunk 加载延迟。我们加了 loading 状态过渡,用户感知不到。

优化 4:字体加载优化

项目用了 4 个字体包:Plus Jakarta Sans、JetBrains Mono、Instrument Serif。全部通过 @fontsource 引入,每个包 50-200KB。

arduino 复制代码
// main.ts
import '@fontsource-variable/plus-jakarta-sans'
import '@fontsource-variable/jetbrains-mono'
import '@fontsource/instrument-serif/400.css'
import '@fontsource/instrument-serif/400-italic.css'

优化方案: 在 CSS 里用 font-display: swap:

css 复制代码
@font-face {
  font-family: 'PlusJakartaSans';
  src: url('/fonts/plus-jakarta-sans-variable.woff2') format('woff2');
  font-display: swap;
}

swap 策略让文字先用系统字体显示,等自定义字体加载完再替换。比 block(等字体加载完再渲染)快得多。

坑点: 字体替换时可能有闪烁。我们用 document.fonts.ready 做全局字体加载监听,在字体加载完后移除 font-loading class。

javascript 复制代码
document.fonts.ready.then(() => {
  document.documentElement.classList.remove('font-loading')
})

优化 5:首屏内容直出

SPA 的首屏 HTML 是空的,全靠 JS 渲染。这对 LCP 和 SEO 都不好。

我们的做法是在 vite build 后用脚本预渲染首页和核心工具页:

javascript 复制代码
// scripts/prerender.mjs
import { chromium } from 'playwright'
const browser = await chromium.launch()
const page = await browser.newPage()
​
const urls = ['/', '/merge', '/compress', '/split', '/word-to-pdf']
for (const url of urls) {
  await page.goto(`http://localhost:4173${url}`, { waitUntil: 'networkidle' })
  const html = await page.content()
  // 写入静态 HTML 文件
}
await browser.close()

效果: 首页可以直接返回带内容的 HTML,首屏 LCP 直接从 3s 降到 1.2s。

坑点: 预渲染会增加构建时间(每个页面约 2-3s),而且 SSR 框架(如 Nuxt)更适合这个场景。我们这个项目比较小,Playwright 方案够用。

优化前后对比

指标 优化前 优化后 改善
LCP 3.2s 1.2s -62%
首屏 JS 180KB 60KB -67%
preload 请求 12 4 -67%
首屏 HTML 空 30KB 内容 -

总结

这 5 个优化里,手动 chunk 拆分 + modulePreload 过滤 性价比最高,改动小、效果大。字体 font-display: swap 是细节但很容易被忽略。预渲染对 SEO 和 LCP 帮助最大,但维护成本也最高。

如果你也在做类似的 SPA 工具站,建议先从 chunk 拆分开始,然后逐步优化。


工具地址:sotool.top

如果对你有帮助,欢迎点赞收藏,有问题评论区见。

相关推荐
陈随易6 分钟前
副业搞钱点子之独立开发
前端·后端·程序员
console.log('npc')16 分钟前
问题跟踪与知识沉淀:从任务到知识的闭环)
前端·javascript·人工智能
恋猫de小郭34 分钟前
最近 Android 有什么更新:Android Dev Summit 2026 技术解读
android·前端·flutter
计算机魔术师36 分钟前
Reddit 宣布转向以登录为主的体验,以应对 AI 搜索带来的流量下滑
前端
熊猫钓鱼>_>36 分钟前
上架审核常见驳回与隐私合规清单:过审前先自查这 8 条【鸿蒙心迹】
开发语言·前端·华为·华为云·harmonyos·openharmony·ai制作
missile11137 分钟前
# HDR 算法 ISP 通路客制化方案(G100 / mt6789)
java·前端·javascript
全栈陈序员1 小时前
【Python】基础语法入门(三十)—— Web 开发入门:用 Flask 搭建第一个网站
前端·python·flask
万少1 小时前
Harmony 7.0 这个skill帮你自动化操作AGC
前端·javascript·后端
小大宇1 小时前
Vue知识点-ElementPlus-02图标
前端·javascript·vue.js
简简单单就是我_hehe1 小时前
一次 ClickHouse 服务器磁盘告警的排查与修复实录
java·服务器·前端