極光日記

Serve pre-compressed gzip assets with Nginx

Created:

Overview

This article describes how to serve files that are already gzip-compressed using Nginx.

Static HTML and CSS files rarely change, so you can pre-compress them instead of compressing them on the fly.

  • Lower CPU use at request time
  • Higher compression ratios (for example, using gzip -9 or zopfli)
  • Better cacheability

Real-time compression can be enabled with gzip on;, but higher-compression tools like zopfli are slow, so pre-compressing is preferable when possible.

References:

Assumed layout

Simple structure under test/, with HTML loading a CSS file:

.
└── test
    ├── test.css
    └── test.html

Requirements:

  • Access without extensions (e.g., /test/test should serve /test/test.html)
  • Keep only pre-gzipped files if possible
  • Ignore clients without gzip support (modern browsers support it)

On omitting extensions, see the W3C article: https://www.w3.org/Provider/Style/URI

“cgi”, even “.html” is something which will change. You may not be using HTML for that page in 20 years time, but you might want today’s links to it to still be valid. The canonical way of making links to the W3C site doesn’t use the extension.

Final layout after compression

.
└── test
    ├── test.css.gz
    ├── test.html
    └── test.html.gz

Notes:

  • test.html.gz will be served, but test.html still needs to exist (explained later)
  • test.css could be removed; only the .gz version will be served

Nginx config example

Depending on the situation, you may also need to set gzip_vary or gzip_proxied. For details, please refer to the link provided earlier.

events {
  worker_connections 1024;
}

http {
  include       mime.types;

  gzip on;
  gzip_static on;
  gzip_types text/css text/javascript;

  server {
    listen 80;
    root /usr/share/nginx/html;

    location ~* \.(js|css?)$ {
      add_header Cache-Control "public, max-age=31536000, immutable";
    }

    location / {
      add_header Cache-Control "no-cache, must-revalidate";
      try_files $uri.html $uri $uri/ =404;
    }
  }
}

Notes:

  • gzip_static on; serves .gz files when present
  • If you only serve pre-gzipped files, gzip on; can be omitted
  • try_files makes Nginx look for $uri.html so you can drop extensions

HTML still needs the non-gz file

If you remove test.html and keep only test.html.gz, you get 404.

Working set:

  • test.html → must exist (could probably be empty)
  • test.html.gz → the content actually served

gzip_static does use the .gz file, but try_files still needs the .html to exist. Changing test.html did not change the served content (it kept using the gzipped one), so an empty placeholder might be fine. My guess is try_files simply requires the .html to resolve.