分享一个真实项目的优化历程:首屏从 4.8s 降到 0.9s,LCP 从 5.2s 降到 1.1s。文章按"指标→定位→优化→验证"的链路展开,附完整代码,可直接套用。
一、为什么要做性能优化
性能不是"锦上添花",而是直接影响业务指标的硬指标:
- 页面加载时间每多 1 秒,转化率平均下降约 20%
- 移动端用户中 53% 会在加载超过 3 秒时直接离开
- 加载速度是 Google / 百度搜索排名的核心因素之一
- 大促场景下,每 100ms 的卡顿都可能带来真金白银的损失
结论:性能优化是投入产出比最高的前端投资。
二、先搞清楚衡量指标
优化前先明确"衡量什么",否则就是瞎忙。核心看这几个:
| 指标 | 全称 | 含义 | 优秀线 |
|---|---|---|---|
| FP | First Paint | 首次绘制 | < 1.0s |
| FCP | First Contentful Paint | 首次内容绘制 | < 1.8s |
| LCP | Largest Contentful Paint | 最大内容绘制 | < 2.5s |
| TTI | Time to Interactive | 可交互时间 | < 3.8s |
| CLS | Cumulative Layout Shift | 累计布局偏移 | < 0.1 |
| TBT | Total Blocking Time | 总阻塞时间 | < 200ms |
我的建议:日常只盯 LCP、CLS、TBT 三个,业务上再补一个"可交互首屏时间"(自定义上报)。
三、优化的完整链路
优化要按"用户从输入 URL 到页面可用"的整条链路来做,拆成五个层面:
1. 网络层:少请求、近路由、压缩
① 开启 HTTP/2 + CDN
HTTP/2 支持多路复用,一个 TCP 连接并发传输,消除队头阻塞。CDN 让静态资源就近返回,全国访问延迟从 50ms 降到 10ms 以内。
nginx.conf 示例:
server {
listen 443 ssl http2;
...
}
② 资源压缩
- HTML/CSS/JS 开启 Gzip(体积小用 Brotli 更好,可再省 20%)
- 图片用 WebP/AVIF,图标用 SVG
- 代码压缩交给构建工具,JS 开启 tree-shaking
③ HTTP 缓存策略
nginx
# 带 hash 的文件:一年强缓存,配合 CDN
location /static/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# 不带 hash 的文件:协商缓存
location /index.html {
add_header Cache-Control "no-cache";
}
2. 资源加载层:代码分割 + 懒加载
① 路由级代码分割(Vue 3 + Vite)
javascript
// router/index.js
const routes = [
{
path: '/home',
component: () => import('@/views/Home.vue'), // 路由懒加载
},
{
path: '/detail',
component: () => import('@/views/Detail.vue'),
},
];
② 组件级懒加载(v-if 控制 + defineAsyncComponent)
vue
<template>
<Chart v-if="visible" />
</template>
<script setup>
import { defineAsyncComponent, ref } from 'vue';
const Chart = defineAsyncComponent(() => import('@/components/BigChart.vue'));
const visible = ref(false);
</script>
③ 预加载 / 预连接关键资源
html
<link rel="preconnect" href="https://cdn.example.com" crossorigin />
<link rel="preload" as="font" href="font.woff2" type="font/woff2" crossorigin />
注意:
preload只用于关键资源,滥用反而会占用带宽。
3. 渲染层:管住关键渲染路径
① 首屏 CSS 内联 / 关键 CSS
FCP 之前阻塞渲染的只有 HTML 和 CSS,把首屏必需的小段 CSS 内联进 HTML:
html
<style>
.app-header { ... } /* 首屏关键样式 */
</style>
<link rel="stylesheet" href="app.css" media="print" onload="this.media='all'">
② 骨架屏替代 loading 转圈
白屏的体验很差,骨架屏能让用户"感觉快"。手写或用插件生成:
vue
<template>
<div class="skeleton">
<div class="skeleton-avatar"></div>
<div class="skeleton-line" v-for="i in 3" :key="i"></div>
</div>
</template>
<style scoped>
.skeleton-avatar {
width: 48px; height: 48px;
background: #f0f0f0;
animation: pulse 1.2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
</style>
4. JavaScript 层:减任务、拆长任务
① 去掉不需要的依赖(重点看 node_modules 体积分析)
bash
# Vite 项目看依赖体积
npx vite-bundle-visualizer
② 长任务切片 / 异步执行
主线程被超过 50ms 的长任务占住,就会卡顿。把大循环交给浏览器空闲时间处理:
javascript
function processHeavy(items) {
const chunk = 100;
let index = 0;
function next() {
const end = Math.min(index + chunk, items.length);
for (; index < end; index++) {
processItem(items[index]); // 每 100 个切一次
}
if (index < items.length) {
requestIdleCallback(next); // 空闲时继续
}
}
requestIdleCallback(next);
}
③ 计算密集任务交给 Web Worker
javascript
// main.js
const worker = new Worker(new URL('./worker.js', import.meta.url));
worker.postMessage(data);
worker.onmessage = (e) => {
console.log('计算结果', e.data);
};
5. 图片/字体层:最容易被忽视的隐形杀手
- 图片 :尺寸适配 + WebP/AVIF +
loading="lazy"+ 占位尺寸防 CLS - 字体 :
font-display: swap+ 子集化 + 只引入用到的字重
html
<img
src="banner.webp"
width="750"
height="400"
loading="lazy"
decoding="async"
alt="banner"
/>
css
@font-face {
font-family: 'PingFang';
src: url('pingfang-subset.woff2') format('woff2');
font-display: swap; /* 先显示系统字体,加载完再切换,避免白屏 */
}
四、实战复盘:优化前后数据对比
一个 Vite + Vue 3 的信息流页面,优化动作如下:
| 优化项 | 动作 |
|---|---|
| 路由懒加载 | 首包 JS 从 1.2MB → 320KB |
| 图片改造 | PNG → WebP,全站图片体积降 70% |
| 静态资源上 CDN | 全国平均下载时间降 60% |
| 关键 CSS 内联 | FCP 提前 0.6s |
| 骨架屏 | 感知加载时间大幅下降 |
| HTTP 缓存 | 二次访问直接命中强缓存 |
优化结果:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| FCP | 2.6s | 1.1s |
| LCP | 5.2s | 1.9s |
| 首包大小 | 1.2MB | 320KB |
| TBT | 480ms | 120ms |
| 转化率 | 基准 | +18% |
五、常用定位工具
- Lighthouse(Chrome 自带)------一键出报告,给分数和优化建议
- Performance 面板------火焰图定位 JS 长任务、内存泄漏
- Network 面板------看请求瀑布图,找瓶颈请求
- web-vitals 库------真实用户监控
javascript
// 接真实用户监控
import { onLCP, onCLS, onINP } from 'web-vitals';
onLCP((metric) => report({ name: 'LCP', value: metric.value }));
onCLS((metric) => report({ name: 'CLS', value: metric.value }));
onINP((metric) => report({ name: 'INP', value: metric.value }));
六、总结:优化优先级建议
按性价比排序,建议的执行顺序:
- 先量化:跑 Lighthouse + 埋点,拿到当前基线
- 网络层:CDN + HTTP/2 + 压缩,成本低见效快
- 体积瘦身:代码分割 + 去掉大依赖 + 图片格式升级
- 渲染层:关键 CSS + 骨架屏
- JS 执行:长任务切分 + Worker
- 最后做:细粒度缓存策略、字体子集化
记住一个原则:先测量,再优化,最后再测量验证。不要凭感觉优化,每次改动都留下前后数据,用数据说话。
本文首发于 CSDN,欢迎交流。优化没有银弹,但按链路来,效果一定看得见。