Gzip Compression #

In the modern web era where frontend JavaScript application sizes and CSS libraries keep swelling, network data transfer speed has become one of the main determinants of user experience. Slow-loading web pages increase bounce rates and lower our site’s SEO ranking on search engines.

One of the easiest, fastest, and most efficient ways to cut our web page loading time by 70-80% is enabling Gzip Compression in Nginx. Gzip is a very popular data compression algorithm for compacting text files before sending them over the internet. In this article, we’ll discuss how Gzip works, detail the best configuration parameters for production, distinguish which data types deserve compression, apply advanced optimization techniques using gzip_static (pre-compression), compare it with Google’s Brotli algorithm, and do direct verification using the terminal.

How Does Gzip Reduce Data Transfer Size? #

Gzip is based on the DEFLATE compression algorithm, which combines the LZ77 and Huffman encoding algorithms. This algorithm works very effectively on text files because text files (like HTML, CSS, JavaScript, and JSON) contain lots of repeated character patterns, keywords, HTML tags, or property names. Gzip replaces those repetitions with short pointer markers that are much smaller in size.

Here’s an overview of the HTTP communication flow using Gzip compression:

  1. Negotiation Header Delivery: The client browser sends an HTTP request to our server including a header declaring its compression support: Accept-Encoding: gzip, deflate, br.
  2. Compression Processing on the Server: Nginx detects that header, reads the requested file, and compresses it instantly in RAM (on-the-fly) before sending it.
  3. Compressed File Delivery: Nginx sends the reduced file to the internet, accompanied by an identification header: Content-Encoding: gzip.
  4. Browser Decompression: The client browser receives the compressed file, extracts (decompresses) it back to its original size in the client computer’s memory, then renders it to the user’s screen.

The trade-off of this process is very favorable: we exchange a little CPU power cycles on the server and client browser for very significant bandwidth savings and drastic network latency reduction.


Basic Gzip Configuration in Nginx #

To enable and optimize Gzip globally, we write declarations inside the http context block in the nginx.conf configuration file:

http {
    # 1. Enable the Nginx Gzip compression module
    gzip on;

    # 2. Minimum response size limit worth compressing (1024 bytes = 1 KB)
    # Don't compress files under 1KB because compression overhead could actually bloat the file
    gzip_min_length 1024;

    # 3. Compression level: 1 (fastest, minimal compression) to 9 (slowest, maximum compression)
    # Level 6 is the ideal sweet-spot: very good compression with minimal CPU load
    gzip_comp_level 6;

    # 4. Minimum HTTP version supported for compression
    gzip_http_version 1.1;

    # 5. Content types (MIME types) allowed to be compressed
    # Note: text/html is automatically compressed by default, don't write it here
    gzip_types
        text/plain
        text/css
        text/xml
        application/json
        application/javascript
        application/xml
        application/xml+rss
        image/svg+xml
        font/woff
        font/ttf
        font/otf;
}

Explaining the Level 6 Sweet-Spot Parameter #

Many beginner administrators think setting gzip_comp_level to the highest value 9 is the best choice for maximum compression. This is a wrong assumption.

Let’s compare the trade-off characteristics of compression levels:

  • Level 1-2: Very fast compression, but the file size reduction results are less than optimal.
  • Level 6 (Sweet-Spot): Produces up to 70-75% reduction from the original size with very low server CPU consumption.
  • Level 9: Only provides an additional 1-2% smaller reduction compared to level 6, but requires 3 to 5 times heavier CPU computing power. This will extremely burden our server CPU when visited by thousands of users simultaneously.

Proxy and CDN Handling: gzip_vary and gzip_proxied #

When our Nginx server sits behind a proxy, load balancer, or Content Delivery Network (CDN like Cloudflare, Akamai, AWS CloudFront), we must configure two additional directives so the caching process doesn’t break.

http {
    # 1. Add the Vary: Accept-Encoding header to every response
    gzip_vary on;

    # 2. Determine the compression policy for requests coming from proxies
    # "any" means compress all proxy requests regardless of Authorization/caching headers
    gzip_proxied any;
}

Why Is gzip_vary on; Very Important? #

Without the gzip_vary on; directive, a CDN or intermediate proxy server on the internet might store only one version of the response cache.

For example:

  1. An old browser that doesn’t support Gzip visits our site. The CDN requests data from our Nginx, Nginx sends the HTML file without compression, and the CDN stores that uncompressed file in its cache.
  2. A few seconds later, a new user with a modern browser (supporting Gzip) visits our site through the same CDN. The CDN serves the uncompressed HTML cache it stored earlier. As a result, the new user loses the transfer speed optimization benefit.
  3. The opposite can also happen: a modern browser triggers the CDN to cache the Gzip version, then an old browser gets the raw Gzip file that it can’t read (the web page looks scrambled/broken).

By enabling gzip_vary on;, Nginx inserts the Vary: Accept-Encoding response header. This header tells the CDN/proxy to store two separate cache versions: one compressed version (for modern browsers) and one uncompressed version (for old browsers).


File Types That Must Be Compressed vs Forbidden to Compress #

Not all files on our server benefit from Gzip compression. We must clearly distinguish the content types we put into the gzip_types directive.

File Types That Must Be Compressed (High Ratio) #

  • Plain Text: text/plain, text/html
  • Style Sheets & Scripts: text/css, application/javascript, application/json
  • Markup & Vector Formats: text/xml, image/svg+xml
  • Legacy Font Formats: font/ttf, font/otf, font/woff

File Types Forbidden to Compress (CPU Waste) #

  • Modern Images: image/jpeg, image/png, image/webp, image/gif. These images internally already use very dense compression algorithms. Re-compressing them with Gzip won’t make them smaller (sometimes the file size can even grow slightly) and only wastes our server CPU.
  • Video & Audio: video/mp4, audio/mp3, video/webm.
  • Archive Files: application/zip, application/gzip, application/x-tar.
  • Modern WOFF2 Fonts: font/woff2. The WOFF2 font format already uses the Brotli compression algorithm internally by default, so Nginx doesn’t need to compress it again.

Advanced Performance Optimization: gzip_static (Pre-compressed Files) #

Although Nginx is very efficient at on-the-fly compression, the compression process for large files (like a 5 MB production application JavaScript bundle) still takes significant time and CPU cycles for every incoming request.

To cut this CPU overhead down to zero, Nginx provides the ngx_http_gzip_static_module module (enabled with the gzip_static on; directive).

With gzip_static active, when a browser requests the app.js file, Nginx first checks whether a file named app.js.gz exists in the same disk directory. If that pre-compressed .gz file is available, Nginx directly sends it to the client without needing to do compression calculations again.

Here’s a comparison of the Dynamic Gzip vs Static Gzip workflows:

sequenceDiagram
    autonumber
    participant Client as Client Browser
    participant Nginx as Nginx Web Server
    participant Disk as Storage Disk

    Note over Client, Nginx: Scenario 1: gzip_static OFF (Dynamic On-the-fly)
    Client->>Nginx: GET /js/app.js (Accept-Encoding: gzip)
    Nginx->>Disk: Read app.js (Plain text)
    Disk-->>Nginx: Return app.js
    Note over Nginx: Nginx processes compression<br/>of app.js using CPU
    Nginx-->>Client: Send app.js (Content-Encoding: gzip)

    Note over Client, Nginx: Scenario 2: gzip_static ON (Pre-compressed)
    Client->>Nginx: GET /js/app.js (Accept-Encoding: gzip)
    Nginx->>Disk: Check whether app.js.gz exists
    Disk-->>Nginx: Yes, app.js.gz exists
    Nginx-->>Client: Directly send app.js.gz (Content-Encoding: gzip)
    Note over Nginx: Zero server CPU overhead!

gzip_static Configuration #

server {
    listen 80;
    server_name myapp.com;
    root /var/www/myapp/dist;

    location /static/ {
        # Enable direct serving of pre-compressed .gz files
        gzip_static on;
        
        # Fallback to regular gzip if the .gz file isn't found on disk
        gzip on;
        gzip_min_length 1024;
        gzip_types text/css application/javascript;
        
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

Frontend Build Pipeline Integration #

To take advantage of gzip_static, we must configure our frontend build tools (like Vite, Webpack, or Rollup) to produce .gz versions of files during the production asset compilation process.

For example, if using Vite, we can install the vite-plugin-compression plugin:

// vite.config.js
import { defineConfig } from 'vite';
import viteCompression from 'vite-plugin-compression';

export default defineConfig({
  plugins: [
    viteCompression({
      algorithm: 'gzip',
      ext: '.gz',
    })
  ],
});

After running the npm run build command, our dist directory will contain paired files like index.js (for old browsers) and index.js.gz (ready to be served directly by our Nginx gzip_static).


A Modern Alternative: Brotli Compression (ngx_brotli) #

Brotli is a generic open-source data compression algorithm developed by Google in 2015. Brotli is specifically designed for compressing web data and has been proven 15% to 25% more efficient in reducing CSS, JS, and HTML file sizes compared to Gzip at equivalent decompression speeds.

Nginx doesn’t include the Brotli module by default in their standard open-source release packages. We must compile Nginx ourselves by adding Google’s ngx_brotli module, or use an Nginx-based web server distribution like OpenResty that already provides it.

If our Nginx server is already equipped with the ngx_brotli module, here’s the recommended parallel configuration alongside Gzip:

http {
    # 1. Brotli Configuration (First Priority for modern browsers)
    brotli on;
    brotli_comp_level 6; # Brotli sweet spot (range 1-11)
    brotli_types
        text/plain
        text/css
        application/json
        application/javascript
        image/svg+xml
        font/woff2; # Brotli is excellent for the woff2 format

    # 2. Gzip Configuration (Second Priority / Fallback for old browsers)
    gzip on;
    gzip_comp_level 6;
    gzip_types
        text/plain
        text/css
        application/json
        application/javascript
        image/svg+xml;
}

How Does Nginx Choose Between Gzip and Brotli? #

Modern browsers supporting both will send the request header: Accept-Encoding: gzip, deflate, br (br is short for Brotli). Nginx configured with both modules above will prioritize Brotli because its compression is denser. If an old browser only sends Accept-Encoding: gzip, Nginx automatically downgrades to using Gzip compression.


Compression Security: The BREACH and CRIME Vulnerabilities #

Although HTTP compression (Gzip and Brotli) provides outstanding data transmission performance benefits, we must be very alert to security holes that can appear if compression is used together with HTTPS encryption (SSL/TLS). Two famous security attacks exploiting this weakness are CRIME (Compression Ratio Info-leak Made Easy) and BREACH (Browser Reconnaissance and Exfiltration via Adaptive Compression of HTML).

How Does the BREACH Attack Work? #

This side-channel attack exploits the fact that TLS encryption hides the content of an HTTP message, but doesn’t hide the compressed response byte length size.

If an attacker is in a Man-in-the-Middle (MitM) position on our network and can inject malicious JavaScript code into the victim’s browser (e.g., through ads on an insecure HTTP site the victim is browsing), the attacker can force the victim’s browser to automatically send hundreds of HTTP requests to our HTTPS server.

If our HTTPS web page fulfills the following three conditions:

  1. Uses HTTP compression (Gzip or Brotli).
  2. Reflects user input parameters in the HTML response body (e.g., showing search text “Showing results for: [Client Input]”).
  3. Contains static sensitive secrets in the HTML body (like CSRF tokens, OAuth tokens, or session IDs).

Then the attacker can inject guess characters (e.g., guessing the CSRF token start token=a, token=b, etc.) through the input parameter. If the attacker’s guess is correct, the guessed text will match the original token in the HTML body. The presence of this duplicate text makes the compression algorithm detect the word repetition and compress the file to a few bytes smaller than if the guess was wrong. By monitoring the TLS packet byte size fluctuations on the network, the attacker can guess that secret character by character within minutes.

Practical Mitigation of Compression Vulnerabilities #

To protect our application from BREACH attacks, we can apply several of the following mitigation tactics:

  1. Selectively Turn Off Compression: Turn off compression only on locations or pages processing sensitive user data (like profile pages, password changes, or admin panels):
    server {
        listen 443 ssl;
        server_name app.unisbadri.com;
    
        # Enable gzip globally
        gzip on;
    
        location /admin/ {
            # Turn off gzip specifically for sensitive admin areas
            gzip off;
            proxy_pass http://admin_backend;
        }
    }
    
  2. CSRF Token Masking: Make sure our backend framework (like Laravel, Django, or Rails) always masks CSRF tokens on every HTML page render, so the token string value in the HTML always changes randomly on every request even though the original token is the same.
  3. Use Random Padding: Add random invisible HTML tags with random string lengths at the end of the HTML response dynamically from the backend side to make the compressed response byte size always random, thereby disrupting the attacker’s statistical analysis.

How to Install the Brotli Module in Nginx #

As discussed earlier, the Brotli algorithm requires the external ngx_brotli module because it isn’t included by default in standard open-source Nginx releases.

Here are the steps to install the Brotli module on Ubuntu/Debian operating systems:

Method 1: Using a Third-Party Repository (Most Practical) #

We can use the PPA repository maintained by Ondřej Surý, who maintains modern Nginx packages along with their supporting modules:

# Add the PPA repository
sudo add-apt-repository ppa:ondrej/nginx-mainline -y
sudo apt update

# Install the Brotli module for Nginx
sudo apt install libnginx-mod-brotli -y

After installation finishes, the dynamic Brotli module is automatically loaded by Nginx (we can verify it by checking the /etc/nginx/modules-enabled/50-mod-brotli.conf file, which contains load_module modules/ngx_http_brotli_filter_module.so; and load_module modules/ngx_http_brotli_static_module.so; lines).

Method 2: Manual Compilation as a Dynamic Module #

If we want to maintain Nginx ourselves and compile it from source code, we must clone Google’s Brotli module repository:

# 1. Clone the Brotli module source code
git clone --recursive https://github.com/google/ngx_brotli.git

# 2. Download the Nginx source code with a version matching the installed Nginx
# Go into the Nginx source directory, then configure with modular options:
./configure --with-compat --add-dynamic-module=../ngx_brotli

# 3. Compile only the module (without recompiling the main Nginx)
make modules

The compilation result is a .so file that can be copied to the Nginx module directory (/etc/nginx/modules/) and loaded manually using the load_module directive at the very top of nginx.conf.


How to Verify Gzip Compression Is Active via CLI #

We can validate whether our Gzip configuration has been successfully implemented on the production server without opening a browser, using the curl command from our terminal.

1. Checking HTTP Response Headers #

Send a request including the Accept-Encoding: gzip header and check whether the response has the Content-Encoding: gzip header:

curl -H "Accept-Encoding: gzip" -I https://example.com/static/js/main.js

A successful response will show:

HTTP/2 200
server: nginx
content-type: application/javascript
content-encoding: gzip                <--- Indicates Gzip compression is active!
vary: Accept-Encoding                 <--- Indicates proxy/CDN handling is active!
cache-control: public, max-age=31536000

2. Comparing the Real Download Sizes #

We can compare the number of data bytes transmitted over the network with and without compression using this command:

# Scenario 1: Download without compression
curl -o /dev/null -s -w "Size Without Gzip: %{size_download} bytes\n" https://example.com/static/js/main.js

# Scenario 2: Download including the Gzip header
curl -H "Accept-Encoding: gzip" -o /dev/null -s -w "Size With Gzip: %{size_download} bytes\n" https://example.com/static/js/main.js

If our configuration succeeds, we’ll see a very striking size difference (e.g., the size without Gzip is 350,000 bytes while with Gzip it’s only 85,000 bytes).


Summary and Best Practices #

  • Set Comp Level to 6: Use gzip_comp_level 6 for the best balance between compression ratio and our server CPU power consumption.
  • Enable gzip_vary: Always turn on gzip_vary on; to guarantee CDNs or intermediate proxies don’t wrongly send compressed cache files to old browsers.
  • Avoid Binary Image Compression: Don’t include JPEG, PNG, WebP, GIF, MP4, or WOFF2 formats in the gzip_types list because it wastes CPU memory pointlessly.
  • Use gzip_static: Use the gzip_static on; directive to serve .gz files pre-compressed by our frontend build tool to cut real-time compression overhead.
  • Use Brotli If Possible: If we have control over the Nginx compilation, install the ngx_brotli module to get compression sizes 20% smaller than standard Gzip.

← Previous: Worker Process   Next: Caching →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact