Apache – enabling brotli compression with gzip/deflate fallback

Edit: October 2022 – I’ve also added tutorial for configuring nginx with brotli compression with gzip fallback (handling both statically compressed files and dynamic content):

Brotli is a relatively new generic-purpose lossless compression algorithm that compresses data using a combination of a modern variant of the LZ77 algorithm, Huffman coding, and 2nd order context modeling.

For our purposes, we just should know that it offers significantly better compression ratios and thus smaller file sizes. Smaller file sizes then mean faster site loads.

According to caniuse, most modern browsers do support brotli natively these days:

https://caniuse.com/brotli

It is however still recommended to support both brotli and gzip if possible, hence this article.

Stats from https://caniuse.com/brotli

What assets should I cache?

Typically it is recommended to only compress text-based formats, such as html, css, js and similar.

You are very unlikely to benefit from compressing already compressed formats such as png, jpeg, gif etc.

As a point of comparison – I like to refer people to aws cloudfront compression list that offers some insights into what formats AWS considers well-compressible (hint: binary formats are not on it).

Install brotli and deflate

I’m using ubuntu 20.04 (with Apache/2.4.41) in my case, your syntax will of course be a little different in alpine/rpm or other distros.

sudo apt-get install brotli
sudo a2enmod brotli
sudo a2enmod deflate
#deflate module automatically enables  gzip compression or certain types, however this is best configured by us, as we need brotli as well. 
rm -f /etc/apache2/mods-enabled/deflate.conf

Note that at least in ubuntu 20.04mod_deflate automatically enables gzip’ping of certain content by default once installed and enabled.

This turned out to be a problem for me as we would want brotli to be used by default with gzip only being a fallback.

Thus in the example above I’ve removed /etc/apache2/mods-enabled/deflate.conf file.

Do not re-compress already compressed brotli and gzip files

It s possible that we pre-compress some assets as .gzip or .br files so apache wouldn’t have to re-compress assets every time.

This is just sensible thing to do and should lead to shorter response times in theory. We can easily add brotli compression into our gulp/webpack pipeline (I’ll make a post on this at a later time.)

Without a further adue:

    # Static Brotli:
    # Browser accepts brotli, and matching pre-compressed file exists => rewrite to .br file
    # For each file format set the correct mime type (otherwise brotli mime type is returned) and prevent Apache for recompressing the files
    # Not sure why mime is necessary, but that's how apache docs mention this.
    RewriteCond %{HTTP:Accept-encoding} br
    RewriteCond %{REQUEST_URI} .*\.(css|html|js|svg)
    RewriteCond %{REQUEST_FILENAME}.br -s
    RewriteRule ^(.+) $1.br
    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]
    RewriteRule "\.svg\.br$" "-" [T=image/svg+xml,E=no-brotli,E=no-gzip]

    # Static Gzip:
    # Browser accepts gzip, and matching pre-compressed file exists => rewrite to .gz file
    # For each file format set the correct mime type (otherwise brotli mime type is returned) and prevent Apache for recompressing the files
    # Not sure why mime is necessary, but that's how apache docs mention this.
    RewriteCond %{HTTP:Accept-Encoding} gzip
    RewriteCond %{REQUEST_URI} .*\.(css|html|js|svg|)
    RewriteCond %{REQUEST_FILENAME}.gz -s
    RewriteRule ^(.+) $1.gz
    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]
    RewriteRule "\.svg\.br$" "-" [T=image/svg+xml,E=no-brotli,E=no-gzip]

    <FilesMatch "\.(css|html|js|svg)\.br$">
        # Correct the encoding type
        # Let proxy cache brotli & non-brotli separately
        Header set Content-Encoding br
        Header append Vary Accept-Encoding
    </FilesMatch>
    <FilesMatch "\.(css|html|js|svg)\.gz$">
        # Serve correct encoding type
        # Let proxies cache gzip & non-gzip files separately
        Header set Content-Encoding gzip
        Header append Vary Accept-Encoding
    </FilesMatch>

Now – the above snippet will check if there is a file on the server with exactly the same filename, but with .br extension (or .gzip extension) and redirect the request to that file.

Apache – compress dynamic content as brotli (with gzip fallback)

After static pre-compressed content is dealt with, we should address dynamic content and anything else that is not pre-compressed but can be compressed effectively.

This snippet should be added below snippet dealing with static content.

Here – I’m only compressing on the fly the most typical content types: css, js, html, svg (svg is text/xml based format so is compressible) -> many applications might have different requirements so ymmv (adjust as needed).

Note – you need to add brotli before gzip in order for it to be a preferred compression mechanism:

    # Dynamic Brotli:
    <IfModule mod_brotli.c>
        <IfModule mod_filter.c>
            AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/xml text/css
            AddOutputFilterByType BROTLI_COMPRESS application/x-javascript application/javascript application/ecmascript text/javascript application/javascript application/json
            AddOutputFilterByType BROTLI_COMPRESS application/rss+xml
            AddOutputFilterByType BROTLI_COMPRESS application/xml
            AddOutputFilterByType BROTLI_COMPRESS image/svg+xml
            AddOutputFilterByType BROTLI_COMPRESS application/x-font-ttf application/vnd.ms-fontobject image/x-icon
        </IfModule>
    </IfModule>

    # Dynamic gzip:
    <IfModule mod_deflate.c>
        <IfModule mod_filter.c>
            AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css
            AddOutputFilterByType DEFLATE application/x-javascript application/javascript application/ecmascript text/javascript application/javascript application/json
            AddOutputFilterByType DEFLATE application/rss+xml
            AddOutputFilterByType DEFLATE application/xml
            AddOutputFilterByType DEFLATE image/svg+xml
            AddOutputFilterByType DEFLATE application/x-font-ttf application/vnd.ms-fontobject image/x-icon
        </IfModule>
    </IfModule>

    #Now some content won't benefit from brotli, like already pre-compressed content,
    #So let us not bother with it
    SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png|webp)$ no-brotli no-gzip dont-vary

    # Make sure proxies don't deliver the wrong content
    # It would be ebtter of course if AddOutputFilterByType owuld allow us to set env, but whatever.
    Header append Vary Accept-Encoding env=!dont-vary

2 thoughts on “Apache – enabling brotli compression with gzip/deflate fallback”

    • shouldn’t be compressing the same content twice.
      it’s been a long time at least 5 years since I’ve used apache so cannot confirm it at the moment. But I recall this configuration was working fine and was tested at the time.

      Reply

Leave a Comment