文章目录
-
- 一、引言
- 二、构建优化:让打包更快
-
- [2.1 合理配置浏览器兼容范围](#2.1 合理配置浏览器兼容范围)
- [2.2 关闭 gzip 压缩大小计算](#2.2 关闭 gzip 压缩大小计算)
- [2.3 移除调试语句,减小产物体积](#2.3 移除调试语句,减小产物体积)
- [2.4 优化 CSS 预处理器配置](#2.4 优化 CSS 预处理器配置)
- [2.5 提升本地开发启动速度](#2.5 提升本地开发启动速度)
- [2.6 关闭生产环境 sourcemap](#2.6 关闭生产环境 sourcemap)
- [2.7 optimizeDeps 精细化控制](#2.7 optimizeDeps 精细化控制)
- 三、产物优化:让体积更小、结构更合理
-
- [3.1 使用可视化分析工具](#3.1 使用可视化分析工具)
- [3.2 使用 manualChunks 合理分割 chunk](#3.2 使用 manualChunks 合理分割 chunk)
- [3.3 用轻量依赖替换重型依赖](#3.3 用轻量依赖替换重型依赖)
- [3.4 Tree-Shaking 生效保障](#3.4 Tree-Shaking 生效保障)
- [3.5 超大库 CDN 外链,排除打包](#3.5 超大库 CDN 外链,排除打包)
- [3.6 路由懒加载与组件按需加载](#3.6 路由懒加载与组件按需加载)
- 四、运行时性能:让页面更流畅
-
- [4.1 虚拟滚动处理长列表](#4.1 虚拟滚动处理长列表)
- [4.2 合理使用 computed 与 watch](#4.2 合理使用 computed 与 watch)
- [4.3 避免不必要的响应式](#4.3 避免不必要的响应式)
- [4.4 图片懒加载与资源优化](#4.4 图片懒加载与资源优化)
- [4.5 Web Worker 处理耗时任务](#4.5 Web Worker 处理耗时任务)
- [4.6 合理使用缓存策略](#4.6 合理使用缓存策略)
- [4.7 首屏性能优化](#4.7 首屏性能优化)
- [4.8 内存优化与防止内存泄漏](#4.8 内存优化与防止内存泄漏)
- [4.9 渲染层优化(重排 / 重绘 / 合成)](#4.9 渲染层优化(重排 / 重绘 / 合成))
- [4.10 请求层优化](#4.10 请求层优化)
- [4.11 静态资源深度优化](#4.11 静态资源深度优化)
- [五、CI/CD 流程优化](#五、CI/CD 流程优化)
-
- [5.1 依赖安装提速](#5.1 依赖安装提速)
- [5.2 并行阶段设计](#5.2 并行阶段设计)
- [5.3 减少上传文件数量](#5.3 减少上传文件数量)
- 六、开发效率提升
-
- [6.1 Vite 开发服务器优化](#6.1 Vite 开发服务器优化)
- [6.2 TypeScript 类型检查分离](#6.2 TypeScript 类型检查分离)
- [6.3 路径别名与自动导入](#6.3 路径别名与自动导入)
- 七、线上监控与长效优化闭环
-
- [7.1 接入 RUM 性能监控](#7.1 接入 RUM 性能监控)
- [7.2 资源加载异常监控](#7.2 资源加载异常监控)
- [7.3 JS 错误监控](#7.3 JS 错误监控)
- [7.4 包体积巡检与超大文件拦截](#7.4 包体积巡检与超大文件拦截)
- [7.5 长效优化建议](#7.5 长效优化建议)
- 八、优化总结
- 九、结语
一、引言
前端性能优化是一个系统工程,不能局限于某一个环节。它贯穿于整个研发生命周期:从本地开发体验、CI/CD 构建效率,到用户侧的运行时体验。本文将从构建优化 、产物优化 、运行时性能 、开发效率四个维度展开,结合实际项目案例,给出可落地的优化方案。
二、构建优化:让打包更快
2.1 合理配置浏览器兼容范围
这是对打包速度影响最大的一个因素,也是最容易被忽视的一个。
以 @vitejs/plugin-legacy 为例,常见的错误写法如下:
js
legacy({
targets: ['chrome >= 55', 'chrome < 85', 'edge < 15'],
renderLegacyChunks: true
})
这个写法等价于对所有 chrome >= 55 的版本注入 polyfill,同时还对极古老的 edge < 15 做兼容,导致:
- 编译体积膨胀 --- 大量 polyfill 注入,产物是原来的 3 倍甚至更多
- 构建耗时暴增 --- 旧版兼容处理本地打包从 3.5 分钟飙升到 10 分钟以上
优化方案: 根据真实用户群体配置合理的兼容范围。
js
legacy({
targets: [
'chrome >= 60',
'edge >= 18'
]
})
实测数据:调整后本地 build 时长从 10 分钟下降到 3.5 分钟,产物体积缩小至原来的三分之一。
最佳实践:
- 优先结合数据埋点、用户画像了解实际用户的浏览器分布
- 与产品、测试明确项目需要支持的浏览器版本
- 对于面向新用户群体的新项目,如果目标浏览器版本足够新(如 Chrome >= 80),可以完全移除
@vitejs/plugin-legacy
2.2 关闭 gzip 压缩大小计算
Vite 构建时默认会对输出文件进行 gzip 压缩大小的计算(注意:只是计算,不是真正压缩),这个过程会对每个文件额外跑一遍压缩测算,当文件数量多时会拖慢整体构建速度。
js
// vite.config.ts
export default defineConfig({
build: {
reportCompressedSize: false // 关闭 gzip 大小计算,加快构建
}
})
2.3 移除调试语句,减小产物体积
生产环境应移除 console、debugger 等调试语句,避免无谓的体积占用和信息泄露。
js
// vite.config.ts
const isProduction = command === 'build';
export default defineConfig({
esbuild: {
drop: isProduction ? ['console', 'debugger'] : [],
}
})
2.4 优化 CSS 预处理器配置
项目中常见两类问题:
① 混入了 webpack 专属配置:
js
// 错误写法(extract、sourceMap、loaderOptions 均为 webpack 配置)
css: {
extract: false,
sourceMap: false,
loaderOptions: {
sass: { prependData: '@import "@/assets/style/variables.scss";' }
}
}
② Sass 废弃写法 @import 未迁移:
scss
/* 废弃写法 */
@import "@/assets/style/variables.scss";
/* 推荐写法 */
@use "@/assets/style/variables.scss" as *;
正确的 Vite 写法:
js
css: {
preprocessorOptions: {
scss: {
additionalData: `
@use "@/assets/style/variables.scss" as *;
@use "@/assets/style/mixin.scss" as *;
`,
logger: {
warn(message, options) {
if (options?.deprecation) return;
if (/deprecated|Deprecation/i.test(message)) return;
console.warn(message);
}
}
}
}
}
2.5 提升本地开发启动速度
一个常见的低效写法:
json
// package.json
"dev": "vue-tsc --noEmit && vite --host"
每次 npm run dev 都会先做一次全量类型检查,这个检查只是一次性的,不能替代 IDE 实时提示,却显著拉长了启动时间。
优化方案:
json
"dev": "vite --host",
"type-check": "vue-tsc --noEmit"
日常开发依赖 Vue Official 扩展进行实时类型提示,在提交代码前按需执行 type-check。
2.6 关闭生产环境 sourcemap
生产环境开启 sourcemap 会显著增大产物体积,并拖慢构建速度。除非有线上调试需求,应默认关闭:
js
// vite.config.ts
build: {
sourcemap: false // 生产环境关闭,缩减体积、加快构建
}
如需线上错误定位,可结合 Sentry 等监控平台上传 sourcemap,构建完成后自动删除本地文件,兼顾安全与调试。
2.7 optimizeDeps 精细化控制
include 预构建频繁引用的依赖,减少 HMR 时的重新处理;exclude 排除无需预构建的内部工具或动态脚本,缩短预构建耗时:
js
optimizeDeps: {
include: ['vue', 'vue-router', 'pinia', 'element-plus', 'axios'],
exclude: ['@/utils/custom-sdk'] // 内部工具、动态脚本无需预构建
}
预构建 = 格式转换(ESM 化)+ 依赖合并 + 磁盘缓存,合理配置后冷启动速度显著提升。
三、产物优化:让体积更小、结构更合理
3.1 使用可视化分析工具
在优化产物之前,首先需要看清楚产物的构成 。推荐使用 rollup-plugin-visualizer:
js
import { visualizer } from 'rollup-plugin-visualizer';
// vite.config.ts plugins 中添加
visualizer({
filename: 'dist/stats.html',
template: 'treemap', // 树状图,更直观
gzipSize: true,
brotliSize: true,
open: true, // 构建完自动打开报告
})
通过分析报告可以发现:体积最大的模块是什么、是否存在重复依赖、哪些依赖被不合理地打入了同一个 chunk。
3.2 使用 manualChunks 合理分割 chunk
默认情况下,Vite 可能将 vue、element-plus 等大型依赖与部分业务代码杂合成一个 chunk,导致:
- 某个业务代码的小改动,导致整个大 chunk 的缓存失效
- 首屏加载了大量用户当前不需要的依赖
通过 manualChunks 按类型将第三方依赖分组:
js
// vite.config.ts
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor-vue': ['vue', 'vue-router', 'pinia', 'vue-i18n', '@vueuse/core'],
'vendor-element': ['element-plus', '@element-plus/icons-vue'],
'vendor-utils': ['axios', 'dayjs', 'lodash-es', 'uuid', 'crypto-js'],
'vendor-charts': ['echarts', 'vue-echarts'],
'vendor-editor': ['codemirror', '@kangc/v-md-editor'],
}
}
}
}
收益:
- 大型稳定依赖(如
element-plus)基本不会变更,可以长期命中浏览器缓存 - 业务代码更新只影响对应的业务 chunk,不影响第三方 chunk
- 避免一次性下载过大的文件,提升并行下载效率
3.3 用轻量依赖替换重型依赖
| 场景 | 重型依赖 | 轻量替代 | 优势 |
|---|---|---|---|
| 时间处理 | moment |
dayjs |
体积小约 97%,API 基本兼容 |
| 工具函数 | lodash |
lodash-es |
支持 Tree Shaking,按需打包 |
对于已有项目中的 lodash,可以通过 alias 将其指向 lodash-es,无需修改业务代码:
js
// vite.config.ts
resolve: {
alias: {
'lodash': 'lodash-es'
}
}
注意 :如果项目中
lodash-es是间接依赖而非显式声明,存在版本不可控的风险,建议在package.json中显式声明。
3.4 Tree-Shaking 生效保障
Tree-Shaking 并不会自动生效,需满足以下前提条件:
- 必须使用 ES Module :Tree-Shaking 依赖静态分析,CommonJS 的
require是动态的,无法被分析 - 严格使用具名导入 :
import { debounce } from 'lodash-es',禁止import _ from 'lodash-es'全量导入 - 避免 CommonJS 混合引用:部分旧插件或工具库仍以 CJS 格式发布,引入后 Tree-Shaking 对其失效
- 检查
sideEffects字段 :确认第三方包的package.json正确声明了sideEffects: false,否则打包器不敢剔除
js
// ✅ Tree-Shaking 生效
import { debounce, throttle } from 'lodash-es'
// ❌ Tree-Shaking 失效,全量引入
import _ from 'lodash-es'
import lodash from 'lodash' // CJS,无法 Tree-Shake
3.5 超大库 CDN 外链,排除打包
echarts、monaco-editor 等超大型库打入 bundle 收益极低,应通过 CDN 外链引入并在打包时排除:
js
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
external: ['echarts'],
output: {
globals: {
echarts: 'echarts'
}
}
}
}
})
html
<!-- public/index.html -->
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
适用场景:体积超过 500KB 的第三方库、版本相对稳定的大型 UI 框架。外链 CDN 可命中浏览器跨站缓存,并行加载不占主 bundle 带宽。
3.6 路由懒加载与组件按需加载
将路由组件改为动态导入,让每个路由页面独立成 chunk,首屏只加载必要的代码:
js
// 静态导入(不推荐)
import UserList from '@/views/user/index.vue'
// 动态导入(推荐)
const UserList = () => import('@/views/user/index.vue')
对于大型组件(如富文本编辑器、图表组件),同样应使用异步组件 + Suspense:
js
import { defineAsyncComponent } from 'vue'
const HeavyChart = defineAsyncComponent(() => import('./HeavyChart.vue'))
四、运行时性能:让页面更流畅
4.1 虚拟滚动处理长列表
当列表数据量超过几百条时,全量渲染会导致严重的性能问题。使用虚拟滚动只渲染可视区域内的 DOM 节点:
vue
<!-- 使用 vue3-virtual-scroller -->
<RecycleScroller
:items="largeList"
:item-size="48"
key-field="id"
v-slot="{ item }"
>
<div class="list-item">{{ item.name }}</div>
</RecycleScroller>
4.2 合理使用 computed 与 watch
computed有缓存机制,依赖不变时不重新计算,适合派生数据watch适合副作用操作,避免在watch里执行大量同步计算- 对于频繁触发的事件(如 scroll、resize、input),使用防抖/节流:
js
import { useDebounceFn } from '@vueuse/core'
const handleSearch = useDebounceFn((val) => {
fetchData(val)
}, 300)
4.3 避免不必要的响应式
不是所有数据都需要是响应式的。对于纯展示的静态配置数据,使用 Object.freeze() 或直接声明为普通变量:
js
// 大型静态配置,无需响应式
const TABLE_COLUMNS = Object.freeze([
{ prop: 'name', label: '名称' },
// ...
])
Vue 3 中,shallowRef 和 shallowReactive 可以减少深层响应式的开销,适用于内部结构不需要追踪的大型对象。
4.4 图片懒加载与资源优化
vue
<!-- 原生懒加载 -->
<img :src="item.cover" loading="lazy" />
- 使用 WebP/AVIF 格式替代 PNG/JPEG,体积减少 30%~50%
- 小图标优先使用 SVG(矢量、可缓存、无失真)
- 超小图标(< 4KB)配置 Vite 内联为 base64,减少 HTTP 请求
js
// vite.config.ts
build: {
assetsInlineLimit: 4096 // 4KB 以下自动内联
}
4.5 Web Worker 处理耗时任务
将大量计算(如数据解析、复杂过滤)移出主线程,避免阻塞 UI:
js
// worker.js
self.onmessage = ({ data }) => {
const result = heavyComputation(data)
self.postMessage(result)
}
// 主线程
const worker = new Worker(new URL('./worker.js', import.meta.url))
worker.postMessage(rawData)
worker.onmessage = ({ data }) => {
tableData.value = data
}
4.6 合理使用缓存策略
HTTP 缓存层面:
- 哈希文件名(Vite 默认)的 JS/CSS 资源:
Cache-Control: max-age=31536000, immutable index.html:Cache-Control: no-cache,强制每次重新验证
应用层缓存:
- 接口数据缓存:对于变化不频繁的字典数据,缓存在 Vuex/Pinia 或 localStorage 中
- 使用
keep-alive缓存列表页,避免切换路由时重复请求
vue
<router-view v-slot="{ Component }">
<keep-alive :include="cachedViews">
<component :is="Component" :key="$route.fullPath" />
</keep-alive>
</router-view>
4.7 首屏性能优化
关键指标:
- FCP (First Contentful Paint):第一个内容绘制时间
- LCP (Largest Contentful Paint):最大内容绘制时间
- TTI (Time to Interactive):可交互时间
优化手段:
html
<!-- 关键 CSS 内联,避免渲染阻塞 -->
<style>/* 关键样式 */</style>
<!-- 预加载关键资源 -->
<link rel="preload" href="/fonts/main.woff2" as="font" crossorigin>
<!-- 预连接第三方域名 -->
<link rel="preconnect" href="https://cdn.example.com">
4.8 内存优化与防止内存泄漏
ToB 后台系统页面常驻、频繁切路由,内存泄漏非常致命,长时间运行后出现卡顿甚至崩溃。
常见泄漏来源与应对方案:
① 定时器 / 延时器未清除
js
let timer = null
onMounted(() => {
timer = setInterval(() => { fetchData() }, 5000)
})
onUnmounted(() => {
timer && clearInterval(timer)
timer = null
})
② 全局事件监听未移除
js
const resizeFn = () => { recalcLayout() }
onMounted(() => {
window.addEventListener('resize', resizeFn)
})
onUnmounted(() => {
window.removeEventListener('resize', resizeFn)
})
③ 大型实例未销毁
ECharts、富文本编辑器、地图等平台具备 dispose / destroy 方法,组件卸载时必顿手动调用,否则 DOM 删除后内存不会释放:
js
let chartInstance = null
onMounted(() => {
chartInstance = echarts.init(chartRef.value)
})
onUnmounted(() => {
chartInstance?.dispose()
chartInstance = null
})
④ 其他注意事项
- 避免滥用全局变量持有大对象引用
- 避免重复绑定事件(如在每次路由进入时重复添加监听而不移除)
- Vuex/Pinia 中的大体量数据在离开页面后考虑重置,避免长期占用内存
4.9 渲染层优化(重排 / 重绘 / 合成)
将频繁变动元素提升为合成层,隔离重绘影响范围:
css
.animated-box {
transform: translateZ(0); /* 创建独立的层,已少用,可用 will-change 替代 */
will-change: transform; /* 提示浏览器提前分配合成层 */
}
注意 :
will-change不应滥用,仅对确实存在性能问题的动画元素使用,过多使用反而增加内存占用。
批量操作 DOM,避免频繁回流:
js
// ✅ 批量操作,一次回流
const fragment = document.createDocumentFragment()
items.forEach(item => {
const li = document.createElement('li')
li.textContent = item.name
fragment.appendChild(li)
})
listEl.appendChild(fragment)
// ❌ 循环中反复操作 DOM,每次都触发回流
items.forEach(item => {
const li = document.createElement('li')
li.textContent = item.name
listEl.appendChild(li) // 每次 appendChild 都可能触发重排
})
避免强制同步布局(Layout Thrashing):
js
// ❌ 读写交替,每次读取 offset 都强制浏览器刷新布局
for (const el of elements) {
const h = el.offsetHeight // 读取布局
el.style.height = h + 10 + 'px' // 写属性导致布局脚本化
}
// ✅ 先批量读,再批量写
const heights = elements.map(el => el.offsetHeight)
elements.forEach((el, i) => { el.style.height = heights[i] + 10 + 'px' })
4.10 请求层优化
① 接口防抖与重复请求合并
相同 URL 在短时间内被并发多次调用(如多个组件同时初始化请求字典),可利用 Map 进行请求共享:
js
const pendingRequests = new Map()
function requestWithDedup(url, params) {
const key = `${url}?${JSON.stringify(params)}`
if (pendingRequests.has(key)) return pendingRequests.get(key)
const promise = api.get(url, { params }).finally(() => {
pendingRequests.delete(key)
})
pendingRequests.set(key, promise)
return promise
}
② 内存级接口缓存
字典、配置项等只读数据,一次请求后内存缓存,后续用就不再发请求:
js
const dictCache = new Map()
async function getDictList(type) {
if (dictCache.has(type)) return dictCache.get(type)
const res = await api.getDictByType(type)
dictCache.set(type, res.data)
return res.data
}
③ 分页 / 分片加载大数据集
- 列表数据务必分页,禁止一次性返回全量数据
- 导出 / 报表等大体量操作采用流式分片下载,避免内存溢出
④ 超时、重试与降级策略
js
const instance = axios.create({
timeout: 15000 // 全局超时时间
})
// 响应拦截器:部分接口超时后兑现缓存数据或默认占位,避免白屏卡死
instance.interceptors.response.use(null, (error) => {
if (axios.isCancel(error)) return Promise.reject(error)
if (error.code === 'ECONNABORTED') {
// 超时处理:默认占位 / 提示 / 重试
return fallbackHandler(error)
}
return Promise.reject(error)
})
4.11 静态资源深度优化
字体优化
- 子集化 :使用
pyftsubset、font-spider等工具对自定义字体进行子集化,只保留项目实际用到的字符集 - 分片加载:中文字体主片较大,可切分成多个子集分批按需加载
- 预加载 :关键路径字体在
<head>中提前声明<link rel="preload"> - 展示策略 :使用
font-display: swap避免字体加载期间隐藏文本
css
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom-subset.woff2') format('woff2');
font-display: swap;
}
视频 / 音频资源
- 预先压缩,限制码率(如导出 mp3 不超过 128kbps)
- 采用懒加载播放(
preload="none"/preload="metadata"),避免页面加载时就拉取媒体流
html
<video src="/assets/intro.mp4" preload="metadata" loading="lazy"></video>
第三方资源
- 按需引入 SDK:第三方 CDN 资源只引入项目实际使用的模块,勿全量引入
- 埋点脚本懒加载:埋点非首屏关键路径,延迟加载,不阻塞主要资源:
js
// 首屏加载完成后再加载埋点脚本
window.addEventListener('load', () => {
const script = document.createElement('script')
script.src = 'https://analytics.example.com/tracker.js'
document.body.appendChild(script)
})
- 稳定性预连接 :对常用第三方域名添加
<link rel="preconnect">减少连接建立耗时
五、CI/CD 流程优化
5.1 依赖安装提速
yaml
# .gitlab-ci.yml
cache:
key:
files:
- package-lock.json # 以 lock 文件哈希为 key,lock 不变则缓存有效
paths:
- node_modules/
注意 :开启缓存后多了一步"保存缓存"的操作,当
node_modules很大时,这个步骤本身也耗时。实测发现整体收益有限,需结合项目实际情况决定是否启用。
5.2 并行阶段设计
合理拆分 CI 阶段,将独立的任务(如安全扫描、代码质量检查)与构建并行执行,而不是串行等待:
yaml
stages:
- prepare
- build_and_security # 构建与安全扫描并行
- publish
5.3 减少上传文件数量
静态资源上传到 CDN(如 Azure Blob Storage)的耗时往往占整个 CI 流程的很大比例。可以从以下方向优化:
- 减少产物文件数量:合理配置浏览器兼容范围,减少 legacy chunk 数量
- 并发上传:将串行上传改为并发上传,充分利用带宽
- 增量上传:只上传内容变化的文件(基于文件哈希对比)
六、开发效率提升
6.1 Vite 开发服务器优化
Vite 采用 ESM 按需编译,冷启动速度远快于 webpack。但随着项目增大,也可能出现速度下降的情况。参见前文 2.7 optimizeDeps 精细化控制 、2.5 提升本地开发启动速度 ,合理配置预构建与启动命令。
此外,开启 Vite 3+ 的沙箏模式可进一步加快冷启动:
js
// vite.config.ts
export default defineConfig({
server: {
fs: {
strict: false // 开发阶段可适当放开,减少文件系统检查开销
}
}
})
6.2 TypeScript 类型检查分离
如前文所述,将类型检查从开发服务器启动中剥离:
json
{
"scripts": {
"dev": "vite --host",
"build": "vite build",
"type-check": "vue-tsc --noEmit",
"lint": "eslint src --ext .vue,.ts,.tsx"
}
}
配合 IDE 插件(Vue Official + TypeScript LSP)提供实时的类型提示和错误提示,不依赖命令行检查。
6.3 路径别名与自动导入
减少手动 import 的心智负担,提升开发效率:
js
// 路径别名
resolve: {
alias: { '@': path.resolve(__dirname, 'src') }
}
// 自动导入(unplugin-auto-import + unplugin-vue-components)
AutoImport({
imports: ['vue', 'vue-router', 'pinia'],
dts: 'src/auto-imports.d.ts'
}),
Components({
resolvers: [ElementPlusResolver()],
dts: 'src/components.d.ts'
})
七、线上监控与长效优化闭环
优化不应是一次性的工作,而应该是一个度量 → 分析 → 优化 → 回归验证的工程闭环。
7.1 接入 RUM 性能监控
关键 Web Vitals 指标:
| 指标 | 含义 | 良好阈值 |
|---|---|---|
| LCP (Largest Contentful Paint) | 最大内容绘制时间 | < 2.5s |
| FID (First Input Delay) | 首次输入延迟 | < 100ms |
| CLS (Cumulative Layout Shift) | 累积布局偏移 | < 0.1 |
| FCP (First Contentful Paint) | 首次内容绘制 | < 1.8s |
| TTI (Time to Interactive) | 可交互时间 | < 3.8s |
| 白屏时间 | 页面开始到首内容出现 | 项目自定 ≤ 1.5s |
使用 web-vitals 库采集指标并上报:
js
import { onLCP, onFID, onCLS, onFCP, onTTFB } from 'web-vitals'
function sendToAnalytics({ name, value, id }) {
navigator.sendBeacon('/api/metrics', JSON.stringify({ name, value, id }))
}
onLCP(sendToAnalytics)
onFID(sendToAnalytics)
onCLS(sendToAnalytics)
onFCP(sendToAnalytics)
onTTFB(sendToAnalytics)
7.2 资源加载异常监控
js
window.addEventListener('error', (event) => {
if (event.target instanceof HTMLScriptElement || event.target instanceof HTMLLinkElement) {
reportResourceError({
type: 'resource',
src: event.target.src || event.target.href,
message: '资源加载失败'
})
}
}, true)
7.3 JS 错误监控
建议接入 Sentry 等错误监控平台:
js
import * as Sentry from '@sentry/vue'
Sentry.init({
app,
dsn: 'YOUR_DSN',
tracesSampleRate: 0.1, // 采样率,生产环境不全量采集
environment: import.meta.env.MODE
})
7.4 包体积巡检与超大文件拦截
定期包体积巡检,防止性能退化不被感知:
yaml
# .gitlab-ci.yml 中添加体积检查步骤
bundle-size-check:
stage: build
script:
- npm run build
- node scripts/check-bundle-size.js # 超过阈值则退出非 0、阻断流水线
artifacts:
paths:
- dist/stats.html # 保留分析报告
js
// scripts/check-bundle-size.js
const fs = require('fs')
const path = require('path')
const THRESHOLD_KB = 500 // 单个 chunk 上限
const files = fs.readdirSync('./dist/assets')
const oversized = files
.filter(f => f.endsWith('.js'))
.map(f => ({ name: f, size: fs.statSync(`./dist/assets/${f}`).size / 1024 }))
.filter(f => f.size > THRESHOLD_KB)
if (oversized.length > 0) {
console.error('✖ 超大 chunk 拦截:')
oversized.forEach(f => console.error(` ${f.name}: ${f.size.toFixed(1)}KB`))
process.exit(1)
}
console.log('✔ 所有 chunk 体积在阈值内')
7.5 长效优化建议
- 每次迭代定期跑
visualizer分析报告,感知包体积变化趋势 - 大依赖升级前先测量体积影响,避免升级后体积暴走
- 建立性能指标 SLO:如首屏白屏时间 ≤ 1.5s、LCP ≤ 3s,达标后功能上线
- 将性能优化纳入 Code Review 流程:新引入大依赖、全量导入需经审查
八、优化总结
| 优化维度 | 具体措施 | 预期收益 |
|---|---|---|
| 构建速度 | 合理配置 legacy targets | 构建时间减少 60%+ |
| 构建速度 | 关闭 gzip 大小计算 | 小幅提速 |
| 构建速度 | 关闭生产 sourcemap | 体积小幅、构建提速 |
| 构建速度 | optimizeDeps 精细化 | 冷启动提速明显 |
| 构建速度 | 移除启动时类型检查 | dev 启动提速明显 |
| 产物体积 | manualChunks 分 chunk | 缓存命中率大幅提升 |
| 产物体积 | lodash → lodash-es + Tree Shaking | 按需打包,减小体积 |
| 产物体积 | moment → dayjs | 减少约 200KB |
| 产物体积 | 超大库 CDN 外链排除 | 主 bundle 体积大幅下降 |
| 产物体积 | esbuild drop console | 轻微减小体积 |
| 运行时 | 路由懒加载 | 首屏加载资源减少 |
| 运行时 | 虚拟滚动 | 长列表渲染性能大幅提升 |
| 运行时 | keep-alive 缓存 | 路由切换体验提升 |
| 运行时 | 图片懒加载 + WebP | 页面加载速度提升 |
| 运行时 | 防内存泄漏(清除定时器/监听/实例) | ToB 常驻页内存稳定性保障 |
| 运行时 | 重排/重绘优化 + will-change | 动画流畅度提升 |
| 运行时 | 请求合并去重 + 接口缓存 | 接口并发减少、响应加快 |
| 运行时 | Web Worker 处理耗时任务 | UI 不卡顿 |
| 资源 | 字体子集化 + swap | 字体加载不隐藏文本 |
| 资源 | 埋点/SDK 懒加载 | 不占首屏关键路径资源 |
| CI/CD | 减少 legacy 产物 | CDN 上传文件数减少 |
| CI/CD | 并发上传静态资源 | 上传耗时减少 |
| 监控 | RUM 指标采集 (LCP/FID/CLS) | 可视化性能轨迹 |
| 监控 | 包体积巡检 + CI 拦截 | 防止性能退化上线 |
九、结语
前端性能优化没有银弹,每个项目的瓶颈都不尽相同。最重要的思路始终是:
先度量,再优化。
通过构建分析工具、性能监控面板、RUM 真实用户数据定位问题,而不是凭经验盲目优化。将性能指标纳入 CI/CD 流水线,建立包体积巡检机制,让性能优化成为日常研发流程的一部分,而不仅仅是项目上线前的临时抖包。