Skip to content
On this page

压缩优化

压缩优化是提高 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
}));

压缩配置建议

内容类型推荐压缩压缩级别说明
HTMLBrotli9高压缩比,文本内容
CSSBrotli9高压缩比,去除空白
JavaScriptBrotli6-9平衡压缩比和性能
JSONGzip/Brotli6API 响应压缩
SVGGzip6XML 格式适合压缩
图片不压缩-已经是压缩格式
字体子集化-按需加载字符集

通过实施这些压缩优化策略,您可以显著减少传输数据量,提高页面加载速度,改善用户体验。