compression vs express-static-gzip vs serve-static
Serving Static Assets and Compression in Express
compressionexpress-static-gzipserve-staticSimilar Packages:

Serving Static Assets and Compression in Express

serve-static is the standard middleware for serving static files in Express applications, providing basic file delivery without modification. compression provides middleware to compress responses dynamically using gzip or deflate, reducing bandwidth for dynamic content. express-static-gzip extends static file serving to automatically serve pre-compressed files (like .gz or .br) if they exist on disk, saving CPU cycles during high traffic.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
compression02,79927.7 kB31a year agoMIT
express-static-gzip015224.1 kB5a month agoMIT
serve-static01,42314.3 kB266 months agoMIT

Serving Static Assets and Compression in Express

When building Express applications, delivering assets efficiently is critical for performance. serve-static, compression, and express-static-gzip solve different parts of this problem. serve-static handles basic file delivery. compression shrinks responses on the fly. express-static-gzip serves pre-shrunk files from disk. Let's compare how they work in real scenarios.

🗜️ Compression Strategy: Dynamic vs Pre-Compressed

compression compresses responses dynamically as they leave the server.

  • It works on any response, including API data or generated HTML.
  • Uses CPU cycles for every single request to compress data.
// compression: Dynamic compression middleware
const compression = require('compression');
app.use(compression({ level: 6 }));
// Compresses all passing responses dynamically

express-static-gzip serves files that are already compressed on disk.

  • It looks for .gz or .br versions of the requested file.
  • Saves CPU because the file is read directly from disk without processing.
// express-static-gzip: Serve pre-compressed files
const expressStaticGzip = require('express-static-gzip');
app.use(expressStaticGzip('public', {
  enableBrotli: true,
  orderPreference: ['br', 'gz']
}));
// Serves style.css.br if it exists and client supports it

serve-static does not compress files itself.

  • It sends the raw file content as stored on disk.
  • Relies on external tools (like Nginx or a CDN) for compression.
// serve-static: No built-in compression
const serveStatic = require('serve-static');
app.use(serveStatic('public'));
// Sends raw file content without modification

📂 Static File Serving Logic

compression does not serve files.

  • It is middleware that wraps around other route handlers.
  • Must be paired with a file server like serve-static to handle assets.
// compression: Must be combined with a file server
const compression = require('compression');
const serveStatic = require('serve-static');
app.use(compression());
app.use(serveStatic('public'));
// Compression middleware wraps the static file response

express-static-gzip handles file serving and compression together.

  • It replaces serve-static for static asset directories.
  • Automatically sets correct Content-Encoding headers for pre-compressed files.
// express-static-gzip: All-in-one static serving
const expressStaticGzip = require('express-static-gzip');
app.use(expressStaticGzip('public', {
  serveStatic: { cacheControl: true }
}));
// Handles file lookup and encoding headers internally

serve-static focuses purely on file lookup and delivery.

  • It maps URLs to files in a directory structure.
  • Provides options for caching, redirects, and hidden files.
// serve-static: Standard file lookup
const serveStatic = require('serve-static');
app.use(serveStatic('public', {
  dotfiles: 'ignore',
  etag: true
}));
// Maps /style.css to public/style.css

⚙️ Configuration & Flexibility

compression offers control over compression levels and thresholds.

  • You can set a minimum size before compression starts.
  • You can filter specific content types to exclude from compression.
// compression: Fine-tune compression behavior
app.use(compression({
  threshold: 512, // Only compress if larger than 512 bytes
  filter: (req, res) => {
    if (req.headers['x-no-compression']) return false;
    return true;
  }
}));

express-static-gzip allows preference ordering for encoding types.

  • You can prioritize Brotli over Gzip based on client support.
  • You can define custom compression extensions beyond the defaults.
// express-static-gzip: Control encoding preference
app.use(expressStaticGzip('public', {
  orderPreference: ['br', 'gz', 'deflate'],
  customCompressions: [{
    encodingName: 'zstd',
    fileExtension: 'zst'
  }]
}));

serve-static provides standard HTTP caching and file options.

  • You can control cache headers, etags, and index files.
  • It does not have compression-specific settings.
// serve-static: Control caching and file behavior
app.use(serveStatic('public', {
  maxAge: '1d',
  index: ['index.html', 'index.htm'],
  redirect: true
}));

⚡ Performance Trade-Offs: CPU vs Disk

compression trades CPU for bandwidth.

  • Every request requires processing power to compress data.
  • Can become a bottleneck under very high traffic loads.
// compression: High CPU usage under load
// Each request triggers zlib processing
app.use(compression({ level: 9 })); // Max compression, max CPU

express-static-gzip trades Disk I/O for CPU.

  • Requires storing multiple versions of each file (original, .gz, .br).
  • Reads are faster because no processing is needed during the request.
// express-static-gzip: Low CPU, higher disk usage
// Requires build step to generate .gz/.br files
// npm run build -> generates dist/main.js, dist/main.js.gz

serve-static has minimal overhead but no bandwidth savings.

  • Fastest raw file delivery if compression is not needed.
  • Bandwidth costs are higher if files are sent uncompressed.
// serve-static: Minimal overhead
// No processing, just file stream
app.use(serveStatic('public'));

🤝 Similarities: Shared Ground

While these packages have different goals, they share common ground in the Express ecosystem.

1. 🚂 All Are Express Middleware

  • Each exports a function that fits into the app.use() chain.
  • They follow the standard (req, res, next) signature.
// All use standard Express middleware pattern
app.use(compression());
app.use(serveStatic('public'));
app.use(expressStaticGzip('public'));

2. 🌐 HTTP Header Management

  • All packages interact with response headers to control client behavior.
  • They set Content-Type, Cache-Control, or Content-Encoding.
// compression: Sets Content-Encoding
res.setHeader('Content-Encoding', 'gzip');

// serve-static: Sets Content-Type
res.setHeader('Content-Type', 'text/css');

// express-static-gzip: Sets both
res.setHeader('Content-Encoding', 'br');

3. 🛠️ Configurable Options

  • Each package accepts an options object to customize behavior.
  • Developers can tune performance vs. quality settings.
// All accept options objects
compression({ level: 6 });
serveStatic('public', { maxAge: '1d' });
expressStaticGzip('public', { enableBrotli: true });

📊 Summary: Key Differences

Featurecompressionexpress-static-gzipserve-static
Primary GoalDynamic response compressionServe pre-compressed static filesServe static files
CPU UsageHigh (per request)Low (disk read only)Low (disk read only)
Disk UsageNormalHigher (multiple file versions)Normal
Best ForAPI responses, dynamic HTMLJS bundles, CSS, imagesSimple static assets
SetupMiddleware wrapperDrop-in replace for serve-staticStandard file server

💡 The Big Picture

compression is like a real-time packing service 📦 — it shrinks everything on the fly. Great for dynamic data, but costs CPU time for every visitor.

express-static-gzip is like a warehouse with pre-packed boxes 🏭 — files are shrunk ahead of time. Perfect for static assets where you want maximum speed and minimum CPU load.

serve-static is like a standard shelf 📚 — it delivers files exactly as they are. Best when compression is handled by your CDN or infrastructure layer.

Final Thought: For most production Express apps, combine express-static-gzip for your static assets folder and compression for your API routes. This gives you the best of both worlds — low CPU for static files and flexible compression for dynamic data.

How to Choose: compression vs express-static-gzip vs serve-static

  • compression:

    Choose compression when you need to compress dynamic responses like API JSON data or server-rendered HTML that cannot be pre-compressed on disk. It is ideal for content that changes frequently and requires on-the-fly reduction of payload size. Use this when your server has enough CPU capacity to handle the compression cost for every request.

  • express-static-gzip:

    Choose express-static-gzip for high-traffic static assets like JavaScript bundles, CSS files, and images where you want to save CPU by serving pre-compressed files from disk. It is best when your build process already generates .gz or .br files alongside your original assets. This package reduces server load significantly compared to dynamic compression for static content.

  • serve-static:

    Choose serve-static for simple static file serving where compression is handled elsewhere, such as by a CDN, load balancer, or reverse proxy. It is also suitable for development environments where compression overhead is unnecessary. Use this when you need reliable, standard file serving without additional compression logic in your Node.js application.

README for compression

compression

NPM Version NPM Downloads Build Status OpenSSF Scorecard Badge Funding

Node.js compression middleware.

The following compression codings are supported:

  • deflate
  • gzip
  • br (brotli)

Note Brotli is supported only since Node.js versions v11.7.0 and v10.16.0.

Install

This is a Node.js module available through the npm registry. Installation is done using the npm install command:

$ npm install compression

API

var compression = require('compression')

compression([options])

Returns the compression middleware using the given options. The middleware will attempt to compress response bodies for all requests that traverse through the middleware, based on the given options.

This middleware will never compress responses that include a Cache-Control header with the no-transform directive, as compressing will transform the body.

Options

compression() accepts these properties in the options object. In addition to those listed below, zlib options may be passed in to the options object or brotli options.

chunkSize

Type: Number
Default: zlib.constants.Z_DEFAULT_CHUNK, or 16384.

See Node.js documentation regarding the usage.

filter

Type: Function

A function to decide if the response should be considered for compression. This function is called as filter(req, res) and is expected to return true to consider the response for compression, or false to not compress the response.

The default filter function uses the compressible module to determine if res.getHeader('Content-Type') is compressible.

level

Type: Number
Default: zlib.constants.Z_DEFAULT_COMPRESSION, or -1

The level of zlib compression to apply to responses. A higher level will result in better compression, but will take longer to complete. A lower level will result in less compression, but will be much faster.

This is an integer in the range of 0 (no compression) to 9 (maximum compression). The special value -1 can be used to mean the "default compression level", which is a default compromise between speed and compression (currently equivalent to level 6).

  • -1 Default compression level (also zlib.constants.Z_DEFAULT_COMPRESSION).
  • 0 No compression (also zlib.constants.Z_NO_COMPRESSION).
  • 1 Fastest compression (also zlib.constants.Z_BEST_SPEED).
  • 2
  • 3
  • 4
  • 5
  • 6 (currently what zlib.constants.Z_DEFAULT_COMPRESSION points to).
  • 7
  • 8
  • 9 Best compression (also zlib.constants.Z_BEST_COMPRESSION).

Note in the list above, zlib is from zlib = require('zlib').

memLevel

Type: Number
Default: zlib.constants.Z_DEFAULT_MEMLEVEL, or 8

This specifies how much memory should be allocated for the internal compression state and is an integer in the range of 1 (minimum level) and 9 (maximum level).

See Node.js documentation regarding the usage.

brotli

Type: Object

This specifies the options for configuring Brotli. See Node.js documentation for a complete list of available options.

strategy

Type: Number
Default: zlib.constants.Z_DEFAULT_STRATEGY

This is used to tune the compression algorithm. This value only affects the compression ratio, not the correctness of the compressed output, even if it is not set appropriately.

  • zlib.constants.Z_DEFAULT_STRATEGY Use for normal data.
  • zlib.constants.Z_FILTERED Use for data produced by a filter (or predictor). Filtered data consists mostly of small values with a somewhat random distribution. In this case, the compression algorithm is tuned to compress them better. The effect is to force more Huffman coding and less string matching; it is somewhat intermediate between zlib.constants.Z_DEFAULT_STRATEGY and zlib.constants.Z_HUFFMAN_ONLY.
  • zlib.constants.Z_FIXED Use to prevent the use of dynamic Huffman codes, allowing for a simpler decoder for special applications.
  • zlib.constants.Z_HUFFMAN_ONLY Use to force Huffman encoding only (no string match).
  • zlib.constants.Z_RLE Use to limit match distances to one (run-length encoding). This is designed to be almost as fast as zlib.constants.Z_HUFFMAN_ONLY, but give better compression for PNG image data.

Note in the list above, zlib is from zlib = require('zlib').

threshold

Type: Number or String
Default: 1kb

The byte threshold for the response body size before compression is considered for the response. This is a number of bytes or any string accepted by the bytes module.

Note this is only an advisory setting; if the response size cannot be determined at the time the response headers are written, then it is assumed the response is over the threshold. To guarantee the response size can be determined, be sure set a Content-Length response header.

windowBits

Type: Number
Default: zlib.constants.Z_DEFAULT_WINDOWBITS, or 15

See Node.js documentation regarding the usage.

enforceEncoding

Type: String
Default: identity

This is the default encoding to use when the client does not specify an encoding in the request's Accept-Encoding header.

.filter

The default filter function. This is used to construct a custom filter function that is an extension of the default function.

var compression = require('compression')
var express = require('express')

var app = express()

app.use(compression({ filter: shouldCompress }))

function shouldCompress (req, res) {
  if (req.headers['x-no-compression']) {
    // don't compress responses with this request header
    return false
  }

  // fallback to standard filter function
  return compression.filter(req, res)
}

res.flush

This module adds a res.flush() method to force the partially-compressed response to be flushed to the client.

Examples

express

When using this module with express, simply app.use the module as high as you like. Requests that pass through the middleware will be compressed.

var compression = require('compression')
var express = require('express')

var app = express()

// compress all responses
app.use(compression())

// add all routes

Node.js HTTP server

var compression = require('compression')({ threshold: 0 })
var http = require('http')

function createServer (fn) {
  return http.createServer(function (req, res) {
    compression(req, res, function (err) {
      if (err) {
        res.statusCode = err.status || 500
        res.end(err.message)
        return
      }

      fn(req, res)
    })
  })
}

var server = createServer(function (req, res) {
  res.setHeader('Content-Type', 'text/plain')
  res.end('hello world!')
})

server.listen(3000, () => {
  console.log('> Listening at http://localhost:3000')
})

Server-Sent Events

Because of the nature of compression this module does not work out of the box with server-sent events. To compress content, a window of the output needs to be buffered up in order to get good compression. Typically when using server-sent events, there are certain block of data that need to reach the client.

You can achieve this by calling res.flush() when you need the data written to actually make it to the client.

var compression = require('compression')
var express = require('express')

var app = express()

// compress responses
app.use(compression())

// server-sent event stream
app.get('/events', function (req, res) {
  res.setHeader('Content-Type', 'text/event-stream')
  res.setHeader('Cache-Control', 'no-cache')

  // send a ping approx every 2 seconds
  var timer = setInterval(function () {
    res.write('data: ping\n\n')

    // !!! this is the important part
    res.flush()
  }, 2000)

  res.on('close', function () {
    clearInterval(timer)
  })
})

Contributing

The Express.js project welcomes all constructive contributions. Contributions take many forms, from code for bug fixes and enhancements, to additions and fixes to documentation, additional tests, triaging incoming pull requests and issues, and more!

See the Contributing Guide for more technical details on contributing.

License

MIT