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

一、请求优化核心思路

1.1 优化四原则

复制代码
请求优化金字塔:
        ┌─────────────┐
        │  不发请求    │ ← 最优(缓存、预加载)
        ├─────────────┤
        │  少发请求    │ ← 次优(合并、懒加载)
        ├─────────────┤
        │  小请求      │ ← 优化(压缩、裁剪)
        ├─────────────┤
        │  快请求      │ ← 基础(CDN、HTTP/2)
        └─────────────┘

优化策略:
1️⃣ 能不发就不发(缓存)
2️⃣ 能少发就少发(合并、懒加载)
3️⃣ 能小发就小发(压缩、精简)
4️⃣ 能快发就快发(CDN、并行)

1.2 常见问题分类

问题 表现 影响 优先级
请求数量过多 127个请求 浏览器并发限制、队列等待 🔴 高
单次请求过大 单个API 3.2MB 网络传输慢、解析慢 🔴 高
请求时机不当 首屏全量加载 白屏时间长 🔴 高
无缓存策略 每次全量请求 浪费带宽、体验差 ⚠️ 中
串行请求 瀑布流 总时间长 ⚠️ 中

二、减少请求数量

2.1 资源合并

2.1.1 图片雪碧图(CSS Sprites)

问题场景

html 复制代码
<!-- 优化前:20个小图标 = 20个请求 -->
<img src="/icons/user.png">
<img src="/icons/cart.png">
<img src="/icons/search.png">
<!-- ... 17个图标 ... -->

问题:
- 20个HTTP请求
- 每个请求都有TCP握手、排队时间
- 浏览器并发限制(6个/域名)

解决方案

css 复制代码
/* 优化后: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;
}

自动化工具

bash 复制代码
# 使用 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')
      }
    })
  ]
};

现代替代方案(推荐)

html 复制代码
<!-- 使用 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合并
javascript 复制代码
// 优化前:多个文件
<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(推荐)

问题场景

javascript 复制代码
// 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方案

javascript 复制代码
// 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):

javascript 复制代码
// 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 批量接口设计
javascript 复制代码
// 优化前:循环请求(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 路由懒加载
javascript 复制代码
// 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示例

javascript 复制代码
// 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 组件懒加载
javascript 复制代码
// 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 图片懒加载
javascript 复制代码
// 方案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预解析)
html 复制代码
<!-- 提前解析第三方域名 -->
<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(预连接)
html 复制代码
<!-- 提前建立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(预获取)
html 复制代码
<!-- 预获取下一个页面可能用到的资源 -->
<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(预加载)
html 复制代码
<!-- 预加载关键资源 -->
<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 复制代码
# 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 图片压缩
bash 复制代码
# 使用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(4000x3000 PNG)
压缩后:200 KB(质量几乎无损)
压缩率:90%

现代图片格式

html 复制代码
<!-- 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 字段过滤
javascript 复制代码
// 后端:只返回前端需要的字段
// 优化前:返回完整用户对象
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 分页加载
javascript 复制代码
// 优化前:一次性返回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)
javascript 复制代码
// 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(树摇)

javascript 复制代码
// 优化前:引入整个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加速

html 复制代码
<!-- 优化前:静态资源从源站加载 -->
<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

nginx 复制代码
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 请求并行化

javascript 复制代码
// 优化前:串行请求
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 复制代码
# 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):

javascript 复制代码
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(内存缓存)
javascript 复制代码
// 简单的内存缓存
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缓存
javascript 复制代码
// 带过期时间的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缓存
javascript 复制代码
// 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 数据预取

javascript 复制代码
// 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 优化清单

markdown 复制代码
☑️ 减少请求数量
   - [ ] 静态资源合并(雪碧图、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
相关推荐
H_ZMY5 小时前
前端实现 HTTPS 强制跳转与移动端域名自动适配
前端·网络协议·https
We་ct5 小时前
LeetCode 42. 接雨水:双指针解法深度剖析与全方法汇总
前端·算法·leetcode·typescript
灰海5 小时前
vue实现即开即用的AI对话打字机效果
前端·javascript·vue.js·打字机
智绘前端5 小时前
React 组件开发速查卡
前端·react.js·前端框架
箫笙默6 小时前
前端相关技术简介
前端
Ulyanov6 小时前
Impress.js深度技术解析:架构基础与结构化设计
开发语言·前端·javascript
小宇的天下6 小时前
Calibre :Standard Verification Rule Format(SVRF) Manual (1-1)
大数据·前端·网络
充气大锤6 小时前
前端实现流式输出配合katex.js
开发语言·前端·javascript·ai·vue
滴水未满6 小时前
uniapp的页面
前端·uni-app