Pre-compression with gzip and brotli in Apache

My blog is generated using Hugo, as I explained before, which is a static site generator. Since all the pages are generated beforehand, their compressed version can be too, so the webserver will only have to send the compressed file instead of compressing on the fly each time a page is called. Reminder: minifying the pages/files before compressing them will also save a lot of space.

Compression over HTTP was mostly limited to DEFLATE and gzip encodings. They are both using the same compression system but a slightly different format. Because of implementation issues across browsers and server, DEFLATE is mostly unreliable. Different software/algorithms can produce gzip compatible outputs, the most common one being zlib.

In the case of pre-compression the compression is done in advance so more CPU intensive settings can be used in order to achieve higher compression ratio and save bandwidth in the end. I use Zopfli to pre-compress my blog’s files instead of zlib. Zopfli is slower than zlib but produces slightly more compressed files (3 to 8 %) and still compatible with gzip.

More recently a new compression format was introduced and is now supported by most of the major web browsers: Brotli. Brotli compressed files should be at least 15 % smaller than zlib compressed ones.

I use grunt to automate the different steps to build my blog, I added an extra step with grunt-contrib-compress module to pre-compress the files with Brotli. With the index page for example, the following files are generated:

  • index.html
  • index.html.br
  • index.html.gz

Here are the results for the whole blog:

CompressionSize (bytes)Ratio
none6 827 4241.00
gzip (9)2 116 4243.22
zopfli1 757 8833.88
brotli1 548 9444.40

In my case Zopfli is 17 % better than gzip and Brotli is 27 % better than gzip.

Now the files have to be served by Apache (in this example I assume only HTML, CSS and JS files are compressed):

<VirtualHost *>
    # (…)
    DocumentRoot /path/to/my/blog
    <Directory /path/to/my/blog/>
        # (…)
        RewriteEngine on

        # Brotli
        # If the web browser accept brotli encoding… 
        RewriteCond %{HTTP:Accept-encoding} br
        # …and the web browser is fetching a probably pre-compressed file…
        RewriteCond %{REQUEST_URI} .*\.(css|html|js)
        # …and a matching pre-compressed file exists… 
        RewriteCond %{REQUEST_FILENAME}.br -s
        # …then rewrite the request to deliver the brotli file
        RewriteRule ^(.+) $1.br
        # For each file format set the correct mime type (otherwise brotli mime type is returned) and prevent Apache for recompressing the files
        RewriteRule "\.css\.br$" "-" [T=text/css,E=no-brotli,E=no-gzip]
        RewriteRule "\.html\.br$" "-" [T=text/html,E=no-brotli,E=no-gzip]
        RewriteRule "\.js\.br$" "-" [T=application/javascript,E=no-brotli,E=no-gzip]
        
        # Gzip
        # If the web browser accept gzip encoding… 
        RewriteCond %{HTTP:Accept-Encoding} gzip
        # …and the web browser is fetching a probably pre-compressed file…
        RewriteCond %{REQUEST_URI} .*\.(css|html|js)
        # …and a matching pre-compressed file exists… 
        RewriteCond %{REQUEST_FILENAME}.gz -s
        # …then rewrite the request to deliver the gzip file
        RewriteRule ^(.+) $1.gz
        # For each file format set the correct mime type (otherwise gzip mime type is returned) and prevent Apache for recompressing the files
        RewriteRule "\.css\.gz$" "-" [T=text/css,E=no-brotli,E=no-gzip]
        RewriteRule "\.html\.gz$" "-" [T=text/html,E=no-brotli,E=no-gzip]
        RewriteRule "\.js\.gz$" "-" [T=application/javascript,E=no-brotli,E=no-gzip]

        <FilesMatch "\.(css|html|js)\.br$">
            # Prevent mime module to set brazilian language header (because the file ends with .br)
            RemoveLanguage .br
            # Set the correct encoding type
            Header set Content-Encoding br
            # Force proxies to cache brotli & non-brotli files separately
            Header append Vary Accept-Encoding
        </FilesMatch>
        <FilesMatch "\.(css|html|js)\.gz$">
            # Serve correct encoding type
            Header set Content-Encoding gzip
            # Force proxies to cache gzip & non-gzip files separately
            Header append Vary Accept-Encoding
        </FilesMatch>
    </Directory>
</VirtualHost>

Comments Add one by sending me an email.

  • From Adrián ·

    Hello Laurent,

    Do you have mod_brotli installed? Or something like that? Or with precompression we don't need that type of stuff?

    Thanks in advance!

  • From Laurent ·

    Hi Adrián,

    No, mod_brotli is not required. The goal of mod_brotli is to compress HTTP responses on the fly, with pre-compression you don't need it. In fact you can see that the rewrite rules specify E=no-brotli to ensure that pre-compressed content is not compressed again.

    You can have mod_brotli activated, it will compress the content that is not pre-compressed.