网络性能优化
网络请求是前端性能优化的关键环节。HTTP 优化有两个大方向:减少请求次数(合并资源、按需加载、雪碧图)和减少单次请求耗时(压缩体积、CDN 加速、HTTP/2 多路复用)。本文按「构建优化 → 传输优化 → 资源优化 → 加速策略」的逻辑,系统梳理网络性能优化的核心知识。
一、构建优化:Webpack 性能调优
构建工具(Webpack / Vite)是日常开发中实现网络优化的主力。本节以 Webpack 为例,讲解构建层面的性能调优策略。
注意:以下内容同时适用于 Vite(基于 Rollup)等现代构建工具,核心思想一致,具体配置方式不同。
1.1 构建过程提速
缩小 Loader 处理范围
通过 include / exclude 避免对不必要的文件进行转译:
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 已内置,无需额外安装:
// 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
}
}
}
}
}对比旧版 CommonsChunkPlugin:
CommonsChunkPlugin在 Webpack 4 中已被废弃,SplitChunks具有更智能的代码拆分能力。
多进程构建
| 方案 | 说明 | 状态 |
|---|---|---|
| thread-loader | 将 loader 放入 worker 池中运行 | Webpack 官方推荐,活跃维护 |
| Happypack | 多进程执行 loader | 已停止维护(2020 年起) |
推荐使用 thread-loader:
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 引入了更强大的持久化缓存能力:
module.exports = {
cache: {
type: "filesystem", // 文件系统级缓存
buildDependencies: {
config: [__filename] // 配置变更时使缓存失效
}
}
}相比 Webpack 4 的 cache-loader 和 HardSourceWebpackPlugin,filesystem cache 提供了:
- 更快的首次和后续构建速度
- 更少的内存占用
- 更好的缓存失效管理
1.2 构建结果体积优化
可视化分析工具
使用 webpack-bundle-analyzer 分析包组成:
npm install --save-dev webpack-bundle-analyzerconst { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer")
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}Tree-Shaking(死代码消除)
Tree-Shaking 基于 ES Module 的静态结构分析,在打包时移除未使用的代码。
启用条件:
- 使用
import/export语法(ESM) - 在
production模式下自动开启 - 确保
package.json中设置"sideEffects": false
示例:
// pages.js
export const page1 = { title: "页面一" }
export const page2 = { title: "页面二" }
// main.js — 只导入 page1
import { page1 } from "./pages"
console.log(page1)最终打包结果中,page2 相关代码会被自动消除。
代码压缩
| 工具 | 说明 | 适用版本 |
|---|---|---|
| TerserWebpackPlugin | JavaScript 压缩(支持 ES6+) | Webpack 5 默认 |
| CssMinimizerWebpackPlugin | CSS 压缩 | 推荐使用 |
| UglifyJsPlugin | JS 压缩(不支持 ES6+) | 已废弃 |
Webpack 5 默认配置(已内置 Terser):
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()(现代标准方式):
// 路由级别懒加载(以 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 路由懒加载:
const routes = [
{
path: "/bug",
component: () => import(/* webpackChunkName: "bug" */ "../pages/BugComponent.vue")
}
]Webpack 配置配合:
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):
// 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):
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 配置
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;
}压缩效果对比:
| 资源类型 | 原始大小 | Gzip | Brotli |
|---|---|---|---|
| HTML | 100KB | 25KB | 20KB |
| CSS | 100KB | 18KB | 15KB |
| JavaScript | 100KB | 30KB | 25KB |
三、传输优化:HTTP 协议与资源压缩
3.1 HTTP/1.1 vs HTTP/2 vs HTTP/3
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 开启 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
// 检测 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 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; }内联资源
<!-- 内联关键 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 请求
// ❌ 多次请求
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 浏览器缓存策略
┌─────────────────────────────────────────────────────┐
│ 浏览器缓存 │
├──────────────────┬──────────────────────────────────┤
│ 强缓存 │ 协商缓存 │
│ Cache-Control │ Last-Modified │
│ Expires │ ETag │
│ │ │
│ 不请求服务器 │ 请求服务器验证 │
│ 直接使用缓存 │ 返回 304 或新资源 │
└──────────────────┴──────────────────────────────────┘Cache-Control 详解
# 静态资源 - 长期缓存
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 | 过期后必须验证 |
版本化缓存策略
<!-- 内容哈希命名 -->
<script src="app.abc123.js"></script>
<link href="styles.def456.css">
<!-- 长期缓存 + 文件名哈希 -->
<!-- 文件内容变化 → 文件名变化 → 强制更新 -->3.6 预加载与预取
资源预加载
<!-- 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>预加载优先级
优先级从高到低:
1. <link rel="preload"> - 当前页面必需
2. <script> - 阻塞渲染
3. <link rel="prefetch"> - 未来页面需要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);
});
}预连接优化
// 检测用户意图,智能预连接
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 请求优化
并发控制
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())))
);请求缓存
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;
}请求取消
// 使用 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;
}请求重试
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
// 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);
}
});
}缓存策略实现
// 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;
});
})
);
});缓存策略模式
// 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 兼容性:
Chrome 23+
Firefox 65+
Edge 14+
Safari 16+ (macOS 13+)
Opera 12.1+
IE(不支持)WebP 使用方案:渐进增强:
<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+ |
<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、简单插画、动画图形 |
使用方式:
<!-- 方式一:内联 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 背景 -->.bg-icon {
background-image: url("icon.svg");
}Base64 编码
Base64 是一种编码方式(非图片格式),用于将小图片直接嵌入 HTML/CSS 中以减少 HTTP 请求。
使用条件:
- 文件尺寸极小(通常 < 2KB)
- 无法与其它图片合并为雪碧图
- 更新频率极低
注意:Base64 编码后体积膨胀约 33%(4/3 原始大小)。大图使用 Base64 会得不偿失。
/* 通过 url-loader 自动处理 */
.icon-search {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...");
}// 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 响应式图片
根据设备屏幕尺寸和分辨率提供不同规格的图片:
<!-- 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 图片优化决策树
是否需要透明?
├─ 是 → 是否是照片类图像?
│ ├─ 是 → 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 工作原理
用户(北京)→ CDN 边缘节点(北京)→ 源站(杭州)
↑ ↑
命中缓存:直接返回 未命中:回源拉取5.2 核心概念
| 概念 | 说明 |
|---|---|
| 缓存(Cache) | 将源站的资源副本存储在 CDN 节点上 |
| 回源(Origin Fetch) | 当 CDN 节点未命中缓存或缓存过期时,向源站请求资源 |
| 命中(Hit) | 用户请求的资源在 CDN 节点有缓存 |
| 未命中(Miss) | CDN 节点无该资源缓存,需要回源 |
5.3 CDN 与前端的关系
静态资源 vs 动态资源
| 类型 | 说明 | 是否适合 CDN |
|---|---|---|
| 静态资源 | JS/CSS/图片/字体等不需要服务端计算即可返回的资源 | 适合 |
| 动态资源 | 需要后端实时生成的数据/API 响应 | 不适合 |
| 非纯静态 HTML | 需要权限认证等额外计算的 HTML 页面 | 视情况 |
核心原则:CDN 用来存放静态资源,业务服务器负责处理动态内容。
CDN 域名分离
将静态资源和主页面放在不同域名下:
主域名: 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-cache 或 max-age=0 |
| 带 hash 的 JS/CSS | 长期缓存 | max-age=31536000, immutable |
| 图片/字体 | 中长期缓存 | max-age=2592000 |
| API 响应 | 不缓存 | no-store |
CDN 回源配置
# 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 使用方式
<!-- 使用公共 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 优化最佳实践
- 版本化文件名 — 使用 contenthash 确保文件更新后 URL 变化
- 合理设置缓存时间 — 静态资源长缓存 + 版本号更新
- 预热关键资源 — 大促前提前将热点资源推送到所有节点
- 监控命中率 — 保持高缓存命中率(通常 > 95% 为健康状态)
- HTTPS 全覆盖 — CDN 同样支持 HTTPS,且可提供免费证书
- 开启 Brotli/Gzip — CDN 节点支持自动压缩
| 策略 | 说明 |
|---|---|
| 静态资源 CDN | 图片、CSS、JS 使用 CDN |
| API 加速 | 使用 CDN 加速 API 请求 |
| 边缘计算 | 在 CDN 边缘节点执行逻辑 |
| 多 CDN 冗余 | 避免单点故障 |
六、性能监控
6.1 Performance API
// 页面加载性能
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
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 性能数据上报
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: 选择策略:
// 静态资源(带哈希):强缓存
// 内容变化 → 文件名变化 → 强制更新
app.abc123.js → Cache-Control: max-age=31536000, immutable
// HTML/动态内容:协商缓存
index.html → Cache-Control: no-cache + ETag
// 用户数据:不缓存
/api/user → Cache-Control: no-storeQ3: 如何处理缓存失效问题?
A: 缓存更新策略:
// 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: 第三方脚本优化:
<!-- 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 加速是保障。