编程 Vue3 SPA 首屏优化记录:5 步把 LCP 从 3.2s 压到 1.2s

2026-09-03 21:31:48

Vue3 SPA 首屏优化记录:5 步把 LCP 从 3.2s 压到 1.2s

sotool.top 是一个 PDF 工具箱,首页包含 17 个工具入口、FAQ、对比表格、推荐位等,首屏 HTML 超过 30KB。Vite 打包后,index.html 加载 app.js(约 180KB gzipped),其中引入了 vue、vue-router、pdf-lib、html2pdf.js、mammoth、xlsx 等库。首页虽然用不到 pdf-lib,但它被打进 vendor chunk,移动端首屏 LCP 经常在 3s 以上。

做了一轮首屏优化,LCP 从 3.2s 降到 1.2s,记录一下每一步的做法和踩到的坑。

优化 1:手动拆分 chunk

Vite 默认把所有第三方依赖打进同一个 chunk,首页不得不加载用不到的库。通过 manualChunks 按库拆分:

// 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 等库按需加载。首页首屏 JS 从 180KB 降到 60KB。

坑点:chunk 拆得太细会增加 HTTP 请求数。虽然 Vite 6 支持 HTTP/2,但移动端 HTTP/2 多路复用不一定生效,chunk 数量控制在 5-8 个更稳妥。

优化 2:过滤 modulePreload

Vite 默认会给每个 chunk 生成 `` 标签,导致首页 HTML 里 preload 了 pdf-lib、html2pdf 这些当前页面根本不会用到的 chunk。通过 modulePreload 钩子过滤掉:

// vite.config.ts
build: {
modulePreload: {
resolveDependencies(filename, deps, { hostId, hostType }) {
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() 动态加载,首页不加载任何工具页面:

{ path: '/merge', name: 'Merge', component: () => import('@/views/Merge.vue') }

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

优化 4:字体加载优化

项目用了 4 个字体包(Plus Jakarta Sans、JetBrains Mono、Instrument Serif),通过 @fontsource 引入,每个包 50-200KB。给字体加上 font-display: swap

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

swap 让文字先用系统字体渲染,自定义字体加载完后再替换,比 block 对首屏的影响小得多。

坑点:字体替换时可能出现 FOIT 闪烁。可以用 document.fonts.ready 做全局监听,字体加载完成后移除 font-loading class,避免在字体切换过程中闪烁:

document.fonts.ready.then(() => { document.documentElement.classList.remove('font-loading') })

优化 5:预渲染首屏内容

SPA 首屏 HTML 是空的,全靠 JS 客户端渲染,对 LCP 和 SEO 都不友好。做法是在 vite build 后用 Playwright 脚本预渲染首页和几个核心工具页,把渲染后的静态 HTML 写回:

// 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 预渲染性价比更高。

优化前后对比

指标优化前优化后变化
LCP3.2s1.2s-62%
首屏 JS180KB60KB-67%
preload 请求数124-67%
首屏 HTML30KB 内容

总结

5 个优化里,手动 chunk 拆分 + modulePreload 过滤性价比最高,改动量小效果明显;font-display: swap 是个容易忽略的细节;预渲染对 LCP 和 SEO 帮助最大,但后续维护成本也最高。做类似 SPA 工具站,建议从 chunk 拆分入手,再逐步往下优化。

复制全文 生成海报 Vue3 Vite 性能优化 首屏加载 LCP

推荐文章

程序员茄子在线接单