Apache HTTP webserver:
How to use brotli compression, and pre-compress files to save bandwidth and CPU

This article is about:

It's standard practise to configure an Apache HTTP webserver to compress "non-binary" files "on the fly". A webpage typically contains several "non-binary" files - HTML, CSS, Javascript, KML, SVG etc. All these are compressed (a CPU intensive process) by the webserver on demand each time a client (browser) requests them. This saves bandwidth when they are sent to the client, and so saves the client time and you money. The browser decompresses them (a much quicker process) so it can use them.

Traditionally 'gzip' is used for compression, but Google developed 'brotli', which, at higher degrees of compression, makes much smaller files. The browser request contains a header with a list of compression formats it supports, so the webserver knows which, if any, it can use. All modern browsers support both gzip (legacy) and brotli (new).

As mentioned above, the webserver compression process takes CPU time. It's 'fast' rather than 'best' quality, so it's quite quick, maybe 0.01 to 0.1 CPU seconds depending upon the size and "randomness" of the file. But, this can add up for the client if several files are being requested, and can add up for the webserver if there's a queue of requests all with the same list of files waiting to be re-compressed.

There is a simple solution, pre-compress the popular files (e.g. site.js, site.css, sprites.svg). This has a second advantage, a much higher degree of compression can be used. This takes more CPU time, but that doesn't matter if its only done once, and not when the client is waiting. So this saves you both CPU time and bandwidth, and is quicker for the client as the files being transferred are smaller.

Examples for this site (sizes in K, time in milliseconds)

CSS Javascript
K ms K ms
normal 33.7 338
gzip 8.5 3 85 12
brotli 8.4 3 78 15
gzip (best) 8.0 3 84 18
brotli (best) 7.4 65 70 79

You can see that compression makes a big difference, saving up to 80%, and while brotli is only a little better at 'quick' compression, it's much better at high quality compression

Multiple Suffixes

An Apache URL can can multiple suffixes, e.g. index.html.en.gz is: HTML (mime-type), English (language), and gzip (compression format)

But we have a problem, the brotli suffix is .br which is already used as the Portuguese Brazilian language suffix, so we need to remove 'Brazilian' so as not to cause confusion (or use something like .brotli as a suffix instead of .br)

How to configure it

1. Install brotli

On Ubuntu, its sudo apt install brotli

2. Load the apache module.

On Ubuntu, its sudo a2enmod brotli

3. Enable compression in the order you want

The Apache compression module is 'mod-deflate', but it uses 'gzip' as the compression method.

Compression will be applied in 'Output Filter' directive order. If BROTLI_COMPRESS is first, then it will be tried first.

Gotcha!

If you are using Ubuntu's Apache package, check /etc/apache2/mods-enabled/deflate.conf to make sure the 'deflate' config does not come first.

Main server config

	# -- Server Config --

	# Brotli then Gzip - order is important!	html, text, js, css, xml, gpx, svg
    # Use brotli (if supported), fallback to deflate (gzip)
	AddOutputFilterByType BROTLI_COMPRESS		text/html text/plain text/javascript text/css text/xml application/gpx+xml application/vnd.google-earth.kml+xml image/svg+xml
	AddOutputFilterByType DEFLATE				text/html text/plain text/javascript text/css text/xml application/gpx+xml application/vnd.google-earth.kml+xml image/svg+xml

	# remove language br (portuguese brazilian) in virtual-hosts (cant be in server config)
	AddEncoding		br   br
	AddEncoding		gzip gz

	# don't re-compress
	SetEnvIf Request_URI \.br$	no-brotli=1 no-gzip=1
	SetEnvIf Request_URI \.gz$	no-brotli=1 no-gzip=1

Each virual host

	# -- Virtual Hosts -- 
	# cannot be in server config,
	# or, edit apache mime types config, and delete 'language br' at source
	RemoveLanguage      br

Test: from a browser, site.js and site.js.br should both return the same content (with the .br file's brotli auto-decompressed by your browser)

4. Use mod-rewrite to serve a '.br' file rather than the original

Gotcha!

Apache is very funny about mod-rewrite config. Mod-rewrite config blocks replace rather than adds to previous mod-rewrite config blocks.

There is a RewriteOptions inherit option, but it doesn't seem to work consistently. Either put all the mod-rewrite code in the same place, or at different 'levels', e.g. "main server config" and ".htaccess".

It took me some (very frustrating) trial and error to get it right.s

If you use .htaccess files in sub-directories, test, using curl, that it works from the sub-directory

Explanation of the mod-redirect statements.

  1. ignore sub-requests (e.g. server side include files),
  2. the client must support brotli compression
  3. the file must be a must a file-type we want to pre-compress (.css, .js, .kml, .svg, .gpx in my case)
  4. a ".br" version of the file must exist.
  5. If all these are true, then serve file.br rather than file. Note: the [NS] means no-sub-requests (again)
	RewriteEngine On
	RewriteOptions InheritDown

	# pre-compressed brotli
	RewriteCond %{IS_SUBREQ}				false
	RewriteCond "%{HTTP:Accept-encoding}"	"br"
	RewriteCond "%{REQUEST_FILENAME}"		".*\.(css|js|kml|svg|gpx)$"
	RewriteCond "%{REQUEST_FILENAME}.br"	"-s"
	RewriteRule "^(.*)"						"$1.br" [NS]

5) Compress some test files

e.g. brotli -fZ site.js

( -f : overwrite existing files, -Z : best compression)

6) Test

The 'content-length' size should be the same as the file.br file size on your server.

curl --compressed -I https://www.example.com/site.js

curl --compressed -I https://www.example.com/site.js.br

( --compressed : use compression, -I output headers only )

HTTP/2 200
strict-transport-security: max-age=15768000
vary: Accept-encoding
last-modified: Tue, 30 May 2023 16:06:54 GMT
etag: "17db2a-11310-5fceb644f8380"
accept-ranges: bytes
content-length: 70416
cache-control: max-age=3600
expires: Fri, 02 Jun 2023 13:40:43 GMT
x-frame-options: SAMEORIGIN
content-type: text/javascript
content-encoding: br
date: Fri, 02 Jun 2023 12:40:43 GMT
server: Apache/2.4.57

When testing, check for these 2 headers added by mod-brotli (see below for using curl):

  • vary: Accept-encoding - so proxy caches store brotli, gzip, and normal version of the file for different types of client
  • content-encoding: br - to tell the client the type of compression used

Check your the actual data-transfer amounts in your server log. You should find nearly all browsers, and some crawlers/bots use HTTP/2.0 and brotli, where as some crawlers/bots use HTTP/1.1 and gzip.

Data transfer amounts should be the 'high quality compression' size, not the 'fast compression' size that Apache makes.

7) Change your build process to auto compress your js/css etc. files

Simply add a brotoli -zf {filename} step

8) Make a donation

Finally, work out how much bandwidth and CPU times you've saved, and make a donation (via Paypal)

This is a hiking club's website, most of the CSS and JS libraries come from CDN's, but for our home grown stuff, we save about 0.2 CPU seconds and an extra 20K per page request by pre-compressing (about 20% of our network bandwidth).