Appearance
压缩优化
压缩优化是提高 Web 应用性能的重要手段,可以显著减少传输数据量,加快页面加载速度。本指南将详细介绍各种压缩技术和优化策略。
HTTP 压缩
Gzip 压缩
javascript
const express = require('express');
const compression = require('compression');
const app = express();
// 启用 Gzip 压缩
app.use(compression({
// 只压缩文本类型的响应
filter: (req, res) => {
// 检查客户端是否支持压缩
if (req.headers['x-no-compression']) {
return false;
}
// 使用默认过滤器
return compression.filter(req, res);
},
// 压缩级别 (0-9, 9 是最高压缩比)
level: 6,
// 最小响应体大小 (字节),小于这个值不压缩
threshold: 1024, // 1KB
// 内存级别 (1-9, 影响内存使用和压缩速度)
memLevel: 8
}));
// 自定义压缩中间件
const customCompression = () => {
return (req, res, next) => {
const acceptEncoding = req.headers['accept-encoding'];
if (!acceptEncoding) {
return next();
}
// 检查是否支持 gzip
if (acceptEncoding.match(/\bgzip\b/)) {
res.setHeader('Content-Encoding', 'gzip');
res.setHeader('Vary', 'Accept-Encoding');
// 创建 gzip 流
const zlib = require('zlib');
const gzip = zlib.createGzip({
level: 6, // 压缩级别
memLevel: 8 // 内存级别
});
// 管道连接
res._originalWrite = res.write;
res._originalEnd = res.end;
gzip.pipe(res);
res.write = function(chunk, encoding) {
gzip.write(chunk, encoding);
};
res.end = function(chunk, encoding) {
if (chunk) {
gzip.write(chunk, encoding);
}
gzip.end();
};
}
next();
};
};
// 使用自定义压缩
app.use(customCompression());
Brotli 压缩
javascript
const express = require('express');
const app = express();
// Brotli 压缩中间件
const brotliCompression = () => {
return (req, res, next) => {
const acceptEncoding = req.headers['accept-encoding'];
if (!acceptEncoding) {
return next();
}
// 优先使用 Brotli,如果支持的话
if (acceptEncoding.match(/\bbr\b/)) {
res.setHeader('Content-Encoding', 'br');
res.setHeader('Vary', 'Accept-Encoding');
const zlib = require('zlib');
const brotli = zlib.createBrotliCompress({
params: {
[zlib.constants.BROTLI_PARAM_QUALITY]: 6, // 压缩质量 (0-11)
[zlib.constants.BROTLI_PARAM_SIZE_HINT]: 1024 // 预期数据大小
}
});
res._originalWrite = res.write;
res._originalEnd = res.end;
brotli.pipe(res);
res.write = function(chunk, encoding) {
brotli.write(chunk, encoding);
};
res.end = function(chunk, encoding) {
if (chunk) {
brotli.write(chunk, encoding);
}
brotli.end();
};
}
// 降级到 Gzip
else if (acceptEncoding.match(/\bgzip\b/)) {
res.setHeader('Content-Encoding', 'gzip');
res.setHeader('Vary', 'Accept-Encoding');
const zlib = require('zlib');
const gzip = zlib.createGzip({ level: 6 });
res._originalWrite = res.write;
res._originalEnd = res.end;
gzip.pipe(res);
res.write = function(chunk, encoding) {
gzip.write(chunk, encoding);
};
res.end = function(chunk, encoding) {
if (chunk) {
gzip.write(chunk, encoding);
}
gzip.end();
};
}
next();
};
};
app.use(brotliCompression());
静态资源压缩
Webpack 压缩配置
javascript
// webpack.config.js
const CompressionPlugin = require('compression-webpack-plugin');
const BrotliPlugin = require('brotli-webpack-plugin');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].chunk.js'
},
plugins: [
// Gzip 压缩插件
new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 8192, // 只压缩大于 8KB 的文件
minRatio: 0.8, // 最小压缩比率,小于这个值不压缩
deleteOriginalAssets: false // 是否删除原始文件
}),
// Brotli 压缩插件
new BrotliPlugin({
asset: '[path].br[query]',
test: /\.(js|css|html|svg)$/,
threshold: 8192,
minRatio: 0.8
})
]
};
资源预压缩
javascript
// 预压缩脚本
const fs = require('fs');
const path = require('path');
const zlib = require('zlib');
const glob = require('glob');
class AssetPrecompressor {
constructor(options = {}) {
this.srcDir = options.srcDir || './dist';
this.destDir = options.destDir || './dist';
this.extensions = options.extensions || ['.js', '.css', '.html', '.svg'];
this.compressionLevel = options.compressionLevel || 6;
}
compressFile(filePath, algorithm = 'gzip') {
return new Promise((resolve, reject) => {
const readStream = fs.createReadStream(filePath);
let writeStream;
let compressedFilePath;
if (algorithm === 'gzip') {
writeStream = fs.createWriteStream(filePath + '.gz');
compressedFilePath = filePath + '.gz';
const gzip = zlib.createGzip({ level: this.compressionLevel });
readStream.pipe(gzip).pipe(writeStream);
} else if (algorithm === 'brotli') {
writeStream = fs.createWriteStream(filePath + '.br');
compressedFilePath = filePath + '.br';
const brotli = zlib.createBrotliCompress({
params: {
[zlib.constants.BROTLI_PARAM_QUALITY]: this.compressionLevel
}
});
readStream.pipe(brotli).pipe(writeStream);
}
writeStream.on('finish', () => {
console.log(`Compressed: ${filePath} -> ${compressedFilePath}`);
resolve(compressedFilePath);
});
writeStream.on('error', reject);
});
}
async compressDirectory(dirPath, algorithms = ['gzip', 'brotli']) {
const files = glob.sync(path.join(dirPath, '**/*.{js,css,html,svg,json,xml}'));
for (const file of files) {
const stat = fs.statSync(file);
if (stat.isFile()) {
for (const algorithm of algorithms) {
try {
await this.compressFile(file, algorithm);
} catch (error) {
console.error(`Failed to compress ${file}:`, error);
}
}
}
}
}
async run() {
console.log('Starting asset pre-compression...');
await this.compressDirectory(this.srcDir);
console.log('Asset pre-compression completed!');
}
}
// 使用预压缩工具
const compressor = new AssetPrecompressor({
srcDir: './build',
compressionLevel: 9
});
compressor.run().catch(console.error);
图片优化压缩
图片压缩中间件
javascript
const sharp = require('sharp');
const path = require('path');
// 图片动态压缩中间件
const imageCompression = () => {
return async (req, res, next) => {
const url = req.url;
// 检查是否为图片请求
if (/\.(jpe?g|png|webp)$/i.test(url)) {
// 检查是否支持 WebP
const accept = req.headers.accept || '';
const supportsWebP = accept.includes('image/webp');
// 获取压缩参数
const width = parseInt(req.query.w) || null;
const quality = parseInt(req.query.q) || 80;
const format = req.query.f || (supportsWebP ? 'webp' : 'auto');
const imagePath = path.join(__dirname, 'public', url);
try {
let pipeline = sharp(imagePath);
// 调整尺寸
if (width) {
pipeline = pipeline.resize(width);
}
// 应用压缩和格式转换
if (format === 'webp') {
pipeline = pipeline.webp({ quality });
res.setHeader('Content-Type', 'image/webp');
} else if (format === 'jpeg') {
pipeline = pipeline.jpeg({ quality });
res.setHeader('Content-Type', 'image/jpeg');
} else if (format === 'png') {
pipeline = pipeline.png({ quality });
res.setHeader('Content-Type', 'image/png');
} else {
// 根据原始格式决定
const ext = path.extname(imagePath).toLowerCase();
if (ext === '.png') {
pipeline = pipeline.png({ quality });
res.setHeader('Content-Type', 'image/png');
} else {
pipeline = pipeline.jpeg({ quality });
res.setHeader('Content-Type', 'image/jpeg');
}
}
// 应用压缩
const buffer = await pipeline.toBuffer();
// 设置缓存头
res.set({
'Cache-Control': 'public, max-age=31536000', // 1年
'Content-Length': buffer.length,
'Vary': 'Accept'
});
res.send(buffer);
return;
} catch (error) {
console.error('Image compression error:', error);
}
}
next();
};
};
app.use(imageCompression());
SVG 压缩
javascript
const SVGO = require('svgo');
// SVG 优化压缩
class SVGCompressor {
constructor(options = {}) {
this.svgo = new SVGO({
plugins: [
{ removeDoctype: true },
{ removeXMLProcInst: true },
{ removeComments: true },
{ removeMetadata: true },
{ removeEditorsNSData: true },
{ cleanupAttrs: true },
{ inlineStyles: true },
{ minifyStyles: true },
{ convertStyleToAttrs: true },
{ cleanupIDs: true },
{ prefixIds: true },
{ removeRasterImages: false },
{ removeUselessDefs: true },
{ cleanupNumericValues: true },
{ cleanupViewBox: true },
{ moveElemsAttrsToGroup: true },
{ moveGroupAttrsToElems: true },
{ collapseGroups: true },
{ removeEmptyAttrs: true },
{ removeEmptyText: true },
{ removeEmptyContainers: true },
{ mergePaths: true },
{ convertShapeToPath: true },
{ sortAttrs: true },
{ removeDimensions: true },
{ removeAttrs: { attrs: '(stroke|fill)' } }
]
});
}
async compress(svgContent) {
try {
const result = await this.svgo.optimize(svgContent);
return result.data;
} catch (error) {
console.error('SVG compression error:', error);
return svgContent; // 返回原始内容
}
}
async compressFile(filePath) {
const fs = require('fs');
const content = fs.readFileSync(filePath, 'utf8');
return await this.compress(content);
}
}
// SVG 压缩中间件
const svgCompression = (options = {}) => {
const compressor = new SVGCompressor(options);
return async (req, res, next) => {
if (req.url.endsWith('.svg')) {
// 检查是否需要压缩
const shouldCompress = req.query.compress !== 'false';
if (shouldCompress) {
// 读取 SVG 文件
const filePath = path.join(__dirname, 'public', req.url);
try {
const compressedSVG = await compressor.compressFile(filePath);
res.set({
'Content-Type': 'image/svg+xml',
'Content-Length': Buffer.byteLength(compressedSVG),
'Cache-Control': 'public, max-age=31536000'
});
res.send(compressedSVG);
return;
} catch (error) {
console.error('SVG compression failed:', error);
}
}
}
next();
};
};
app.use(svgCompression());
字体压缩优化
字体子集化
javascript
const fontverter = require('fontverter');
// 字体子集化服务
class FontSubsetter {
async subsetFont(fontBuffer, text, format = 'woff2') {
try {
// 这里可以使用字体处理库来创建子集
// 实际实现可能需要更专业的字体处理工具
const subsetFont = await fontverter.subset(fontBuffer, text, format);
return subsetFont;
} catch (error) {
console.error('Font subsetting error:', error);
return fontBuffer; // 返回原始字体
}
}
async createFontSubset(fontPath, text, outputPath) {
const fs = require('fs');
const fontBuffer = fs.readFileSync(fontPath);
const subsetBuffer = await this.subsetFont(fontBuffer, text);
fs.writeFileSync(outputPath, subsetBuffer);
}
}
// 字体压缩中间件
const fontCompression = () => {
const subsetter = new FontSubsetter();
return async (req, res, next) => {
if (/\.(woff|woff2|ttf|otf)$/.test(req.url)) {
const fontPath = path.join(__dirname, 'public', req.url);
// 检查是否有预生成的子集字体
const textParam = req.query.text;
if (textParam) {
try {
const subsetBuffer = await subsetter.subsetFont(
fs.readFileSync(fontPath),
textParam,
path.extname(fontPath).substring(1)
);
res.set({
'Content-Type': 'font/' + path.extname(fontPath).substring(1),
'Content-Length': subsetBuffer.length,
'Cache-Control': 'public, max-age=31536000',
'Vary': 'User-Agent'
});
res.send(subsetBuffer);
return;
} catch (error) {
console.error('Font subsetting failed:', error);
}
}
}
next();
};
};
app.use(fontCompression());
压缩性能监控
压缩效果监控
javascript
// 压缩效果监控
class CompressionMonitor {
constructor() {
this.metrics = {
totalRequests: 0,
compressedRequests: 0,
originalSize: 0,
compressedSize: 0,
compressionRatio: 0
};
}
recordCompression(originalSize, compressedSize) {
this.metrics.totalRequests++;
this.metrics.compressedRequests++;
this.metrics.originalSize += originalSize;
this.metrics.compressedSize += compressedSize;
if (this.metrics.originalSize > 0) {
this.metrics.compressionRatio =
(1 - this.metrics.compressedSize / this.metrics.originalSize) * 100;
}
}
getMetrics() {
return {
...this.metrics,
savings: this.metrics.originalSize - this.metrics.compressedSize,
averageCompressionRatio: this.metrics.compressionRatio
};
}
}
const compressionMonitor = new CompressionMonitor();
// 带监控的压缩中间件
const monitoredCompression = () => {
return (req, res, next) => {
const acceptEncoding = req.headers['accept-encoding'];
if (!acceptEncoding) {
return next();
}
// 保存原始的 write 和 end 方法
const originalWrite = res.write;
const originalEnd = res.end;
let chunks = [];
let originalSize = 0;
if (acceptEncoding.match(/\bgzip\b/)) {
res.setHeader('Content-Encoding', 'gzip');
res.setHeader('Vary', 'Accept-Encoding');
const zlib = require('zlib');
const gzip = zlib.createGzip({ level: 6 });
gzip.on('data', (chunk) => {
chunks.push(chunk);
});
gzip.on('end', () => {
const compressedSize = Buffer.concat(chunks).length;
compressionMonitor.recordCompression(originalSize, compressedSize);
});
res.write = function(chunk, encoding) {
if (chunk) {
originalSize += Buffer.byteLength(chunk, encoding);
}
gzip.write(chunk, encoding);
};
res.end = function(chunk, encoding) {
if (chunk) {
originalSize += Buffer.byteLength(chunk, encoding);
gzip.write(chunk, encoding);
}
gzip.end();
};
}
next();
};
};
app.use(monitoredCompression());
// 监控端点
app.get('/metrics/compression', (req, res) => {
res.json(compressionMonitor.getMetrics());
});
压缩策略最佳实践
选择合适的压缩算法
javascript
// 智能压缩选择器
class SmartCompressor {
static getBestCompression(req, res) {
const acceptEncoding = req.headers['accept-encoding'] || '';
// 优先级: br > gzip > identity
if (acceptEncoding.includes('br')) {
return 'brotli';
} else if (acceptEncoding.includes('gzip')) {
return 'gzip';
} else {
return 'identity'; // 不压缩
}
}
static shouldCompress(req, res, options = {}) {
// 不压缩小文件
const minSize = options.minSize || 1024; // 1KB
// 不压缩已压缩的文件类型
const noCompressTypes = options.noCompressTypes || [
'image/',
'video/',
'application/zip',
'application/x-gzip'
];
// 检查 Content-Type
const contentType = res.getHeader('Content-Type') || '';
if (noCompressTypes.some(type => contentType.includes(type))) {
return false;
}
// 检查 Accept-Encoding
if (!req.headers['accept-encoding']) {
return false;
}
return true;
}
}
// 高级压缩中间件
const advancedCompression = (options = {}) => {
return (req, res, next) => {
// 检查是否应该压缩
if (!SmartCompressor.shouldCompress(req, res, options)) {
return next();
}
// 选择最佳压缩算法
const compressionType = SmartCompressor.getBestCompression(req, res);
if (compressionType === 'brotli') {
res.setHeader('Content-Encoding', 'br');
res.setHeader('Vary', 'Accept-Encoding');
const zlib = require('zlib');
const brotli = zlib.createBrotliCompress({
params: {
[zlib.constants.BROTLI_PARAM_QUALITY]: options.brotliQuality || 6
}
});
// 实现压缩逻辑...
} else if (compressionType === 'gzip') {
res.setHeader('Content-Encoding', 'gzip');
res.setHeader('Vary', 'Accept-Encoding');
const zlib = require('zlib');
const gzip = zlib.createGzip({
level: options.gzipLevel || 6
});
// 实现压缩逻辑...
}
next();
};
};
app.use(advancedCompression({
minSize: 2048, // 2KB
brotliQuality: 9,
gzipLevel: 9
}));
压缩配置建议
| 内容类型 | 推荐压缩 | 压缩级别 | 说明 |
|---|---|---|---|
| HTML | Brotli | 9 | 高压缩比,文本内容 |
| CSS | Brotli | 9 | 高压缩比,去除空白 |
| JavaScript | Brotli | 6-9 | 平衡压缩比和性能 |
| JSON | Gzip/Brotli | 6 | API 响应压缩 |
| SVG | Gzip | 6 | XML 格式适合压缩 |
| 图片 | 不压缩 | - | 已经是压缩格式 |
| 字体 | 子集化 | - | 按需加载字符集 |
通过实施这些压缩优化策略,您可以显著减少传输数据量,提高页面加载速度,改善用户体验。