一、请求优化核心思路
1.1 优化四原则
请求优化金字塔:
┌─────────────┐
│ 不发请求 │ ← 最优(缓存、预加载)
├─────────────┤
│ 少发请求 │ ← 次优(合并、懒加载)
├─────────────┤
│ 小请求 │ ← 优化(压缩、裁剪)
├─────────────┤
│ 快请求 │ ← 基础(CDN、HTTP/2)
└─────────────┘
优化策略:
1️⃣ 能不发就不发(缓存)
2️⃣ 能少发就少发(合并、懒加载)
3️⃣ 能小发就小发(压缩、精简)
4️⃣ 能快发就快发(CDN、并行)
1.2 常见问题分类
| 请求数量过多 | 127个请求 | 浏览器并发限制、队列等待 | 🔴 高 |
| 单次请求过大 | 单个API 3.2MB | 网络传输慢、解析慢 | 🔴 高 |
| 请求时机不当 | 首屏全量加载 | 白屏时间长 | 🔴 高 |
| 无缓存策略 | 每次全量请求 | 浪费带宽、体验差 | ⚠️ 中 |
| 串行请求 | 瀑布流 | 总时间长 | ⚠️ 中 |
二、减少请求数量
2.1 资源合并
2.1.1 图片雪碧图(CSS Sprites)
问题场景:
<!– 优化前:20个小图标 = 20个请求 –>
<img src="/icons/user.png">
<img src="/icons/cart.png">
<img src="/icons/search.png">
<!– … 17个图标 … –>
问题:
– 20个HTTP请求
– 每个请求都有TCP握手、排队时间
– 浏览器并发限制(6个/域名)
解决方案:
/* 优化后:1个雪碧图 = 1个请求 */
.sprite {
background-image: url('/images/sprite.png');
background-repeat: no-repeat;
display: inline-block;
}
.icon-user {
width: 20px;
height: 20px;
background-position: 0 0;
}
.icon-cart {
width: 20px;
height: 20px;
background-position: -20px 0;
}
.icon-search {
width: 20px;
height: 20px;
background-position: -40px 0;
}
自动化工具:
# 使用 webpack-spritesmith
npm install webpack-spritesmith –save-dev
# webpack.config.js
const SpritesmithPlugin = require('webpack-spritesmith');
module.exports = {
plugins: [
new SpritesmithPlugin({
src: {
cwd: path.resolve(__dirname, 'src/assets/icons'),
glob: '*.png'
},
target: {
image: path.resolve(__dirname, 'dist/sprite.png'),
css: path.resolve(__dirname, 'dist/sprite.css')
}
})
]
};
现代替代方案(推荐):
<!– 使用 SVG Sprite(更灵活) –>
<svg class="icon">
<use xlink:href="#icon-user"></use>
</svg>
<!– 使用 Icon Font –>
<i class="iconfont icon-user"></i>
<!– 使用内联SVG(最优) –>
<svg width="20" height="20" viewBox="0 0 20 20">
<path d="M10 0C4.48…"/>
</svg>
2.1.2 JS/CSS合并
// 优化前:多个文件
<script src="jquery.js"></script>
<script src="lodash.js"></script>
<script src="utils.js"></script>
<script src="app.js"></script>
// 4个请求
// 优化后:Webpack打包
<script src="bundle.js"></script>
// 1个请求
// webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.[contenthash].js'
},
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\\\/]node_modules[\\\\/]/,
name: 'vendors',
priority: 10
}
}
}
}
};
注意事项:
⚠️ 合并不是越多越好!
过度合并的问题:
❌ Bundle过大(超过500KB)
❌ 解析时间长(阻塞渲染)
❌ 缓存失效(一个文件改动,全部失效)
最佳实践:
✅ 按路由分割(Route-based splitting)
✅ 按组件分割(Component-based splitting)
✅ 第三方库单独打包(Vendor splitting)
2.2 接口合并
2.2.1 GraphQL(推荐)
问题场景:
// RESTful API:3个请求获取用户完整信息
async function getUserInfo(userId) {
const user = await fetch(`/api/users/${userId}`); // 请求1
const posts = await fetch(`/api/users/${userId}/posts`); // 请求2
const comments = await fetch(`/api/users/${userId}/comments`); // 请求3
return { user, posts, comments };
}
// 总耗时:500ms + 300ms + 200ms = 1000ms(串行)
GraphQL方案:
// 1个请求获取所有数据
const query = `
query GetUser($userId: ID!) {
user(id: $userId) {
id
name
avatar
posts {
id
title
content
}
comments {
id
content
}
}
}
`;
const data = await graphqlClient.query(query, { userId: 123 });
// 总耗时:500ms(1个请求)
// 节省:500ms(50%)
优势:
✅ 请求数量:3个 → 1个
✅ 数据精确:只返回需要的字段
✅ 避免过度获取(Over-fetching)
✅ 避免获取不足(Under-fetching)
2.2.2 BFF(Backend For Frontend)
架构设计:
传统方式:
前端 → 用户服务 (200ms)
→ 订单服务 (300ms)
→ 商品服务 (250ms)
总耗时:750ms(串行)或 300ms(并行,但请求多)
BFF方式:
前端 → BFF层 → 用户服务
→ 订单服务
→ 商品服务
BFF聚合数据 → 返回前端
总耗时:350ms(BFF内部并行 + 聚合)
实现示例(Node.js BFF):
// BFF层:聚合多个微服务数据
app.get('/api/page/dashboard', async (req, res) => {
// 并行请求多个服务
const [user, orders, products] = await Promise.all([
fetch('http://user-service/api/user'),
fetch('http://order-service/api/orders'),
fetch('http://product-service/api/products')
]);
// 数据聚合、字段裁剪
const result = {
user: {
id: user.id,
name: user.name
// 只返回前端需要的字段
},
orderCount: orders.total,
topProducts: products.slice(0, 5)
};
res.json(result);
});
// 前端:只需1个请求
const data = await fetch('/api/page/dashboard');
2.2.3 批量接口设计
// 优化前:循环请求(N个请求)
async function getProductDetails(productIds) {
const promises = productIds.map(id =>
fetch(`/api/products/${id}`)
);
return Promise.all(promises);
}
getProductDetails([1, 2, 3, 4, 5]); // 5个请求
// 优化后:批量接口(1个请求)
async function getProductDetailsBatch(productIds) {
return fetch('/api/products/batch', {
method: 'POST',
body: JSON.stringify({ ids: productIds })
});
}
getProductDetailsBatch([1, 2, 3, 4, 5]); // 1个请求
// 后端实现(示例)
app.post('/api/products/batch', async (req, res) => {
const { ids } = req.body;
const products = await ProductModel.find({
_id: { $in: ids }
});
res.json(products);
});
2.3 懒加载与按需加载
2.3.1 路由懒加载
// React示例
// 优化前:所有路由组件一次性加载
import Home from './pages/Home';
import About from './pages/About';
import Products from './pages/Products';
import Dashboard from './pages/Dashboard';
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
{ path: '/products', component: Products },
{ path: '/dashboard', component: Dashboard }
];
// Bundle大小:2.5 MB(包含所有页面)
// 优化后:路由懒加载
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Products = lazy(() => import('./pages/Products'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
{ path: '/products', component: Products },
{ path: '/dashboard', component: Dashboard }
];
// 首屏Bundle:500 KB(只加载Home页面)
// 其他页面:按需加载(进入时才下载)
// 使用
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
{routes.map(route => (
<Route key={route.path} {…route} />
))}
</Routes>
</Suspense>
);
}
Vue示例:
// Vue Router懒加载
const routes = [
{
path: '/',
component: () => import('./views/Home.vue')
},
{
path: '/about',
component: () => import('./views/About.vue')
}
];
// 分组打包(相关页面打包在一起)
const routes = [
{
path: '/user/profile',
component: () => import(/* webpackChunkName: "user" */ './views/Profile.vue')
},
{
path: '/user/settings',
component: () => import(/* webpackChunkName: "user" */ './views/Settings.vue')
}
];
// 生成 user.[hash].js(包含Profile和Settings)
2.3.2 组件懒加载
// React.lazy
// 优化前:ECharts图表库始终加载(500KB)
import ReactECharts from 'echarts-for-react';
function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
{showChart && <ReactECharts option={option} />}
</div>
);
}
// 优化后:点击时才加载
const ReactECharts = lazy(() => import('echarts-for-react'));
function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<h1>Dashboard</h1>
<button onClick={() => setShowChart(true)}>显示图表</button>
{showChart && (
<Suspense fallback={<div>加载中…</div>}>
<ReactECharts option={option} />
</Suspense>
)}
</div>
);
}
// 首次加载:不包含500KB的ECharts
// 点击按钮后:动态加载ECharts
2.3.3 图片懒加载
// 方案1:原生Intersection Observer
function LazyImage({ src, alt }) {
const imgRef = useRef();
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
observer.observe(imgRef.current);
return () => observer.disconnect();
}, []);
return <img ref={imgRef} data–src={src} alt={alt} />;
}
// 方案2:loading="lazy"(原生支持,最简单)
<img src="image.jpg" loading="lazy" alt="产品图片" />
// 方案3:react-lazyload(第三方库)
import LazyLoad from 'react-lazyload';
<LazyLoad height={200} offset={100}>
<img src="image.jpg" alt="产品图片" />
</LazyLoad>
效果对比:
优化前:
– 页面加载时:下载所有图片(63个,9.5MB)
– 网络耗时:8秒
优化后(懒加载):
– 页面加载时:只下载首屏图片(8个,1.2MB)
– 网络耗时:1.5秒
– 滚动时:按需加载剩余图片
2.4 预加载与预连接
2.4.1 dns-prefetch(DNS预解析)
<!– 提前解析第三方域名 –>
<link rel="dns-prefetch" href="https://cdn.example.com">
<link rel="dns-prefetch" href="https://api.example.com">
<link rel="dns-prefetch" href="https://analytics.google.com">
效果:
– 节省DNS查询时间(20-120ms)
– 适用于即将访问的域名
2.4.2 preconnect(预连接)
<!– 提前建立TCP连接 –>
<link rel="preconnect" href="https://cdn.example.com">
<link rel="preconnect" href="https://api.example.com">
效果:
– DNS解析 + TCP握手 + TLS协商
– 节省时间:100-300ms
– 注意:最多3-6个(浏览器限制)
2.4.3 prefetch(预获取)
<!– 预获取下一个页面可能用到的资源 –>
<link rel="prefetch" href="/next-page.js">
<link rel="prefetch" href="/next-page.css">
<!– React Router中动态预获取 –>
<Link
to="/products"
onMouseEnter={() => {
import('./pages/Products'); // 鼠标悬停时预加载
}}
>
商品列表
</Link>
效果:
– 空闲时下载资源(低优先级)
– 下次访问:从缓存加载(瞬间打开)
2.4.4 preload(预加载)
<!– 预加载关键资源 –>
<link rel="preload" href="/fonts/custom.woff2" as="font" crossorigin>
<link rel="preload" href="/critical.css" as="style">
<link rel="preload" href="/hero-image.jpg" as="image">
效果:
– 高优先级加载
– 避免阻塞渲染
– 适用于首屏关键资源
四种预加载对比:
┌─────────────┬──────────────┬──────────┬──────────┐
│ 技术 │ 时机 │ 优先级 │ 适用场景 │
├─────────────┼──────────────┼──────────┼──────────┤
│ dns-prefetch│ 提前DNS解析 │ 低 │ 第三方域名│
│ preconnect │ 提前建立连接 │ 中 │ API域名 │
│ prefetch │ 空闲时下载 │ 低 │ 下一页面 │
│ preload │ 立即下载 │ 高 │ 关键资源 │
└─────────────┴──────────────┴──────────┴──────────┘
三、减小请求体积
3.1 数据压缩
3.1.1 Gzip / Brotli压缩
# Nginx配置
http {
# 开启Gzip
gzip on;
gzip_min_length 1k;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript;
gzip_vary on;
# 开启Brotli(更高压缩率)
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/json application/javascript;
}
压缩效果:
┌──────────────┬──────────┬──────────┬──────────┐
│ 文件类型 │ 原始大小 │ Gzip压缩 │ Brotli压缩│
├──────────────┼──────────┼──────────┼──────────┤
│ app.js │ 500 KB │ 150 KB │ 120 KB │
│ styles.css │ 200 KB │ 40 KB │ 30 KB │
│ data.json │ 1 MB │ 100 KB │ 80 KB │
└──────────────┴──────────┴──────────┴──────────┘
压缩率:70-90%
3.1.2 图片压缩
# 使用imagemin-webpack-plugin
npm install imagemin-webpack-plugin –save-dev
# webpack.config.js
const ImageminPlugin = require('imagemin-webpack-plugin').default;
module.exports = {
plugins: [
new ImageminPlugin({
pngquant: {
quality: '80-90' // PNG压缩质量
},
jpegtran: {
progressive: true // JPEG渐进式
},
optipng: {
optimizationLevel: 5 // PNG优化级别
}
})
]
};
压缩效果:
原图:2 MB(4000×3000 PNG)
压缩后:200 KB(质量几乎无损)
压缩率:90%
现代图片格式:
<!– WebP格式(推荐) –>
<picture>
<source srcset="image.webp" type="image/webp">
<source srcset="image.jpg" type="image/jpeg">
<img src="image.jpg" alt="产品图片">
</picture>
格式对比:
┌──────┬──────────┬────────┬────────┐
│ 格式 │ 文件大小 │ 压缩率 │ 支持度 │
├──────┼──────────┼────────┼────────┤
│ JPEG │ 150 KB │ 基准 │ 100% │
│ PNG │ 200 KB │ -33% │ 100% │
│ WebP │ 80 KB │ +47% │ 96% │
│ AVIF │ 50 KB │ +67% │ 85% │
└──────┴──────────┴────────┴────────┘
3.2 数据裁剪
3.2.1 字段过滤
// 后端:只返回前端需要的字段
// 优化前:返回完整用户对象
app.get('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
res.json(user);
});
// 响应数据:2 KB(包含password、internalId等敏感字段)
// 优化后:字段过滤
app.get('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id)
.select('id name avatar email createdAt'); // 只选择需要的字段
res.json(user);
});
// 响应数据:300 B(减少85%)
// 前端:GraphQL方式
query GetUser($id: ID!) {
user(id: $id) {
id
name
avatar
# 只请求需要的字段
}
}
3.2.2 分页加载
// 优化前:一次性返回2000条数据
app.get('/api/products', async (req, res) => {
const products = await Product.find();
res.json(products);
});
// 响应大小:3.2 MB
// 优化后:分页
app.get('/api/products', async (req, res) => {
const { page = 1, pageSize = 20 } = req.query;
const products = await Product.find()
.skip((page – 1) * pageSize)
.limit(pageSize);
const total = await Product.countDocuments();
res.json({
data: products,
pagination: {
page: parseInt(page),
pageSize: parseInt(pageSize),
total,
totalPages: Math.ceil(total / pageSize)
}
});
});
// 首次响应:32 KB(20条数据)
// 减少:99%
3.2.3 虚拟滚动(Virtual Scrolling)
// React-Window示例
import { FixedSizeList } from 'react-window';
// 优化前:渲染2000个DOM节点
function ProductList({ products }) {
return (
<div>
{products.map(product => (
<ProductCard key={product.id} data={product} />
))}
</div>
);
}
// 问题:
// – 2000个DOM节点
// – 渲染时间:1.5s
// – 内存占用:150 MB
// 优化后:虚拟滚动(只渲染可见部分)
function ProductList({ products }) {
return (
<FixedSizeList
height={600} // 容器高度
itemCount={products.length}
itemSize={100} // 每项高度
width="100%"
>
{({ index, style }) => (
<div style={style}>
<ProductCard data={products[index]} />
</div>
)}
</FixedSizeList>
);
}
// 优化效果:
// – 只渲染7个可见DOM节点(600/100 = 6,+1缓冲)
// – 渲染时间:50ms
// – 内存占用:5 MB
3.3 Tree Shaking(树摇)
// 优化前:引入整个lodash库
import _ from 'lodash';
_.debounce(func, 300);
// Bundle增加:70 KB
// 优化后:按需引入
import debounce from 'lodash/debounce';
// Bundle增加:2 KB
// 更好的方式:使用lodash-es(支持Tree Shaking)
import { debounce } from 'lodash-es';
// 配合Webpack Tree Shaking,自动移除未使用代码
// package.json配置
{
"sideEffects": false // 标记为无副作用,允许Tree Shaking
}
// webpack.config.js
module.exports = {
mode: 'production', // 生产模式自动开启Tree Shaking
optimization: {
usedExports: true // 标记未使用的导出
}
};
四、提升请求速度
4.1 CDN加速
<!– 优化前:静态资源从源站加载 –>
<script src="https://www.example.com/static/app.js"></script>
<!– 用户在广州,服务器在北京 –>
<!– 网络延迟:50ms RTT × 3次握手 = 150ms –>
<!– 下载时间:500KB ÷ 5MB/s = 100ms –>
<!– 总耗时:250ms –>
<!– 优化后:使用CDN –>
<script src="https://cdn.example.com/static/app.js"></script>
<!– CDN节点在广州(就近访问)–>
<!– 网络延迟:5ms RTT × 3次握手 = 15ms –>
<!– 下载时间:500KB ÷ 50MB/s = 10ms –>
<!– 总耗时:25ms –>
<!– 提升:90% –>
CDN配置示例(阿里云OSS + CDN):
1. 上传静态资源到OSS
2. 绑定CDN域名
3. 配置缓存策略:
– HTML: 不缓存
– JS/CSS: 1年(文件名带hash)
– 图片: 1年
4.2 HTTP/2
HTTP/1.1问题:
– 浏览器限制6个并发连接
– 队头阻塞(HOL Blocking)
– 重复的Header
HTTP/2优势:
✅ 多路复用(Multiplexing)
– 1个连接,无限制并发请求
– 解决队头阻塞
✅ Header压缩(HPACK)
– 减少重复Header
– 节省带宽
✅ Server Push(服务器推送)
– 主动推送CSS、JS
– 无需等待请求
效果对比:
HTTP/1.1:加载100个资源
├── 6个并发
├── 需要17轮(100/6)
├── 每轮等待延迟:50ms
└── 总额外延迟:850ms
HTTP/2:加载100个资源
├── 无限并发
├── 1轮完成
├── 延迟:50ms
└── 节省:800ms
Nginx启用HTTP/2:
server {
listen 443 ssl http2; # 启用HTTP/2
server_name example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
}
4.3 请求并行化
// 优化前:串行请求
async function loadData() {
const user = await fetch('/api/user'); // 300ms
const orders = await fetch('/api/orders'); // 400ms
const products = await fetch('/api/products');// 200ms
return { user, orders, products };
}
// 总耗时:900ms
// 优化后:并行请求
async function loadData() {
const [user, orders, products] = await Promise.all([
fetch('/api/user'),
fetch('/api/orders'),
fetch('/api/products')
]);
return { user, orders, products };
}
// 总耗时:400ms(最慢的请求)
// 提升:55%
// 进阶:并发控制(避免同时发起100个请求)
async function batchFetch(urls, concurrency = 6) {
const results = [];
const queue = […urls];
async function worker() {
while (queue.length > 0) {
const url = queue.shift();
const result = await fetch(url);
results.push(result);
}
}
// 启动6个并发worker
await Promise.all(
Array(concurrency).fill(0).map(() => worker())
);
return results;
}
// 使用
await batchFetch(productUrls, 6);
五、智能缓存策略
5.1 HTTP缓存
5.1.1 强缓存(Cache-Control)
# Nginx配置
location ~* \\.(js|css|png|jpg|jpeg|gif|ico|woff2)$ {
expires 1y; # 1年
add_header Cache-Control "public, immutable";
}
location ~* \\.html$ {
expires -1; # 不缓存
add_header Cache-Control "no-cache";
}
效果:
第一次访问:
– app.123abc.js (500 KB) – 下载耗时 500ms
第二次访问(文件未变):
– app.123abc.js – 从缓存读取 (0ms) ✅
文件更新后:
– app.456def.js (500 KB) – 下载新文件
– 旧缓存自动失效(文件名变了)
5.1.2 协商缓存(ETag / Last-Modified)
工作流程:
1. 首次请求
浏览器 → 服务器:GET /api/products
服务器 → 浏览器:200 OK
ETag: "v1.0"
Data: {…}
2. 再次请求
浏览器 → 服务器:GET /api/products
If-None-Match: "v1.0"
如果数据未变:
服务器 → 浏览器:304 Not Modified
(无Body,节省带宽)
如果数据已变:
服务器 → 浏览器:200 OK
ETag: "v2.0"
Data: {…}
效果:
– 节省带宽(304响应无Body)
– 减少服务器压力(数据未变时无需查询DB)
后端实现(Express):
const express = require('express');
const etag = require('etag');
app.get('/api/products', async (req, res) => {
const products = await getProducts();
const dataString = JSON.stringify(products);
const hash = etag(dataString);
// 检查客户端ETag
if (req.headers['if-none-match'] === hash) {
return res.status(304).end(); // 304 Not Modified
}
res.setHeader('ETag', hash);
res.json(products);
});
5.2 前端缓存
5.2.1 Memory Cache(内存缓存)
// 简单的内存缓存
const cache = new Map();
async function fetchWithCache(url, ttl = 60000) {
const cached = cache.get(url);
if (cached && Date.now() – cached.time < ttl) {
console.log('从缓存读取');
return cached.data;
}
console.log('发起请求');
const data = await fetch(url).then(r => r.json());
cache.set(url, {
data,
time: Date.now()
});
return data;
}
// 使用
const products = await fetchWithCache('/api/products', 5 * 60 * 1000); // 5分钟缓存
5.2.2 LocalStorage缓存
// 带过期时间的LocalStorage
function setCache(key, data, ttl = 3600000) {
const item = {
data,
expiry: Date.now() + ttl
};
localStorage.setItem(key, JSON.stringify(item));
}
function getCache(key) {
const itemStr = localStorage.getItem(key);
if (!itemStr) return null;
const item = JSON.parse(itemStr);
if (Date.now() > item.expiry) {
localStorage.removeItem(key);
return null;
}
return item.data;
}
// 封装fetch
async function fetchWithLocalStorage(url, ttl = 3600000) {
const cached = getCache(url);
if (cached) {
return cached;
}
const data = await fetch(url).then(r => r.json());
setCache(url, data, ttl);
return data;
}
// 使用
const userData = await fetchWithLocalStorage('/api/user', 30 * 60 * 1000); // 30分钟
5.2.3 Service Worker缓存
// sw.js(Service Worker)
const CACHE_NAME = 'v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/scripts/app.js'
];
// 安装时缓存资源
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
});
// 拦截请求
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// 缓存命中:返回缓存
if (response) {
return response;
}
// 缓存未命中:发起请求
return fetch(event.request).then(response => {
// 缓存新请求
if (response.status === 200) {
const responseClone = response.clone();
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, responseClone);
});
}
return response;
});
})
);
});
// 注册Service Worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
5.3 数据预取
// React Query示例
import { useQuery } from 'react-query';
function ProductList() {
const { data: products } = useQuery('products', fetchProducts, {
staleTime: 5 * 60 * 1000, // 5分钟内认为数据是新鲜的
cacheTime: 30 * 60 * 1000, // 缓存30分钟
refetchOnWindowFocus: false // 窗口聚焦时不重新请求
});
return <div>{/* 渲染产品列表 */}</div>;
}
// SWR示例
import useSWR from 'swr';
function Profile() {
const { data, error } = useSWR('/api/user', fetcher, {
revalidateOnFocus: false,
dedupingInterval: 60000 // 60秒内相同请求返回缓存
});
return <div>{data.name}</div>;
}
六、实战案例
6.1 案例回顾
优化目标:
– 请求数量:127个 → <30个
– 页面大小:15.2 MB → <2 MB
– 加载时间:8秒 → <2秒
6.2 优化实施
阶段1:减少请求数量(127 → 35)
1. 图片懒加载
– 首屏外的图片:53个 → 0个(滚动时加载)
– 节省请求:53个
2. 路由懒加载
– 非当前页面组件:15个 → 0个
– 节省请求:15个
3. 接口合并
– 商品详情请求:20个 → 1个批量接口
– 节省请求:19个
4. 雪碧图
– 小图标:8个 → 1个sprite
– 节省请求:7个
总计:127 → 33个请求
阶段2:减小请求体积(15.2 MB → 1.8 MB)
1. API数据分页
– 商品列表:3.2 MB (2000条) → 100 KB (20条)
– 节省:3.1 MB
2. 图片优化
– 压缩 + WebP:9.5 MB → 1.2 MB
– 节省:8.3 MB
3. JS Bundle优化
– Tree Shaking + 代码分割:2.5 MB → 600 KB
– 节省:1.9 MB
4. Gzip压缩
– 文本资源压缩:剩余1.8 MB → 500 KB
– 节省:1.3 MB
总计:15.2 MB → 1.8 MB(未压缩)→ 800 KB(Gzip后)
阶段3:提升请求速度
1. 启用CDN
– 静态资源加载时间:500ms → 50ms
– 提升:90%
2. 启用HTTP/2
– 并发限制:6个 → 无限制
– 减少队列等待:300ms
3. 预连接API域名
<link rel="preconnect" href="https://api.example.com">
– 节省连接时间:100ms
4. 请求并行化
– 串行请求:900ms → 并行:400ms
– 节省:500ms
阶段4:缓存策略
1. 强缓存(静态资源)
– JS/CSS/图片:Cache-Control: max-age=31536000
– 二次访问:0ms(从缓存)
2. API缓存(React Query)
– 用户信息:缓存5分钟
– 商品分类:缓存30分钟
– 减少重复请求:60%
3. Service Worker
– 离线访问支持
– 秒开体验
6.3 优化成果
最终效果对比:
┌──────────────┬──────────┬──────────┬──────────┐
│ 指标 │ 优化前 │ 优化后 │ 提升 │
├──────────────┼──────────┼──────────┼──────────┤
│ 请求数量 │ 127个 │ 33个 │ 74% ✅ │
│ 首次加载大小 │ 15.2 MB │ 1.8 MB │ 88% ✅ │
│ Gzip后大小 │ 3.5 MB │ 800 KB │ 77% ✅ │
│ 加载时间 │ 8.0s │ 1.6s │ 80% ✅ │
│ FCP │ 4.2s │ 0.9s │ 79% ✅ │
│ LCP │ 8.1s │ 1.8s │ 78% ✅ │
│ Lighthouse │ 32分 │ 92分 │ +60分✅ │
└──────────────┴──────────┴──────────┴──────────┘
二次访问(有缓存):
– 加载时间:1.6s → 0.3s
– 请求数量:33个 → 5个(仅API请求)
七、总结
7.1 优化清单
☑️ 减少请求数量
– [ ] 静态资源合并(雪碧图、Bundle)
– [ ] 接口合并(批量、GraphQL、BFF)
– [ ] 懒加载(路由、组件、图片)
– [ ] 预加载(preload、prefetch、dns-prefetch)
☑️ 减小请求体积
– [ ] 数据压缩(Gzip、Brotli)
– [ ] 图片优化(压缩、WebP、AVIF)
– [ ] 数据裁剪(字段过滤、分页)
– [ ] Tree Shaking(移除未使用代码)
☑️ 提升请求速度
– [ ] CDN加速
– [ ] HTTP/2
– [ ] 请求并行化
– [ ] 域名收敛
☑️ 缓存策略
– [ ] HTTP缓存(强缓存、协商缓存)
– [ ] 前端缓存(Memory、LocalStorage)
– [ ] Service Worker
– [ ] 数据预取
7.2 优化优先级
🔴 P0级(立即执行):
1. API分页(数据量大 → 首屏慢)
2. 图片懒加载(请求多 → 并发阻塞)
3. 路由懒加载(Bundle大 → 解析慢)
⚠️ P1级(重要):
4. 图片压缩 + WebP
5. 启用Gzip/Brotli
6. 启用CDN
⭕ P2级(优化):
7. 接口合并(GraphQL/BFF)
8. HTTP/2
9. Service Worker
网硕互联帮助中心





评论前必须登录!
注册