You’ve all heard of Brotli, right? If not, in short, Brotli is a form of compression which surpasses Gzip making websites smaller in size and therefore quicker to load.

Here, at Codo Digital, we use WP-Rocket for our caching, annoyingly it does not come with Brotli support. However it is on their feature list. I found a huge amount of articles suggesting using Cloudflare as it comes with Brotli support. That goes against our ethos as we limit the use of 3rd party services unless absolutely necessary.

Currently we use Gzip to compress the HTML, CSS and Javascript which drastically reduces the size of the assets. After seeing that Brotli can make these assets even smaller, it makes sense to offer that option to our clients as well. 

As I mentioned before, WP-Rocket does not yet support Brotli, which I found odd as Brotli isn’t new by any stretch.

I started by trying to install Brotli on the server on the hope of using one process to compress all assets across all clients. Now, this ended up being scratched off as the Synology server seems to have intentionally removed Brotli from it’s in-built Apache 2.4.

I decided to do this in two stages.
1) compress cached HTML files
2) Let Apache compress assets on the fly

I wrote this python script which will compress cached HTML files generated from WP-Rocket. There was a lot of trial and error with this script, specifically around encodings.

import brotlicffi, sys, getopt, os
from charset_normalizer import from_path

def main(argv):
    inputfile = ''
    try:
        opts, args = getopt.getopt(argv,"hi:",["ifile="])
    except getopt.GetoptError:
        print('test.py -i <inputfile>')
        sys.exit(2)
    for opt, arg in opts:
      if opt == '-h':
        print('test.py -i <inputfile>')
        sys.exit()
      elif opt in ("-i", "--ifile"):
        inputfile = arg
    /* Do not process if Brotli file already exists */
    if not os.path.isfile(inputfile + '_br'):
        file_data = from_path(inputfile)
        /* Have we managed to get the encoding */
        if file_data.best() is not None:
            print('Processing ' + inputfile + '(' + file_data.best().encoding + ')')
            thefile = open(inputfile, 'r', encoding=str(file_data.best().encoding))
        else:
            print('Processing ' + inputfile)
            thefile = open(inputfile)
        file_contents_decoded = thefile.read()
        /* Compress the file into Brotli format from the UTF-8 encoded contents */
        data = brotlicffi.compress(file_contents_decoded.encode('utf-8'))
        thefile.close()
        /* Save the file with the _br extension appended, eg. .html becomes .html_br */
        file = open(inputfile + '_br', "wb")
        file.write(data)
        file.close()
        /* Fix ownership permissions */
        os.chown(inputfile + '_br', 33, 33)
    else:
        print('.', end='')
if __name__ == "__main__":
    main(sys.argv[1:])

A cron is run to find the relevant files and compress them hourly. If the Brotli version does not exist, the Gzip version will be used instead.

find /path/to/files \
-type f \
-size +1b \
\( -name "index-https.html" \) \
-print0 | \
while IFS= read -r -d '' path; do \
if [ ! -f "${path}_br" ]; then \
python3 /path/to/generate-brotli-files.py -i $path; \
fi \
done

This scripts finds all files named “index-https.html” and generates a Brotli version for it.

Next, was to modify Apache Vhost configuration to serve the Brotli version of the file if the browser supports it. Now, I should point out that Brotli is widely supported, a full list can be found at https://caniuse.com/brotli.

<IfModule mod_mime.c>
    AddType text/html .html_gzip
    AddEncoding gzip .html_gzip
    AddType text/html .html_br
    AddEncoding br .html_br
</IfModule>
<IfModule mod_setenvif.c>
    SetEnvIfNoCase Request_URI \.html_gzip$ no-gzip
    SetEnvIfNoCase Request_URI \.html_br$ no-brotli
</IfModule>
<IfModule mod_rewrite.c>
        RewriteEngine On
        RewriteBase /
        RewriteCond %{HTTPS} on [OR]
        RewriteCond %{SERVER_PORT} ^443$ [OR]
        RewriteCond %{HTTP:X-Forwarded-Proto} https
        RewriteRule .* - [E=WPR_SSL:-https]
        RewriteCond %{HTTP:Accept-Encoding} gzip
        RewriteCond "%{DOCUMENT_ROOT}/wp-content/cache/wp-rocket/%{HTTP_HOST}%{REQUEST_URI}/index%{ENV:WPR_SSL}%{ENV:WPR_WEBP}.html_gzip" -f
        RewriteRule .* - [E=WPR_ENC:_gzip]
        RewriteCond %{HTTP:Accept-Encoding} br
        RewriteCond "%{DOCUMENT_ROOT}/wp-content/cache/wp-rocket/%{HTTP_HOST}%{REQUEST_URI}/index%{ENV:WPR_SSL}%{ENV:WPR_WEBP}.html_br" -f
        RewriteRule .* - [E=WPR_ENC:_br]
        RewriteCond %{REQUEST_METHOD} GET
        RewriteCond %{QUERY_STRING} =""
        RewriteCond %{HTTP:Cookie} !(wordpress_logged_in_.+|wp-postpass_|wptouch_switch_toggle|comment_author_|comment_author_email_) [NC]
        RewriteCond %{REQUEST_URI} !^(/(.+/)?feed/?.+/?|/(?:.+/)?embed/|/(index\.php/)?wp\-json(/.*|$))$ [NC]
        RewriteCond %{HTTP_USER_AGENT} !^(facebookexternalhit).* [NC]
        RewriteCond "%{DOCUMENT_ROOT}/wp-content/cache/wp-rocket/%{HTTP_HOST}%{REQUEST_URI}/index%{ENV:WPR_SSL}%{ENV:WPR_WEBP}.html%{ENV:WPR_ENC}" -f
        RewriteRule .* "/wp-content/cache/wp-rocket/%{HTTP_HOST}%{REQUEST_URI}/index%{ENV:WPR_SSL}%{ENV:WPR_WEBP}.html%{ENV:WPR_ENC}" [L]
</IfModule>

That has sorted the cached HTML, but we still have a lot to do. The assets (CSS and Javascript) are still Gzipped as they’re not cached like the HTML. As I mentioned previously, the Synology server does not have Brotli support, so I ended up replacing the Web Station entries with Docker containers.

Here, at Codo Digital, we love Docker. Each website has a WordPress container, Database container and now an Assets container which is a pure and simple Apache container with some bespoke rules in order to output in Brotli format.

Instead of having to replace every single css and js file with a Brotli version, I only needed to add the following code

There you have it, a site running WordPress with Brotli compressed HTML, CSS and Javascript.

    # 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>

This will automatically send a Brotli compressed file (if the browser supports it) and Gzipped otherwise. At this point I was a bit cautious as what I didn’t want to do was cause a drain on the server for the busy sites.

<IfModule mod_cache.c>
    CacheQuickHandler on
    <IfModule mod_cache_disk.c>
        CacheRoot "/var/cache/apache2/mod_cache_disk"
        CacheEnable disk  "/"
        CacheDirLevels 5
        CacheDirLength 3
    </IfModule>
</IfModule>

This snippet of code will cache the headers and data to disk so repeated requests are not processed again.

There we have it, a Brotli supported website. I couldn’t say that was the easiest thing to achieve, which is probably why WP-Rocket haven’t yet implemented it, but I feel a sense of achievement.