大家好,我是那个看到 3 秒白屏就心里发毛的前端。做前端久了,慢慢摸到一个真理:性能就是用户的耐心,磨没了就走人。今天以 Vue 3 为例,聊聊在编码和工程化两条线上的一些优化心得。
一、编码层面(以 Vue 3 为例)#
1. 合理使用 key#
- Key 是 diff 的身份证号,不要随便用数组 index 充数,特别是列表会增删项的时候,会导致 DOM 复用错位,出现莫名其妙的 UI bug。
- 用稳定唯一的业务 ID:比如 item.id。只有在你百分之百确定渲染顺序不会变更时,index 才是备选。
2. 合理使用 v-if 和 v-show#
- v-if 是“生孩子”,需要创建/销毁组件;v-show 是“拉窗帘”,只是切换 display。
- 频繁切换就用 v-show;很少出现的大模块(比如弹一次就关)用 v-if。别反着来,不然要么白屏慢半拍,要么白占 DOM 内存。
- 小贴士:v-if 下的异步初始化成本更高,尤其组件里有重型计算或网络请求。
3. 合理使用 computed 和 watch#
- computed 用来描述“值是怎么来的”,它会有缓存,依赖不变就不算,适合纯派生数据。
- watch 更像“盯梢触发动作”,比如请求数据、写 localStorage、节流/防抖副作用等。
- watchEffect 适合快速原型,但生产上更建议 watch 明确依赖,避免“雨露均沾”导致难以维护。
小例子:
// computed:把业务逻辑留在派生数据里const list = ref<Item[]>([])const visibleList = computed(() => list.value.filter(i => !i.hidden))
// watch:副作用与异步watch(() => route.params.id, async (id) => { if (!id) return data.value = await fetchDetail(id)}, { immediate: true })
4. 配合 动态组件#
- 针对“来回切换会丢状态”的页面(表单/列表筛选等),keep-alive 非常香。
- 好用但别滥用:缓存多了内存涨,注意 include/exclude/max。
- 路由场景下常用写法:
<keep-alive include="UserList,UserForm" :max="5"> <router-view v-slot="{ Component }"> <component :is="Component" /> </router-view></keep-alive>
5. 路由懒加载、动态导入#
- 首屏不背锅:路由级别用动态 import 切 chunk,打包自然就拆了。
const routes = [ { path: '/', component: () => import('@/pages/Home.vue') }, { path: '/user', component: () => import('@/pages/User.vue') },]
- 搭配 webpackChunkName 注释/rollup 配置更可控,命名可读性强。
6. 插件按需加载#
- UI 库、图表库、富文本编辑器,能按需就按需。一次性全家桶,用户网络会“泪流满面”。
- 配合 unplugin-vue-components / unplugin-auto-import,省心且稳定。
- 大件(比如 echarts)可延迟加载:进入对应页面再 import。
7. 普通图片升级为 WebP#
- WebP 一般能省 30%~70% 体积,肉眼看不出差别,速度却嗖嗖的。
- 用
<picture>
做回退,兼容老浏览器:
<picture> <source srcset="/img/banner.webp" type="image/webp" /> <img src="/img/banner.jpg" alt="banner" loading="lazy" /></picture>
- 别忘了设置尺寸、开启 lazy。
8. 骨架屏#
- 骨架屏不是“变快”,是“看起来快”。白屏三秒不如骨架两秒。
- 列表页优先安排,骨架样式尽量贴近真实布局,避免切换时晃眼。
9. 延迟装载:异步组件 + <Suspense>
#
- 重型组件按需加载,空档期用 fallback 顶上,用户体验丝滑。
<script setup lang="ts">import { defineAsyncComponent } from 'vue'const Chart = defineAsyncComponent(() => import('./Chart.vue'))</script>
<template> <Suspense> <template #default> <Chart /> </template> <template #fallback> <div class="loading">图表加载中...</div> </template> </Suspense></template>
- 失败重试可以自己包一层逻辑,或用第三方 helper。
10. Vue 细节再进阶:省点“响应式账单”#
- ref:适合基本类型或单值容器。Ref 是个带 value 的对象,Vue 3 底层依然基于 Proxy 的依赖收集/触发机制进行追踪,不是 Vue 2 的 defineProperty 老路子。
- reactive:适合对象/数组等复合数据结构。对属性访问进行 track/trigger,读时收集依赖、写时触发更新。
- 实战建议:
- 表单用 reactive 管对象,表单项单值可用 ref。
- 解构 reactive 会丢响应式,用 toRefs/toRef 兜底。
- 深层对象频繁更新,考虑拆分为多个更小的 ref,减少无谓更新面。
- v-once/v-memo(Vue 3.4+)减少不必要更新
- shallowRef/shallowReactive/markRaw:避免深层追踪导致的级联更新
- computed 开销大时记得“分治”:把慢计算拆小、缓存命中率更高
- 避免“过大 reactive 对象”:拆成多个更小的 ref,粒度更细
- 组件通讯:事件/props 简洁化,避免层层 watch 深监听
11. 渲染与交互:主线程“别堵车”#
- 拆分长任务:把大计算切片(requestIdleCallback/分帧/rAF),或丢到 Web Worker
- 虚拟列表:滚动列表几百上千项必须上(Vue Virtual Scroller/自研)
- CSS 优先:动画尽量用 transform/opacity;避免频繁触发 reflow
- 新能力:
- content-visibility: auto + contain-intrinsic-size,列表/长页渲染提速明显
- will-change 只在必要时用,别常驻
- 图表/绘制:OffscreenCanvas + Worker;大数据渲染别跟主线程抢饭碗
二、工程化方面#
1. 图片压缩、懒加载#
- 构建链路接入 imagemin 或 CI 阶段统一压图,别相信“美术已经压了”。
- 响应式图片(不同屏幕用不同尺寸)、SVG 图标尽量用矢量。
- 所有非首屏图片统一 loading=“lazy”,别客气。
2. CDN#
- 静态资源走 CDN,离用户更近、并发更高。
- 注意版本号与缓存策略,路径上打 content hash,避免回源抖动。
- 跨域与子资源完整性(SRI)一起考虑。
3. 静态文件压缩、代码分割、提取公共 CSS、优化 SourceMap#
- 代码分割:按路由、按功能模块、按依赖拆;避免“一个入口一个巨无霸”。
- CSS 提取:将组件内样式抽成独立 css,充分利用浏览器并行下载。
- SourceMap:生产环境建议 hidden-source-map 或上传到错误上报平台,别光明正大放线上导致泄露。开发环境保持高可读性即可。
- 产物压缩:结合 gzip/br(见下条),构建环节预压。
4. gzip 压缩(或 brotli)#
- 服务器开启 gzip/br 压缩,文本类(js/css/json/svg)收益巨大。
- brotli 压缩比更好,但压缩时间更长;可以构建时预压,线上直接回源。
5. DLL 加载(老 Webpack 项目的备选方案)#
- 早年 webpack 用 DLL 把不常变的库(vue/vue-router/axios 等)单独打成静态资源,构建更快、缓存更稳。
- 现在 Vite/现代打包器下,多用“预构建 + vendor chunk”替代,效果更稳定,配置简单。新项目建议直接走 Vite 的依赖预构建。
6. 文件长缓存#
- 文件名带 contenthash(app.abc123.js),配合强缓存一年。
- index.html 永远不要强缓存(或极短),用来“指挥”新旧产物的切换。
- 运行时代码与业务拆开,避免一点小改动导致 vendor 失效。
7. HTTP/2(或 HTTP/3)#
- 二进制帧:解析快一些。
- 头部压缩(HPACK/QPACK):请求多了更明显。
- 多路复用:一个连接跑多个请求,再也不是“队头阻塞”的年代。
- 实操小贴士:开启 H2 后别盲目精灵图/强合并,过度合并反而浪费并发优势;按需拆分才是正解。
8. 缓存策略#
- HTTP 缓存头:
- 强缓存 + 协商缓存搭配使用。
- index.html 设置强制不缓存(或很短 max-age + must-revalidate),其它静态资源强缓存一年,文件名带 hash 即可安全更新。
- 浏览器缓存(localStorage/sessionStorage):
- 一些“变化不频繁”的数据(比如菜单、字典)可以本地缓存,配上版本号/时间戳失效策略。
- 尽量避免把“需要强一致”的数据放在本地长时间缓存,容易出幺蛾子。
- 更进一步:Service Worker 做离线与缓存优先策略,但这块属于“加强版工程化”,上线前要认真设计更新策略,别把用户锁死在老版本。
9. 指标与监控:别盲修,先会量#
- 核心指标(Core Web Vitals)
- LCP 首屏最大内容绘制 < 2.5s
- INP 交互响应(取代 FID)< 200ms
- CLS 累积位移 < 0.1
- 其他常用:TTFB、FCP、TTI、TBT、Long Task>50ms
- 工具与手段
- 开发时:Chrome Performance/Network/Coverage、Lighthouse
- 线上 RUM:web-vitals 库、Sentry/Datadog/阿里 ARMS,采样+看板+告警
- 性能预算(Performance Budget):给包体/首屏/LCP设“红线”,CI 超标就挂
10. 网络与资源调度:让“路”更顺#
- 资源提示(Resource Hints)
- preconnect/dns-prefetch:提早握手
- preload:关键 CSS/字体/LCP 图片抢跑
- prefetch:下一页的资源趁闲时备好(hover 预取/空闲预取)
- 优先级提示
- fetchpriority=“high” 给 LCP 图;低价值资源降级
- HTTP 细节
- Cache-Control: immutable、stale-while-revalidate/stale-if-error
- ETag/Last-Modified 协商缓存
- HTTP/3(QUIC)在弱网下更稳
11. 构建与包体积:少即是多#
- Tree-shaking 到底:确认库支持 ESM;避免副作用文件;配置 sideEffects
- 分析与减重
- Bundle Analyzer / Source Map Explorer 定位大块头
- 体积替换:lodash-es/按需;moment→dayjs/date-fns;图标改 SVG sprite
- Polyfill 精准注入:按浏览器目标+按需注入,别“一锅端”
- 代码切分策略
- manualChunks/vendor 拆分有边界;避免一个巨型 vendor
- 国际化/富文本/图表等大模块独立 chunk,真到页面再拉
12. SSR/SSG/流式渲染:“先看见,再水合”#
- SSR/SSG(Nuxt/Vite SSR):首屏快、SEO 友好
- 流式 SSR(Streaming):边渲染边发,减少白屏时间
- 部分水合/岛屿架构:只对需要交互的区域水合,减少无效 JS
13. PWA 与离线:体验加成,但要管好“更新”#
- Service Worker 缓存策略(Cache First / Network First / Stale-While-Revalidate)
- 离线兜底页、弱网提示、后台同步(Background Sync)
- 注意版本更新策略:否则用户“卡在老版本”会抓狂
14. 内存与泄漏:慢慢变慢也是慢#
- 组件卸载时清理定时器/监听器/Observer
- 长列表/地图/编辑器一类重组件,离开路由记得释放
- DevTools Memory/Performance 查 Detached Nodes、持续增高的快照
15. 工程实战小清单#
- 首屏资源:只保留“必需”,其余延迟/异步
- LCP 图:preload + fetchpriority=high + 固定尺寸避免 CLS
- 字体:subset + woff2 + swap;关键字体 preload
- 图片:webp/avif + srcset + lazy;骨架/占位
- 缓存:静态资源 hash 强缓存一年;HTML 短缓存/不缓存
- 监控:web-vitals 上报 + 自定义关键路径打点
- 构建:Bundle 分析每迭代看一次;性能预算接入 CI
- 弱网/低端机:模拟“慢 3G + 4x CPU throttling”自测一回
小结#
性能优化是“系统工程”:监控先行、定位精准、落地务实、持续回归。上面这些点,覆盖了从指标、构建、网络到运行时的关键环节——“手起刀落,干净利索”。