云计算百科
云计算领域专业知识百科平台

前端性能优化系列(二):请求优化策略

一、请求优化核心思路

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} datasrc={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

赞(0)
未经允许不得转载:网硕互联帮助中心 » 前端性能优化系列(二):请求优化策略
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!