Apache HTTP webserver:
How to use brotli compression, and pre-compress files to save bandwidth and CPU
This article is about:
- configure your Apache HTTP server to use brotli rather than gzip
- pre-compressing files with a higher degree of compression to save the web server CPU-time and transfer bandwidth
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.
- ignore sub-requests (e.g. server side include files),
- the client must support brotli compression
- the file must be a must a file-type we want to pre-compress (.css, .js, .kml, .svg, .gpx in my case)
- a
".br"
version of the file must exist. - If all these are true, then serve
file.br
rather thanfile
. 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 clientcontent-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).