{T}

网络性能优化

网络请求是前端性能优化的关键环节。HTTP 优化有两个大方向:减少请求次数(合并资源、按需加载、雪碧图)和减少单次请求耗时(压缩体积、CDN 加速、HTTP/2 多路复用)。本文按「构建优化 → 传输优化 → 资源优化 → 加速策略」的逻辑,系统梳理网络性能优化的核心知识。


一、构建优化:Webpack 性能调优

构建工具(Webpack / Vite)是日常开发中实现网络优化的主力。本节以 Webpack 为例,讲解构建层面的性能调优策略。

注意:以下内容同时适用于 Vite(基于 Rollup)等现代构建工具,核心思想一致,具体配置方式不同。

1.1 构建过程提速

缩小 Loader 处理范围

通过 include / exclude 避免对不必要的文件进行转译:

javascript
module: {
  rules: [
    {
      test: /\.jsx?$/,
      exclude: /node_modules/,
      use: {
        loader: "babel-loader",
        options: {
          presets: ["@babel/preset-env"],
          cacheDirectory: true
        }
      }
    }
  ]
}

关键点:

  • exclude: /node_modules/ — 跳过庞大的第三方库
  • cacheDirectory: true — 开启文件系统缓存,可将 babel-loader 效率提升 2 倍以上

第三方库处理方案

方案原理适用场景
Externals将依赖排除出打包,通过 CDN 引入小型项目、简单场景
SplitChunks(推荐)自动分析依赖关系,智能拆分代码块Webpack 4+ 标准方案
DllPlugin预编译第三方库为独立文件大型项目、频繁构建

SplitChunks(Webpack 4+ 推荐)

Webpack 5 中 SplitChunksPlugin 已内置,无需额外安装:

javascript
// webpack.config.js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: "all",
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: "vendors",
          chunks: "all",
          priority: -10
        },
        common: {
          name: "common",
          minChunks: 2,
          chunks: "initial",
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  }
}

对比旧版 CommonsChunkPluginCommonsChunkPlugin 在 Webpack 4 中已被废弃,SplitChunks 具有更智能的代码拆分能力。

多进程构建

方案说明状态
thread-loader将 loader 放入 worker 池中运行Webpack 官方推荐,活跃维护
Happypack多进程执行 loader已停止维护(2020 年起)

推荐使用 thread-loader

javascript
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          "thread-loader",  // 放在耗时的 loader 之前
          {
            loader: "babel-loader",
            options: { cacheDirectory: true }
          }
        ]
      }
    ]
  }
}

Happypack 已过时:Happypack 自 2020 年起停止维护。在 Webpack 5 中应使用官方推荐的 thread-loader 替代。

持久化缓存(Webpack 5 新特性)

Webpack 5 引入了更强大的持久化缓存能力:

javascript
module.exports = {
  cache: {
    type: "filesystem",  // 文件系统级缓存
    buildDependencies: {
      config: [__filename]  // 配置变更时使缓存失效
    }
  }
}

相比 Webpack 4 的 cache-loader 和 HardSourceWebpackPlugin,filesystem cache 提供了:

  • 更快的首次和后续构建速度
  • 更少的内存占用
  • 更好的缓存失效管理

1.2 构建结果体积优化

可视化分析工具

使用 webpack-bundle-analyzer 分析包组成:

bash
npm install --save-dev webpack-bundle-analyzer
javascript
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer")

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
}

Tree-Shaking(死代码消除)

Tree-Shaking 基于 ES Module 的静态结构分析,在打包时移除未使用的代码。

启用条件

  • 使用 import / export 语法(ESM)
  • production 模式下自动开启
  • 确保 package.json 中设置 "sideEffects": false

示例

javascript
// pages.js
export const page1 = { title: "页面一" }
export const page2 = { title: "页面二" }

// main.js — 只导入 page1
import { page1 } from "./pages"
console.log(page1)

最终打包结果中,page2 相关代码会被自动消除。

代码压缩

工具说明适用版本
TerserWebpackPluginJavaScript 压缩(支持 ES6+)Webpack 5 默认
CssMinimizerWebpackPluginCSS 压缩推荐使用
UglifyJsPluginJS 压缩(不支持 ES6+)已废弃

Webpack 5 默认配置(已内置 Terser):

javascript
module.exports = {
  mode: "production",  // production 模式下自动压缩
  optimization: {
    minimize: true,
    minimizer: [
      new TerserWebpackPlugin({
        parallel: true,       // 并行压缩
        extractComments: false, // 不生成 .LICENSE 文件
        terserOptions: {
          compress: {
            drop_console: true,   // 移除 console
            drop_debugger: true,  // 移除 debugger
            pure_funcs: ["console.log"] // 移除特定函数调用
          }
        }
      }),
      new CssMinimizerWebpackPlugin()
    ]
  }
}

UglifyJsPlugin 已被弃用:Webpack 5 默认使用 TerserWebpackPlugin 进行 JS 压缩。Terser 支持 ES6+ 语法,压缩效果更好。

按需加载(Code Splitting)

将代码拆分为多个 chunk,按需加载,减少首屏加载体积。

动态 import()(现代标准方式)

javascript
// 路由级别懒加载(以 React Router 为例)
import { lazy, Suspense } from "react"

const BugComponent = lazy(() => import("../pages/BugComponent"))

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Route path="/bug" component={BugComponent} />
    </Suspense>
  )
}

Vue 路由懒加载

javascript
const routes = [
  {
    path: "/bug",
    component: () => import(/* webpackChunkName: "bug" */ "../pages/BugComponent.vue")
  }
]

Webpack 配置配合

javascript
output: {
  path: path.resolve(__dirname, "dist"),
  filename: "[name].[contenthash:8].js",
  chunkFilename: "[name].[contenthash:8].chunk.js",
  clean: true  // Webpack 5: 每次构建前清理输出目录
}

require.ensure 已废弃require.ensure() 是 Webpack 早期的异步加载语法,在现代项目中应统一使用动态 import() 语法。React.lazy + Suspense 是 React 推荐的现代模式。

1.3 完整配置示例

以下是一个整合了上述所有优化策略的 Webpack 5 生产环境配置(来源:examples/webpack/webpack.prod.config.js):

javascript
// Webpack 5 性能优化完整配置示例
// 适用于中大型前端项目

const path = require("path")
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer")
const TerserPlugin = require("terser-webpack-plugin")
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin")
const CompressionPlugin = require("compression-webpack-plugin")

module.exports = {
  mode: "production",

  entry: {
    app: "./src/index.js",
  },

  output: {
    filename: "[name].[contenthash].js",
    chunkFilename: "[name].[contenthash].chunk.js",
    path: path.resolve(__dirname, "dist"),
    clean: true,
  },

  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: "babel-loader",
      },
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
    ],
  },

  optimization: {
    // 代码分割
    splitChunks: {
      chunks: "all",
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: "vendors",
          priority: 10,
          chunks: "all",
        },
      },
    },
    // 压缩 JS
    minimizer: [
      new TerserPlugin({
        parallel: true,
        terserOptions: {
          compress: { drop_console: true },
        },
      }),
      // 压缩 CSS
      new CssMinimizerPlugin(),
    ],
  },

  plugins: [
    // 抽离 CSS
    new MiniCssExtractPlugin({
      filename: "[name].[contenthash].css",
    }),
    // 分析打包体积
    new BundleAnalyzerPlugin(),
    // Gzip 预压缩
    new CompressionPlugin({
      algorithm: "gzip",
      test: /\.(js|css|html|svg)$/,
      threshold: 10240,
      minRatio: 0.8,
    }),
  ],

  devtool: "source-map"
}

二、传输优化:Gzip 压缩原理

2.1 什么是 HTTP 压缩

HTTP 压缩是一种内置到服务器和客户端中的机制,通过重新编码 HTTP 内容来改进传输速度和带宽利用率。

Gzip 是目前最主流的 HTTP 压缩方案,其内核基于 Deflate 算法。

2.2 是否应该使用 Gzip?

结论:对于非极小型文件,强烈建议启用 Gzip。

  • 压缩通常可减小响应体积约 60%-70%
  • 压缩/解压的时间开销远小于传输节省的时间
  • 仅对 < 1KB 的微型文件意义不大

2.3 Gzip 的局限性

Gzip 压缩效果取决于文件的重复率——重复字符串越多,压缩率越高。

文件类型压缩效果说明
HTML/CSS/JS高(70%+)文本重复率高
JSON/XML/SVG同上
图片(JPG/PNG/WebP)极低或无效已经是压缩格式
已压缩文件(zip/gz)无效不会二次压缩

2.4 构建时预压缩 vs 服务端实时压缩

方式优点缺点
服务端 Gzip实现简单,灵活占用服务器 CPU
构建时预压缩分担服务器压力,响应更快增加构建时间

构建时预压缩示例(CompressionWebpackPlugin):

javascript
const CompressionPlugin = require("compression-webpack-plugin")

module.exports = {
  plugins: [
    new CompressionPlugin({
      algorithm: "gzip",
      test: /\.(js|css|html|svg)$/,
      threshold: 10240,     // 只处理大于 10KB 的文件
      minRatio: 0.8,        // 压缩率低于 0.8 才处理
      deleteOriginalAssets: false  // 保留原始文件
    })
  ]
}

Brotli 压缩:比 Gzip 压缩率高 15%-25%,但兼容性稍差。现代浏览器(Chrome/Firefox/Edge)均已支持。可通过类似方式使用 brotli-webpack-plugin 实现。

2.5 服务端压缩配置

nginx
# Nginx 配置
http {
  # Gzip 压缩
  gzip on;
  gzip_vary on;
  gzip_proxied any;
  gzip_comp_level 6;
  gzip_types text/plain text/css text/xml application/json application/javascript application/xml;
  gzip_min_length 1000;

  # Brotli 压缩(更高效)
  brotli on;
  brotli_comp_level 6;
  brotli_types text/plain text/css application/json application/javascript;
}

压缩效果对比

资源类型原始大小GzipBrotli
HTML100KB25KB20KB
CSS100KB18KB15KB
JavaScript100KB30KB25KB

三、传输优化:HTTP 协议与资源压缩

3.1 HTTP/1.1 vs HTTP/2 vs HTTP/3

code
HTTP/1.1:  每个请求需要单独的 TCP 连接
┌───┐     ┌───┐     ┌───┐
│ R │ ──→ │ R │ ──→ │ R │  串行请求
└───┘     └───┘     └───┘

HTTP/2:    多路复用,单个连接并行请求
┌───┐
│ R ├───┐
│ R ├───┼──→ 并行传输
│ R ├───┘
└───┘
单个 TCP 连接

HTTP/3:    基于 QUIC,更快的连接建立
┌───┐
│ Q │ ← 0-RTT 连接恢复
│ U │ ← 内置 TLS 1.3
│ I │ ← 解决队头阻塞
│ C │
└───┘

3.2 HTTP/2 特性

特性说明性能提升
多路复用单连接并行传输消除队头阻塞
头部压缩HPACK 压缩减少传输量
服务器推送主动推送资源减少请求延迟
二进制分帧更高效解析提升处理效率
nginx
# Nginx 开启 HTTP/2
server {
    listen 443 ssl http2;

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;
}

3.3 HTTP/3 与 QUIC

javascript
// 检测 HTTP/3 支持
async function checkHTTP3Support(url) {
  try {
    const response = await fetch(url, {
      headers: { 'Alt-Used': 'h3' }
    });
    return response.headers.get('alt-svc')?.includes('h3');
  } catch {
    return false;
  }
}

3.4 减少请求数量

合并资源

css
/* 雪碧图(CSS Sprite) */
.icon {
  background-image: url('sprite.png');
  background-repeat: no-repeat;
}

.icon-home { background-position: 0 0; width: 20px; height: 20px; }
.icon-user { background-position: -20px 0; width: 20px; height: 20px; }
.icon-cart { background-position: -40px 0; width: 20px; height: 20px; }

内联资源

html
<!-- 内联关键 CSS -->
<head>
  <style>
    /* Critical CSS - 首屏渲染必需的样式 */
    .header { ... }
    .hero { ... }
  </style>
  <!-- 非关键 CSS 异步加载 -->
  <link rel="preload" href="styles.css" as="style" onload="this.rel='stylesheet'">
</head>

<!-- 内联小图片 -->
<img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==" alt="icon">

合并 API 请求

javascript
// ❌ 多次请求
const user = await fetch('/api/user');
const posts = await fetch('/api/posts');
const comments = await fetch('/api/comments');

// ✅ 合并请求(后端支持)
const data = await fetch('/api/user-with-details');

// ✅ 使用 GraphQL
const query = `
  query {
    user { id name }
    posts { id title }
    comments { id text }
  }
`;
const data = await fetch('/graphql', {
  method: 'POST',
  body: JSON.stringify({ query })
});

// ✅ 批量请求
const batchRequest = {
  requests: [
    { method: 'GET', path: '/user' },
    { method: 'GET', path: '/posts' }
  ]
};

3.5 浏览器缓存策略

code
┌─────────────────────────────────────────────────────┐
│                   浏览器缓存                         │
├──────────────────┬──────────────────────────────────┤
│     强缓存        │         协商缓存                  │
│  Cache-Control   │     Last-Modified               │
│     Expires      │          ETag                   │
│                  │                                  │
│  不请求服务器     │    请求服务器验证                 │
│  直接使用缓存     │    返回 304 或新资源             │
└──────────────────┴──────────────────────────────────┘

Cache-Control 详解

nginx
# 静态资源 - 长期缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
  expires 1y;
  add_header Cache-Control "public, immutable";
  # immutable: 文件永不变化,无需验证
}

# HTML 文件 - 协商缓存
location ~* \.html$ {
  add_header Cache-Control "no-cache";
  etag on;
  # 每次请求都验证
}

# API 响应 - 不缓存
location /api/ {
  add_header Cache-Control "no-store, no-cache, must-revalidate";
}

Cache-Control 指令说明

指令说明
public可被任何缓存存储
private只能被浏览器缓存
no-cache使用前需验证
no-store不存储任何缓存
max-age=3600缓存 1 小时
immutable资源永不变化
must-revalidate过期后必须验证

版本化缓存策略

html
<!-- 内容哈希命名 -->
<script src="app.abc123.js"></script>
<link href="styles.def456.css">

<!-- 长期缓存 + 文件名哈希 -->
<!-- 文件内容变化 → 文件名变化 → 强制更新 -->

3.6 预加载与预取

资源预加载

html
<!-- preload: 预加载当前页面必需资源 -->
<link rel="preload" href="critical.js" as="script">
<link rel="preload" href="font.woff2" as="font" crossorigin>
<link rel="preload" href="hero.jpg" as="image">

<!-- prefetch: 预取下一页面资源 -->
<link rel="prefetch" href="next-page.js">
<link rel="prefetch" href="next-page.html">

<!-- preconnect: 预连接域名 -->
<link rel="preconnect" href="https://api.example.com">
<link rel="preconnect" href="https://cdn.example.com" crossorigin>

预加载优先级

code
优先级从高到低:
1. <link rel="preload">  - 当前页面必需
2. <script>              - 阻塞渲染
3. <link rel="prefetch"> - 未来页面需要

JavaScript 预加载

javascript
// 预加载图片
function preloadImages(urls) {
  return Promise.all(
    urls.map(url => new Promise((resolve, reject) => {
      const img = new Image();
      img.onload = resolve;
      img.onerror = reject;
      img.src = url;
    }))
  );
}

// 预加载关键 CSS
function preloadCSS(url) {
  return new Promise((resolve) => {
    const link = document.createElement('link');
    link.rel = 'preload';
    link.as = 'style';
    link.href = url;
    link.onload = () => {
      link.rel = 'stylesheet';
      resolve();
    };
    document.head.appendChild(link);
  });
}

预连接优化

javascript
// 检测用户意图,智能预连接
document.querySelectorAll('a[href]').forEach(link => {
  link.addEventListener('mouseenter', () => {
    const url = new URL(link.href);
    const preconnect = document.createElement('link');
    preconnect.rel = 'preconnect';
    preconnect.href = url.origin;
    document.head.appendChild(preconnect);
  }, { once: true });
});

3.7 请求优化

并发控制

javascript
class RequestQueue {
  constructor(maxConcurrent = 4) {
    this.maxConcurrent = maxConcurrent;
    this.queue = [];
    this.running = 0;
  }

  async add(requestFn) {
    return new Promise((resolve, reject) => {
      this.queue.push({ requestFn, resolve, reject });
      this.process();
    });
  }

  process() {
    while (this.running < this.maxConcurrent && this.queue.length) {
      const { requestFn, resolve, reject } = this.queue.shift();
      this.running++;
      requestFn()
        .then(resolve, reject)
        .finally(() => {
          this.running--;
          this.process();
        });
    }
  }
}

// 使用示例
const queue = new RequestQueue(4);
const results = await Promise.all(
  urls.map(url => queue.add(() => fetch(url).then(r => r.json())))
);

请求缓存

javascript
class RequestCache {
  constructor(options = {}) {
    this.cache = new Map();
    this.maxAge = options.maxAge || 60000; // 默认 1 分钟
    this.maxSize = options.maxSize || 100;
  }

  get(key) {
    const item = this.cache.get(key);
    if (!item) return null;

    // 过期则删除
    if (Date.now() - item.timestamp > this.maxAge) {
      this.cache.delete(key);
      return null;
    }
    return item.value;
  }

  set(key, value) {
    // 超过最大容量时清理最旧数据
    if (this.cache.size >= this.maxSize) {
      const oldestKey = this.cache.keys().next().value;
      this.cache.delete(oldestKey);
    }
    this.cache.set(key, { value, timestamp: Date.now() });
  }
}

// 使用示例:带缓存的请求
async function requestWithCache(url, cache) {
  const cached = cache.get(url);
  if (cached) return cached;

  const data = await fetch(url).then(r => r.json());
  cache.set(url, data);
  return data;
}

请求取消

javascript
// 使用 AbortController 取消请求
const controller = new AbortController();

async function fetchWithAbort(url) {
  try {
    const response = await fetch(url, {
      signal: controller.signal
    });
    return await response.json();
  } catch (error) {
    if (error.name === 'AbortError') {
      console.log('请求被取消');
    }
    throw error;
  }
}

// 在 React 组件中结合 useEffect 实现请求取消
function useFetch(url) {
  const [data, setData] = useState(null);

  useEffect(() => {
    const controller = new AbortController();
    fetch(url, { signal: controller.signal })
      .then(res => res.json())
      .then(setData)
      .catch(err => {
        if (err.name !== 'AbortError') console.error(err);
      });

    // 组件卸载时取消请求
    return () => controller.abort();
  }, [url]);

  return data;
}

请求重试

javascript
async function fetchWithRetry(url, options = {}) {
  const { retries = 3, delay = 1000, backoff = 2 } = options;

  for (let i = 0; i < retries; i++) {
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return await response.json();
    } catch (error) {
      if (i === retries - 1) throw error;

      const waitTime = delay * Math.pow(backoff, i);
      console.log(`重试 ${i + 1}/${retries},等待 ${waitTime}ms`);
      await new Promise(resolve => setTimeout(resolve, waitTime));
    }
  }
}

3.8 Service Worker 缓存

注册 Service Worker

javascript
// main.js
if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () => {
    try {
      const registration = await navigator.serviceWorker.register('/sw.js');
      console.log('SW registered:', registration);
    } catch (error) {
      console.log('SW registration failed:', error);
    }
  });
}

缓存策略实现

javascript
// sw.js
const CACHE_NAME = 'v1';
const STATIC_ASSETS = [
  '/',
  '/styles.css',
  '/app.js'
];

// 安装:预缓存静态资源
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then((cache) => cache.addAll(STATIC_ASSETS))
      .then(() => self.skipWaiting())
  );
});

// 激活:清理旧缓存
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys()
      .then((keys) => Promise.all(
        keys.filter((key) => key !== CACHE_NAME)
            .map((key) => caches.delete(key))
      ))
      .then(() => self.clients.claim())
  );
});

// 请求:缓存优先,回退网络
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then((cached) => {
        if (cached) return cached;
        return fetch(event.request).then((response) => {
          // 只缓存同源 GET 请求
          if (response.ok && event.request.method === 'GET') {
            const clone = response.clone();
            caches.open(CACHE_NAME)
              .then((cache) => cache.put(event.request, clone));
          }
          return response;
        });
      })
  );
});

缓存策略模式

javascript
// Cache First - 缓存优先
async function cacheFirst(request) {
  const cached = await caches.match(request);
  if (cached) return cached;
  return fetch(request);
}

// Network First - 网络优先
async function networkFirst(request) {
  try {
    const response = await fetch(request);
    const cache = await caches.open(CACHE_NAME);
    cache.put(request, response.clone());
    return response;
  } catch {
    return caches.match(request);
  }
}

// Stale While Revalidate - 后台更新
async function staleWhileRevalidate(request) {
  const cached = await caches.match(request);

  const fetchPromise = fetch(request).then(response => {
    caches.open(CACHE_NAME).then(cache => cache.put(request, response));
    return response;
  });

  return cached || fetchPromise;
}

四、资源优化:图片优化策略

图片资源通常占页面总资源体积的 50% 以上(HTTP Archive 统计数据)。图片优化是前端性能优化中投入产出比最高的环节之一,核心是在质量与性能之间寻找最佳平衡点。

4.1 各格式对比与选型指南

JPEG / JPG

特性说明
开发者国际标准化组织(1992)
压缩方式有损压缩
核心优势兼容性最好、压缩率较高
劣势不支持透明、有损压缩会损失细节
适用场景照片、渐变色丰富的复杂图像

PNG

特性说明
压缩方式无损压缩
核心优势支持透明、无损、细节清晰
劣势文件体积大,不适合照片
适用场景Logo、图标、透明背景图片、需要清晰边缘的图形

WebP(推荐)

特性说明
压缩方式支持有损 + 无损
核心优势比 JPEG 小 25-34%,比 PNG 小 26%,支持透明 + 动图
兼容性现代浏览器全面支持(Chrome/Firefox/Edge/Safari 16+)

WebP 兼容性

code
Chrome 23+
Firefox 65+
Edge 14+
Safari 16+ (macOS 13+)
Opera 12.1+
IE(不支持)

WebP 使用方案:渐进增强

html
<picture>
  <source srcset="image.webp" type="image/webp" />
  <img src="image.jpg" alt="描述文字" loading="lazy" />
</picture>

AVIF(新一代图片格式)

特性说明
开发者Netflix/Google/Apple 等联合开发(2020)
核心优势比 WebP 再小 20%-50%,支持 HDR、高色深
兼容性Chrome 85+、Firefox 93+、Safari 16.1+
html
<picture>
  <source srcset="image.avif" type="image/avif" />
  <source srcset="image.webp" type="image/webp" />
  <img src="image.jpg" alt="描述文字" loading="lazy" />
</picture>

SVG

特性说明
本质基于 XML 的文本文件(矢量图形)
优势无限放大不失真、体积小、可编程、可用 CSS 控制
劣势复杂图形渲染成本较高
适用场景图标、Logo、简单插画、动画图形

使用方式

html
<!-- 方式一:内联 SVG -->
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200">
  <circle cx="100" cy="100" r="80" fill="#3498db" />
</svg>

<!-- 方式二:引用外部 SVG 文件 -->
<img src="icon.svg" alt="图标" width="48" height="48" />

<!-- 方式三:CSS 背景 -->
css
.bg-icon {
  background-image: url("icon.svg");
}

Base64 编码

Base64 是一种编码方式(非图片格式),用于将小图片直接嵌入 HTML/CSS 中以减少 HTTP 请求。

使用条件

  • 文件尺寸极小(通常 < 2KB)
  • 无法与其它图片合并为雪碧图
  • 更新频率极低

注意:Base64 编码后体积膨胀约 33%(4/3 原始大小)。大图使用 Base64 会得不偿失。

css
/* 通过 url-loader 自动处理 */
.icon-search {
  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...");
}
javascript
// Webpack 配置
{
  test: /\.(png|jpe?g|gif)$/i,
  type: "asset",
  parser: {
    dataUrlCondition: {
      maxSize: 4 * 1024 // 小于 4KB 转 base64
    }
  }
}

雪碧图(CSS Sprites)

将多个小图标合并到一张大图中,通过 background-position 定位显示各个图标。

适用场景:大量固定尺寸的小图标(如导航栏图标、工具栏图标)

现状:随着 HTTP/2 多路复用的普及和 icon font / SVG 的流行,雪碧图的使用已大幅减少。但在某些需要极致性能的场景下仍有价值。

4.2 响应式图片

根据设备屏幕尺寸和分辨率提供不同规格的图片:

html
<!-- srcset + sizes -->
<img
  src="image-400w.jpg"
  srcset="image-400w.jpg 400w,
          image-800w.jpg 800w,
          image-1200w.jpg 1200w"
  sizes="(max-width: 600px) 100vw, 50vw"
  alt="响应式图片"
  loading="lazy"
/>

4.3 图片优化决策树

code
是否需要透明?
├─ 是 → 是否是照片类图像?
│       ├─ 是 → WebP(或 AVIF)+ PNG 降级
│       └─ 否 → PNG-8(优先)/ PNG-24
└─ 否 → 是否是照片/色彩丰富?
        ├─ 是 → JPEG / WebP / AVIF
        └─ 否 → 是否是图标/Logo?
                ├─ 是且简单 → SVG(首选)/ PNG
                ├─ 是且复杂 → PNG / WebP
                └─ 极小图标 → Base64 / 雪碧图

4.4 其他优化手段

手段说明
loading="lazy"原生懒加载属性(2020+ 浏览器支持)
指定宽高避免 CLS(布局偏移)
压缩工具imagemin、sharp、squoosh
CDN 自适应根据客户端能力自动返回最佳格式
占位图低质量图占位(LQIP)、模糊占位

五、加速策略:CDN 缓存与回源

缓存和本地存储的优化,都发生在"获取到资源并存起来"之后。首次请求时这些优化手段无法生效。CDN 通过将静态资源部署到全球各地的边缘节点,从物理距离上缩短了用户与服务器之间的传输路径。

5.1 CDN 工作原理

code
用户(北京)→ CDN 边缘节点(北京)→ 源站(杭州)
                ↑                    ↑
            命中缓存:直接返回      未命中:回源拉取

5.2 核心概念

概念说明
缓存(Cache)将源站的资源副本存储在 CDN 节点上
回源(Origin Fetch)当 CDN 节点未命中缓存或缓存过期时,向源站请求资源
命中(Hit)用户请求的资源在 CDN 节点有缓存
未命中(Miss)CDN 节点无该资源缓存,需要回源

5.3 CDN 与前端的关系

静态资源 vs 动态资源

类型说明是否适合 CDN
静态资源JS/CSS/图片/字体等不需要服务端计算即可返回的资源适合
动态资源需要后端实时生成的数据/API 响应不适合
非纯静态 HTML需要权限认证等额外计算的 HTML 页面视情况

核心原则:CDN 用来存放静态资源,业务服务器负责处理动态内容。

CDN 域名分离

将静态资源和主页面放在不同域名下:

code
主域名:    www.example.com        → 业务服务器(HTML)
CDN 域名:  cdn.example.com / static.example.com → CDN 节点(JS/CSS/图片)

为什么要域名分离?

Cookie 是跟随域名的。同一域名下所有请求都会携带 Cookie,而静态资源通常不需要 Cookie 信息。域名分离可以:

  • 避免 Cookie 带来的不必要带宽消耗
  • 减少请求头体积
  • 允许浏览器对 CDN 域名建立更多并行连接
  • 利用 HTTP/2 的域名聚合能力

实际案例:淘宝首页

  • HTML 页面 → www.taobao.com(业务服务器)
  • 图片资源 → *.alicdn.com(CDN 节点)
  • JS/CSS 资源 → g.alicdn.com(CDN 节点)

HTML 返回时图片位置用空 div 占位,JS/CSS 从 CDN 加载后填充内容。

5.4 CDN 缓存策略配置

缓存时间设置

资源类型推荐缓存策略Cache-Control 示例
HTML短缓存或不缓存no-cachemax-age=0
带 hash 的 JS/CSS长期缓存max-age=31536000, immutable
图片/字体中长期缓存max-age=2592000
API 响应不缓存no-store

CDN 回源配置

nginx
# Nginx 作为源站时的示例配置
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
    # 设置浏览器缓存
    add_header Cache-Control "public, max-age=31536000, immutable";

    # 设置 CDN 节点缓存
    add_header CDN-Cache-Control "max-age=604800";

    # ETag 用于协商缓存
    etag on;
}

5.5 CDN 使用方式

html
<!-- 使用公共 CDN -->
<script src="https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.prod.js"></script>
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>

<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="//cdn.example.com">

<!-- 预连接(DNS + TCP + TLS) -->
<link rel="preconnect" href="https://cdn.example.com" crossorigin>

5.6 CDN 优化最佳实践

  1. 版本化文件名 — 使用 contenthash 确保文件更新后 URL 变化
  2. 合理设置缓存时间 — 静态资源长缓存 + 版本号更新
  3. 预热关键资源 — 大促前提前将热点资源推送到所有节点
  4. 监控命中率 — 保持高缓存命中率(通常 > 95% 为健康状态)
  5. HTTPS 全覆盖 — CDN 同样支持 HTTPS,且可提供免费证书
  6. 开启 Brotli/Gzip — CDN 节点支持自动压缩
策略说明
静态资源 CDN图片、CSS、JS 使用 CDN
API 加速使用 CDN 加速 API 请求
边缘计算在 CDN 边缘节点执行逻辑
多 CDN 冗余避免单点故障

六、性能监控

6.1 Performance API

javascript
// 页面加载性能
function getPageTiming() {
  const timing = performance.timing;

  return {
    // DNS 解析
    dns: timing.domainLookupEnd - timing.domainLookupStart,
    // TCP 连接
    tcp: timing.connectEnd - timing.connectStart,
    // SSL 握手
    ssl: timing.connectEnd - timing.secureConnectionStart,
    // TTFB(首字节时间)
    ttfb: timing.responseStart - timing.requestStart,
    // 白屏时间(首次渲染)
    fpt: timing.responseEnd - timing.fetchStart,
    // DOM 解析
    domParse: timing.domComplete - timing.domLoading,
    // 页面完全加载
    loadTime: timing.loadEventEnd - timing.navigationStart,
  };
}

// 获取关键资源加载耗时
function getResourceTiming() {
  return performance.getEntriesByType("resource")
    .filter((resource) => ["img", "script", "css"].includes(resource.initiatorType))
    .map((resource) => ({
      name: resource.name,
      type: resource.initiatorType,
      duration: resource.duration,
      size: resource.transferSize,
      ttfb: resource.responseStart - resource.requestStart
    }));
}

6.2 Navigator Connection API

javascript
function getNetworkInfo() {
  const connection = navigator.connection;

  if (!connection) return null;

  return {
    effectiveType: connection.effectiveType,  // 4g, 3g, 2g
    downlink: connection.downlink,            // Mbps
    rtt: connection.rtt,                      // ms
    saveData: connection.saveData             // 省流模式
  };
}

// 根据网络状况调整策略
function adaptToNetwork() {
  const info = getNetworkInfo();

  if (info?.saveData || info?.effectiveType === '2g') {
    // 低质量图片、减少请求
    return { quality: 'low', preload: false };
  }

  return { quality: 'high', preload: true };
}

6.3 性能数据上报

javascript
function reportPerformance() {
  const data = {
    timing: getPageTiming(),
    resources: getResourceTiming(),
    network: getNetworkInfo(),
    url: location.href,
    userAgent: navigator.userAgent
  };

  // 使用 sendBeacon 确保数据发送
  navigator.sendBeacon('/api/performance', JSON.stringify(data));
}

// 页面加载完成后上报
window.addEventListener('load', () => {
  setTimeout(reportPerformance, 0);
});

七、常见问题解答

Q1: HTTP/2 是否还需要合并资源?

A: HTTP/2 下合并策略变化:

  • JS/CSS:不需要合并,代码分割更灵活
  • 图片:雪碧图效果有限,考虑使用图片 CDN
  • API:批量请求仍有价值,减少 RTT

Q2: 强缓存和协商缓存如何选择?

A: 选择策略:

javascript
// 静态资源(带哈希):强缓存
// 内容变化 → 文件名变化 → 强制更新
app.abc123.js  → Cache-Control: max-age=31536000, immutable

// HTML/动态内容:协商缓存
index.html     → Cache-Control: no-cache + ETag

// 用户数据:不缓存
/api/user      → Cache-Control: no-store

Q3: 如何处理缓存失效问题?

A: 缓存更新策略:

javascript
// 1. 版本号/哈希命名
<script src="app.v2.abc123.js"></script>

// 2. Service Worker 更新
self.addEventListener('activate', (event) => {
  // 清理旧缓存
});

// 3. 主动刷新
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.addEventListener('controllerchange', () => {
    location.reload();
  });
}

Q4: 如何优化第三方脚本?

A: 第三方脚本优化:

html
<!-- 1. 异步加载 -->
<script async src="analytics.js"></script>

<!-- 2. 延迟加载 -->
<script defer src="widget.js"></script>

<!-- 3. facades(外观模式) -->
<div class="youtube-facade" data-video-id="xxx">
  <img src="thumbnail.jpg">
  <button>播放</button>
</div>
<script>
  // 点击后加载真实播放器
</script>

<!-- 4. 使用 Intersection Observer 延迟加载 -->

八、优化总结

优化方式说明效果
Webpack 构建优化Tree-Shaking、代码分割、持久化缓存减少包体积,加速构建
Gzip/Brotli压缩传输减小 60%-70% 传输体积
HTTP/2多路复用、头部压缩消除队头阻塞
图片优化格式选型、响应式图片、懒加载减小 50%+ 资源体积
CDN就近访问、域名分离大幅降低网络延迟
缓存策略强缓存 + 协商缓存减少重复请求
Service Worker离线缓存支持离线访问
预加载提前加载资源减少请求延迟

提示:网络性能优化需要前后端配合,重点关注首屏加载时间和缓存策略。构建优化是起点,传输优化是核心,资源优化是杠杆,CDN 加速是保障。