Skip to main content
Compression can significantly improve network performance, but improper use can actually degrade performance. This document explains optimization strategies for using compression effectively.

The Trade-offs of Compression

Compression trades CPU time for network time.

Break-even Point

// Without compression
Total time = Network time
           = 100KB / 1MB/s = 100ms

// With compression
Total time = Compression time + Network time + Decompression time
           = 5ms + (10KB / 1MB/s) + 2ms
           = 5ms + 10ms + 2ms = 17ms

Benefit = 100ms - 17ms = 83ms (83% improvement)
Conclusion: The slower the network and larger the data, the greater the compression benefit

Finding the Optimal Threshold

Threshold is the minimum size to compress.

Effects by Threshold

SizeThreshold: 256Threshold: 1024Threshold: 4096
100BNo compression (overhead)No compressionNo compression
500BCompress (small effect)No compressionNo compression
2KBCompressCompressNo compression
10KBCompressCompressCompress
compress: {
  threshold: 256,
  encodings: ["br", "gzip", "deflate"],
}
Suitable for:
  • Mobile networks (slow speeds)
  • Expensive bandwidth costs
  • Most responses are small (< 10KB)
Disadvantage: Increased CPU usage

Finding Optimal Value Through Experimentation

// Test configuration
const thresholds = [256, 512, 1024, 2048, 4096];

for (const threshold of thresholds) {
  console.time(`threshold-${threshold}`);

  // 100 requests
  for (let i = 0; i < 100; i++) {
    await fetch('/api/products', {
      headers: { 'Accept-Encoding': 'gzip' }
    });
  }

  console.timeEnd(`threshold-${threshold}`);
}

// Analyze results: Select the fastest threshold

Algorithm Selection Strategy

Brotli vs Gzip

CriteriaBrotli (br)Gzip
Compression ratioBest (15-20% better)Medium
Compression speedSlow (higher CPU usage)Fast
Decompression speedFastFast
Browser supportModern browsersAll browsers

Usage Strategies

Pre-compression -> Use Brotli
# Pre-compress with brotli at build time
brotli -q 11 bundle.js  # Highest quality
# -> Creates bundle.js.br

# Serve pre-compressed files from server
Advantages:
  • No compression time concerns (pre-compressed)
  • Best compression ratio (quality 11)
  • No runtime CPU load

Environment-Based Strategy

const isProduction = process.env.NODE_ENV === 'production';
const isFastNetwork = process.env.NETWORK_SPEED === 'fast';

export const config: SonamuConfig = {
  server: {
    plugins: {
      compress: isProduction
        ? isFastNetwork
          ? {
              // Fast network: conservative
              threshold: 4096,
              encodings: ["gzip"],
            }
          : {
              // Slow network: aggressive
              threshold: 256,
              encodings: ["br", "gzip", "deflate"],
            }
        : false  // Development: no compression
    }
  }
};

Combining Caching and Compression

Using compression with caching achieves the best performance.
@cache({ ttl: "10m" })  // Server cache
@api({
  httpMethod: 'GET',
  cacheControl: { maxAge: 60 },  // HTTP cache
  compress: CompressPresets.aggressive,  // Compression
})
async getProducts() {
  return this.findMany({});
}

Effect

Per-layer optimization:
  1. Browser cache: 0ms (fastest)
  2. CDN cache: 10ms (network only)
  3. Server cache: 50ms (compression only, no DB)
  4. DB query: 500ms (compression + DB)

CDN and Compression

CloudFront Optimization

@api({
  httpMethod: 'GET',
  cacheControl: {
    visibility: 'public',
    maxAge: 60,        // Browser: 1 minute
    sMaxAge: 300,      // CDN: 5 minutes
  },
  compress: {
    threshold: 1024,
    encodings: ["br", "gzip"],  // CloudFront supports brotli
  }
})
async getData() {
  return this.findMany({});
}
CloudFront configuration:
  • Receive compressed response from origin
  • CloudFront caches as-is
  • Serve quickly from edge

Vary Header Consideration

When using compression, responses differ based on Accept-Encoding:
@api({
  cacheControl: {
    vary: ['Accept-Encoding'],  // Separate cache by encoding
  },
  compress: true,
})
async getData() {
  return this.findMany({});
}

Pre-compression

Pre-compressing static files at build time eliminates runtime CPU load.

Build Script

// scripts/compress-assets.ts
import { brotliCompressSync, gzipSync } from "zlib";
import { readdirSync, readFileSync, writeFileSync } from "fs";

const assetsDir = "./dist/assets";
const files = readdirSync(assetsDir);

for (const file of files) {
  if (/\.(js|css|html|json|svg)$/.test(file)) {
    const content = readFileSync(`${assetsDir}/${file}`);

    // Brotli (best compression ratio)
    const br = brotliCompressSync(content, {
      params: {
        [zlib.constants.BROTLI_PARAM_QUALITY]: 11  // Highest quality
      }
    });
    writeFileSync(`${assetsDir}/${file}.br`, br);

    // Gzip (compatibility)
    const gz = gzipSync(content, { level: 9 });
    writeFileSync(`${assetsDir}/${file}.gz`, gz);

    console.log(`${file}: ${content.length} -> br: ${br.length}, gz: ${gz.length}`);
  }
}

Server Configuration

// Serve pre-compressed files
server.get('/assets/:filename', async (req, reply) => {
  const { filename } = req.params;
  const acceptEncoding = req.headers['accept-encoding'] || '';

  // Brotli support
  if (acceptEncoding.includes('br')) {
    const brFile = `./dist/assets/${filename}.br`;
    if (fs.existsSync(brFile)) {
      reply.header('Content-Encoding', 'br');
      return reply.sendFile(`${filename}.br`, { root: './dist/assets' });
    }
  }

  // Gzip support
  if (acceptEncoding.includes('gzip')) {
    const gzFile = `./dist/assets/${filename}.gz`;
    if (fs.existsSync(gzFile)) {
      reply.header('Content-Encoding', 'gzip');
      return reply.sendFile(`${filename}.gz`, { root: './dist/assets' });
    }
  }

  // No compression
  return reply.sendFile(filename, { root: './dist/assets' });
});
Advantages:
  • No runtime CPU load
  • Best compression ratio (quality 11)
  • Instant response

Performance Measurement

1. Browser Developer Tools

Network tab -> Select request

Size:
  1.2 KB (transferred)  <- Compressed size
  10.5 KB (size)        <- Original size

Time:
  Waiting (TTFB): 50ms  <- Server processing (including compression)
  Content Download: 10ms <- Network transfer

2. Server Logging

import { performance } from "perf_hooks";

@api({
  httpMethod: 'GET',
  compress: CompressPresets.aggressive,
})
async getData() {
  const start = performance.now();
  const data = await this.findMany({});
  const dbTime = performance.now() - start;

  console.log(`DB: ${dbTime.toFixed(2)}ms`);
  // Compression time is handled automatically by Fastify

  return data;
}

3. Load Testing

# Load test with Apache Bench
ab -n 1000 -c 10 -H "Accept-Encoding: gzip" http://localhost:3000/api/products

# Without compression
ab -n 1000 -c 10 http://localhost:3000/api/products

# Compare results:
# - Time per request (average response time)
# - Transfer rate

Practical Optimization Checklist

Enable global compression
plugins: {
  compress: CompressPresets.default
}
Set threshold
  • General: 1024 (1KB)
  • Mobile: 256 (256B)
  • High-performance: 4096 (4KB)
Select algorithm
  • Static: brotli (pre-compressed)
  • Dynamic: gzip (real-time)

Common Mistakes

Mistakes to avoid:
  1. Compressing all responses: Inefficient for small responses
    // Bad example
    threshold: 0  // Compress all responses
    
    // Good example
    threshold: 1024  // Only 1KB or larger
    
  2. Re-compressing already compressed files: No benefit
    // Compressing JPEG, PNG, MP4
    @api({ compress: true })
    async getImage() { ... }
    
    // Exclude from compression
    @api({ compress: false })
    async getImage() { ... }
    
  3. Compressing streams: Causes delays
    // SSE compression
    @stream({ type: 'sse' })
    @api({ compress: true })
    async *streamData() { ... }
    
    // Disable compression
    @api({ compress: false })
    async *streamData() { ... }
    
  4. Compression in development environment: Difficult debugging
    // Environment-based settings
    compress: process.env.NODE_ENV === 'production'
      ? CompressPresets.default
      : false
    
  5. Brotli for real-time compression: High CPU load
    // Warning: brotli for API responses
    compress: {
      encodings: ["br"]  // Slow
    }
    
    // Use gzip
    compress: {
      encodings: ["gzip"]  // Fast
    }
    

Performance Benchmark

Based on actual 100KB JSON response:
ConfigurationCompression TimeTransfer SizeNetwork TimeTotal Time
No compression0ms100KB1000ms1000ms
Gzip2ms14KB140ms142ms
Brotli5ms12KB120ms125ms
Pre-compressed0ms12KB120ms120ms
Conclusion:
  • Gzip: 85% time savings
  • Brotli: 87% time savings
  • Pre-compressed: 88% time savings (best)

Next Steps