Express can compress responses and tell browsers how to cache them, dramatically improving load times and reducing server load.

Here’s a live example of an Express app serving a static file with both compression and caching enabled. Notice the Content-Encoding: gzip and Cache-Control headers in the response:

const express = require('express');
const compression = require('compression');
const app = express();
const port = 3000;

// Enable gzip compression for all responses
app.use(compression());

// Serve static files with cache control
app.use(express.static('public', {
  maxAge: '1y', // Cache for 1 year
  setHeaders: (res, path, stat) => {
    res.set('Cache-Control', 'public, max-age=' + Math.floor(86400 * 365)); // Explicitly set Cache-Control
  }
}));

app.get('/', (req, res) => {
  res.send('Hello World!');
});

app.listen(port, () => {
  console.log(`App listening at http://localhost:${port}`);
});

To see this in action, create a public directory, put an index.html file in it, run the app, and then inspect the network requests in your browser’s developer tools. You’ll see the Content-Encoding: gzip header for text-based assets and Cache-Control: public, max-age=31536000 for files served from the public directory.

The Problem: Slow Loads and High Server Costs

Without compression, text-based assets like HTML, CSS, and JavaScript are sent over the network in their raw, uncompressed form. This means larger payloads, longer download times for users, and more bandwidth consumed by your server. For frequently requested static assets, this inefficiency adds up quickly, leading to a sluggish user experience and increased hosting costs.

The Solution: Gzip Compression and Smart Caching

Gzip Compression: This is a widely supported compression algorithm that can significantly reduce the size of text-based files. When a client (like a browser) requests a resource, it can tell the server it accepts gzip encoding by sending the Accept-Encoding: gzip header. If the server supports it, it compresses the response on the fly and sends it back with the Content-Encoding: gzip header. The browser then automatically decompresses it.

Cache-Control Headers: These HTTP headers instruct browsers and intermediate caches (like CDNs) on how to cache resources. By setting appropriate Cache-Control directives, you can tell the browser to store a copy of a resource locally for a specified period. When the user requests the same resource again, the browser can serve it from its local cache instead of re-downloading it from your server. This drastically reduces load times for repeat visits and reduces the load on your server.

How It Works Under the Hood

The compression middleware in Express works by inspecting the Accept-Encoding header of incoming requests. If it finds gzip (or deflate), it compresses the outgoing response body using the corresponding algorithm. It then adds the Content-Encoding header to the response. For static files, express.static can be configured to set the Cache-Control header. The maxAge option in express.static is a convenient way to set this, specifying the duration in milliseconds for which the resource should be considered fresh. The setHeaders callback provides more granular control, allowing you to construct custom Cache-Control strings.

The public, max-age=31536000 directive means:

  • public: The response can be cached by any cache, including browser and shared caches (like CDNs).
  • max-age=31536000: The resource is considered fresh for 31,536,000 seconds (1 year). After this period, the cache must revalidate with the origin server.

Fine-Tuning Compression

The compression middleware is quite configurable. You can pass an options object to compression() to customize its behavior. For instance, you might want to set a minimum threshold for compression, or exclude certain file types.

app.use(compression({
  level: 9, // Compression level (0-9, 9 is best compression)
  threshold: 10 * 1024, // Compress responses larger than 10KB
  filter: (req, res) => {
    // Only compress responses with 'text/html' or 'text/css' content types
    return res.getHeader('content-type') && res.getHeader('content-type').indexOf('text') === 0;
  }
}));

Setting level: 9 uses the highest compression ratio, which takes slightly more CPU but yields the smallest file sizes. threshold: 10 * 1024 ensures that very small files, which might not benefit much from compression (and could even be larger after compression due to overhead), are not compressed. The filter function allows you to precisely control which MIME types get compressed.

The most surprising true thing about Cache-Control is that max-age is a suggestion to the cache, not a hard command. While browsers and most proxies honor it strictly, a misconfigured or aggressive proxy might ignore it entirely. This is why combining max-age with other directives like public and potentially immutable (for truly unchanging assets) can help ensure consistent caching behavior across different environments.

The next logical step is to implement a Content Delivery Network (CDN) to further distribute your cached assets geographically.

Want structured learning?

Take the full Express course →