电商网站秒级加载优化实战
项目背景
客户情况
某中型电商网站,日均 PV 约 50 万,主要经营电子数码产品。网站使用 Vue 2 + Nuxt 2 构建,后端为 Node.js + MongoDB 的架构。
2024 年初,客户反馈网站"越来越慢",用户投诉增多,移动端跳出率持续上升。经过初步测试,我们发现问题确实严重。
初始状态
在优化前,我们对网站进行了全面测试,结果令人担忧:
| 指标 | 测试结果 | 目标值 | 状态 |
|---|---|---|---|
| 首屏加载时间 (FCP) | 4.2 秒 | < 1.8 秒 | 🔴 严重 |
| 最大内容绘制 (LCP) | 8.1 秒 | < 2.5 秒 | 🔴 严重 |
| 累积布局偏移 (CLS) | 0.35 | < 0.1 | 🔴 严重 |
| 首次输入延迟 (FID) | 280ms | < 100ms | 🔴 严重 |
| 完全加载时间 | 12.6 秒 | < 5 秒 | 🔴 严重 |
| 页面总大小 | 8.2 MB | < 2 MB | 🔴 严重 |
| 请求数量 | 186 个 | < 50 个 | 🔴 严重 |
Lighthouse 评分:Performance 23/100
这些数据意味着什么?
- 用户体验极差:8 秒的 LCP 意味着用户要等待 8 秒才能看到主要内容,大多数人会在这之前离开
- 搜索排名受损:Core Web Vitals 是 Google 排名因素,这样的表现会严重影响 SEO
- 转化率低下:每增加 1 秒加载时间,转化率下降约 7%
- 服务器压力大:186 个请求意味着服务器负担重,成本高
业务影响
通过分析历史数据,我们发现:
- 过去 6 个月,移动端跳出率从 45% 上升到 68%
- 购物车放弃率达到 78%
- 用户平均停留时间下降 40%
- 自然搜索流量下降 25%
估算损失:以日均 50 万 PV、2% 转化率、客单价 500 元计算,如果能将跳出率降低 10%,每月可增加约 150 万元收入。
问题诊断
在开始优化前,我们需要找出问题的根源。盲目优化往往事倍功半,正确的诊断是成功的一半。
诊断工具与方法
我们使用了以下工具进行全面诊断:
1. Chrome DevTools
- Network 面板:分析资源加载瀑布图
- Performance 面板:分析运行时性能
- Coverage 面板:分析代码利用率
2. Lighthouse
- 自动化性能审计
- 具体优化建议
3. WebPageTest
- 多地点测试
- 详细的瀑布图分析
- 视频对比功能
4. Real User Monitoring (RUM)
- 真实用户数据
- 地理分布分析
- 设备分布分析
瓶颈分析
经过详细分析,我们发现以下主要问题:
问题一:巨大的 JavaScript Bundle
主 Bundle 大小达到 2.8 MB(未压缩),包含了:
- 整个 lodash 库(仅使用了 3 个函数)
- 完整的 moment.js(仅用于日期格式化)
- 多个未使用的第三方组件库
- 所有路由组件打包在一起
bundle 分析结果:
├── vendor.js: 1.8 MB
│ ├── lodash: 530 KB
│ ├── moment + locales: 480 KB
│ ├── element-ui (full): 420 KB
│ └── echarts (full): 370 KB
├── app.js: 680 KB
└── 其他 chunks: 320 KB
问题二:未优化的图片资源
首页加载了 45 张产品图片,总计 4.2 MB:
- 图片格式:全部是 PNG/JPG,没有使用 WebP
- 图片尺寸:原始尺寸上传,没有根据显示尺寸压缩
- 加载方式:同时加载所有图片,没有懒加载
- CDN 使用:部分图片没有走 CDN
问题三:阻塞渲染的资源
检查发现大量阻塞渲染的资源:
- 16 个第三方 CSS 文件
- 8 个同步加载的 JS 文件
- 多个 Google 字体文件
- 未优化的图标字体(完整的 Font Awesome)
问题四:服务端性能问题
API 响应时间分析:
- 首页数据接口:平均 1.2 秒
- 产品列表接口:平均 800ms
- 数据库查询未优化,缺乏索引
问题五:缺乏缓存策略
- 静态资源没有设置长期缓存
- API 响应没有缓存
- 服务端渲染没有缓存
优先级排序
基于影响程度和实施难度,我们制定了优化优先级:
| 优化项 | 预期收益 | 实施难度 | 优先级 |
|---|---|---|---|
| 图片优化 | 减少 3+ MB | 低 | P0 |
| JS Bundle 拆分 | 减少首屏 JS 60% | 中 | P0 |
| 懒加载实现 | 减少初始请求 50% | 低 | P0 |
| 关键 CSS 提取 | FCP 提升 1s+ | 中 | P1 |
| API 优化 | 响应时间 -50% | 中 | P1 |
| 缓存策略 | 重复访问加速 | 低 | P2 |
优化实施
第一阶段:图片优化(效果最显著)
图片优化通常是性价比最高的优化方向,我们从这里开始。
步骤 1:图片格式转换
将所有图片转换为 WebP 格式,同时保留 JPG 作为降级方案:
<!-- 使用 picture 元素实现格式回退 -->
<picture>
<source srcset="/images/product.webp" type="image/webp">
<source srcset="/images/product.jpg" type="image/jpeg">
<img src="/images/product.jpg" alt="产品图片">
</picture>
效果:图片体积平均减少 45%
步骤 2:响应式图片
根据设备屏幕提供不同尺寸的图片:
<img
srcset="
/images/product-400.webp 400w,
/images/product-800.webp 800w,
/images/product-1200.webp 1200w
"
sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
src="/images/product-800.webp"
alt="产品图片"
>
效果:移动端图片加载量减少 60%
步骤 3:图片懒加载
只加载视口内的图片,其他图片延迟加载:
// 使用 Intersection Observer 实现懒加载
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target
img.src = img.dataset.src
if (img.dataset.srcset) {
img.srcset = img.dataset.srcset
}
imageObserver.unobserve(img)
}
})
}, {
rootMargin: '200px' // 提前 200px 开始加载
})
document.querySelectorAll('img[data-src]').forEach(img => {
imageObserver.observe(img)
})
效果:首屏图片请求从 45 个减少到 8 个
步骤 4:配置 CDN 和缓存
确保所有图片通过 CDN 分发,并设置合理的缓存:
# Nginx 配置
location ~* \.(jpg|jpeg|png|gif|webp|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header Vary "Accept-Encoding";
}
图片优化阶段成果:
| 指标 | 优化前 | 优化后 | 改善 |
|---|---|---|---|
| 图片总大小 | 4.2 MB | 890 KB | -79% |
| 首屏图片请求 | 45 个 | 8 个 | -82% |
| LCP | 8.1 秒 | 5.2 秒 | -36% |
第二阶段:JavaScript 优化
JS Bundle 是第二大性能瓶颈,需要系统性优化。
步骤 1:代码分割
将单一的大 Bundle 拆分为多个小 chunks:
// nuxt.config.js - 配置代码分割
export default {
build: {
optimization: {
splitChunks: {
chunks: 'all',
maxSize: 244 * 1024, // 最大 244KB
cacheGroups: {
// 分离 Vue 相关
vue: {
test: /[\\/]node_modules[\\/](vue|vuex|vue-router)[\\/]/,
name: 'vue-vendor',
priority: 20
},
// 分离 UI 组件库
ui: {
test: /[\\/]node_modules[\\/](element-ui)[\\/]/,
name: 'ui-vendor',
priority: 15
},
// 其他第三方库
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
priority: 10
}
}
}
}
}
}
步骤 2:路由懒加载
每个路由组件独立打包,按需加载:
// router/index.js
const routes = [
{
path: '/',
component: () => import(/* webpackChunkName: "home" */ '@/pages/Home.vue')
},
{
path: '/product/:id',
component: () => import(/* webpackChunkName: "product" */ '@/pages/Product.vue')
},
{
path: '/cart',
component: () => import(/* webpackChunkName: "cart" */ '@/pages/Cart.vue')
}
// ...更多路由
]
步骤 3:替换重量级库
| 原库 | 替代方案 | 大小变化 |
|---|---|---|
| lodash (全量) | lodash-es (按需) | 530KB → 12KB |
| moment.js | dayjs | 480KB → 2KB |
| element-ui (全量) | 按需引入 | 420KB → 85KB |
// 按需引入 Element UI
import { Button, Input, Table, Pagination } from 'element-ui'
Vue.use(Button)
Vue.use(Input)
Vue.use(Table)
Vue.use(Pagination)
// 使用 dayjs 替代 moment
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import 'dayjs/locale/zh-cn'
dayjs.extend(relativeTime)
dayjs.locale('zh-cn')
步骤 4:Tree Shaking 优化
确保 webpack 可以正确进行 Tree Shaking:
// package.json
{
"sideEffects": [
"*.css",
"*.scss"
]
}
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
modules: false // 保持 ES 模块格式,启用 tree shaking
}]
]
}
JavaScript 优化阶段成果:
| 指标 | 优化前 | 优化后 | 改善 |
|---|---|---|---|
| JS 总大小 | 2.8 MB | 680 KB | -76% |
| 首屏 JS | 2.8 MB | 180 KB | -94% |
| 解析时间 | 1.8 秒 | 320ms | -82% |
第三阶段:关键渲染路径优化
消除阻塞渲染的资源,让页面更快地开始渲染。
步骤 1:关键 CSS 内联
提取首屏需要的关键 CSS,内联到 HTML 中:
// nuxt.config.js - 使用 critters 提取关键 CSS
export default {
buildModules: [
['@nuxtjs/critters', {
preload: 'swap',
fonts: true
}]
]
}
结果示例:
<head>
<!-- 关键 CSS 内联 -->
<style>
/* 首屏关键样式 */
.header { /* ... */ }
.hero { /* ... */ }
.product-grid { /* ... */ }
</style>
<!-- 非关键 CSS 异步加载 -->
<link rel="preload" href="/css/full.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/css/full.css"></noscript>
</head>
步骤 2:字体优化
问题:完整的 Font Awesome(1.2 MB)只用了 20 个图标
解决方案:
- 使用 SVG 图标替代图标字体
- 或使用 Font Awesome 的子集化工具
// 使用 SVG 图标组件
// components/icons/CartIcon.vue
<template>
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M7 18c-1.1 0-1.99.9-1.99 2S5.9 22 7 22s2-.9 2-2-.9-2-2-2z..."/>
</svg>
</template>
对于必须使用的 Web 字体:
<!-- 预加载关键字体 -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<!-- 使用 font-display: swap -->
<style>
@font-face {
font-family: 'MainFont';
src: url('/fonts/main.woff2') format('woff2');
font-display: swap;
}
</style>
步骤 3:预连接关键资源
<head>
<!-- 预连接到 CDN -->
<link rel="preconnect" href="https://cdn.example.com">
<!-- 预连接到 API 服务器 -->
<link rel="preconnect" href="https://api.example.com">
<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="https://analytics.google.com">
</head>
关键渲染路径优化成果:
| 指标 | 优化前 | 优化后 | 改善 |
|---|---|---|---|
| FCP | 4.2 秒 | 1.2 秒 | -71% |
| 渲染阻塞资源 | 24 个 | 3 个 | -87% |
| 关键 CSS 大小 | 420 KB | 15 KB | -96% |
第四阶段:服务端优化
前端优化接近极限后,需要关注服务端性能。
步骤 1:API 响应优化
首页数据接口从 1.2 秒降到 200ms:
// 优化前:多次数据库查询
async function getHomeData() {
const banners = await Banner.find() // 200ms
const categories = await Category.find() // 150ms
const hotProducts = await Product.find({ hot: true }).limit(10) // 400ms
const newProducts = await Product.find().sort({ createdAt: -1 }).limit(10) // 450ms
return { banners, categories, hotProducts, newProducts }
}
// 优化后:并行查询 + 索引 + 缓存
async function getHomeData() {
// 尝试从缓存获取
const cached = await redis.get('home:data')
if (cached) return JSON.parse(cached)
// 并行执行查询
const [banners, categories, hotProducts, newProducts] = await Promise.all([
Banner.find().lean(),
Category.find().lean(),
Product.find({ hot: true }).select('name price image').limit(10).lean(),
Product.find().select('name price image').sort({ createdAt: -1 }).limit(10).lean()
])
const data = { banners, categories, hotProducts, newProducts }
// 缓存 5 分钟
await redis.setex('home:data', 300, JSON.stringify(data))
return data
}
关键优化点:
- 使用
Promise.all并行查询 - 添加数据库索引
- 使用
select只查询需要的字段 - 使用
lean()返回纯 JSON,减少内存占用 - 添加 Redis 缓存
步骤 2:服务端渲染缓存
对于不经常变化的页面,缓存 SSR 结果:
// server/middleware/cache.js
const LRU = require('lru-cache')
const pageCache = new LRU({
max: 100,
maxAge: 1000 * 60 * 5 // 5 分钟
})
export default function (req, res, next) {
const cacheable = ['/'].includes(req.url)
if (!cacheable) return next()
const cached = pageCache.get(req.url)
if (cached) {
return res.send(cached)
}
// 修改 res.send 以捕获响应
const originalSend = res.send.bind(res)
res.send = (body) => {
pageCache.set(req.url, body)
originalSend(body)
}
next()
}
服务端优化成果:
| 指标 | 优化前 | 优化后 | 改善 |
|---|---|---|---|
| 首页 API 响应 | 1.2 秒 | 200ms | -83% |
| SSR 时间 | 800ms | 150ms | -81% |
| 服务器 CPU 使用率 | 75% | 35% | -53% |
第五阶段:缓存策略完善
让重复访问的用户获得更好的体验。
HTTP 缓存配置
# 静态资源长期缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# HTML 文件短期缓存
location ~* \.html$ {
expires 1h;
add_header Cache-Control "public, must-revalidate";
}
# API 响应缓存
location /api/ {
add_header Cache-Control "private, max-age=60";
}
Service Worker 缓存
// service-worker.js
const CACHE_NAME = 'ecommerce-v1'
const STATIC_ASSETS = [
'/',
'/css/critical.css',
'/js/app.js',
'/images/logo.svg'
]
// 安装时缓存静态资源
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(STATIC_ASSETS))
)
})
// 请求时优先使用缓存
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
)
})
最终成果
经过 6 周的优化,网站性能有了质的飞跃:
性能指标对比
| 指标 | 优化前 | 优化后 | 改善幅度 |
|---|---|---|---|
| LCP | 8.1 秒 | 1.4 秒 | -83% |
| FCP | 4.2 秒 | 0.9 秒 | -79% |
| CLS | 0.35 | 0.05 | -86% |
| FID | 280ms | 45ms | -84% |
| 页面大小 | 8.2 MB | 1.2 MB | -85% |
| 请求数量 | 186 个 | 38 个 | -80% |
| Lighthouse 分数 | 23 | 92 | +300% |
业务指标提升
优化上线后一个月的数据变化:
| 业务指标 | 变化 |
|---|---|
| 移动端跳出率 | 从 68% 降至 42% (-38%) |
| 页面停留时间 | 增加 65% |
| 购物车转化率 | 提升 35% |
| 自然搜索流量 | 增加 28% |
| 服务器成本 | 降低 40% |
投资回报:优化项目投入约 15 万元,上线后第一个月带来的额外收入超过 200 万元。
经验总结
优化原则
- 测量先于优化:不要凭感觉优化,用数据说话
- 关注关键路径:80% 的效果来自 20% 的优化
- 渐进式改进:分阶段实施,每阶段验证效果
- 监控持续化:建立性能监控,防止回退
优化优先级建议
基于本次项目经验,推荐以下优化顺序:
第一优先级(投入小,收益大)
├── 图片格式优化(WebP)
├── 图片懒加载
├── 关键 CSS 内联
└── 资源压缩(Gzip/Brotli)
第二优先级(投入中,收益大)
├── 代码分割
├── 路由懒加载
├── 第三方库优化
└── 缓存策略
第三优先级(投入大,收益中)
├── 服务端渲染优化
├── 数据库优化
├── CDN 部署
└── Service Worker
常见陷阱
- 过度优化:追求极致分数而忽略开发效率
- 忽视真实用户:实验室数据好但真实用户体验差
- 一劳永逸:优化后不再监控,性能逐渐回退
- 只关注技术:忽略业务场景的特殊需求
工具推荐
| 工具 | 用途 | 推荐度 |
|---|---|---|
| Lighthouse | 综合性能审计 | ⭐⭐⭐⭐⭐ |
| WebPageTest | 深度瀑布图分析 | ⭐⭐⭐⭐⭐ |
| Chrome DevTools | 开发调试 | ⭐⭐⭐⭐⭐ |
| Bundle Analyzer | JS 包分析 | ⭐⭐⭐⭐ |
| Squoosh | 图片压缩 | ⭐⭐⭐⭐ |
| GTmetrix | 性能监控 | ⭐⭐⭐⭐ |
总结
本次优化项目的成功证明:即使是"慢到不能用"的网站,通过系统性的优化也能达到秒级加载。关键在于:
- 全面诊断:找出真正的瓶颈,而不是盲目优化
- 分层实施:从易到难,每步验证
- 前后端配合:性能优化不只是前端的事
- 持续监控:建立机制防止性能回退
性能优化永远没有终点,但找对方向、用对方法,就能事半功倍。


