There are many tutorials and How-to guides on how to enable compression for WordPress. Recently, I stumbled over Arne Blankerts excellent talk at the 2019 International PHP Conference on YouTube and his suggestion makes a lot of sense. In a world where code is deployed in a automated fashion, it doesn’t make sense to re-compress static assets upon request. Instead, static assets should be compressed upon deploy, for example using our build tools such as Gulp or Grunt. Then we tell Nginx to deliver that pre-compressed file.
That said, it is totally fine, to have the server compress dynamic content such as HTML pages being returned from PHP, but for static assets such as CSS and JS files that doesn’t make sense. That would be a waste of CPU resources and might slow down page load times.

The only time these static files usually change is when we deploy our code to the server.
Both the Brotli and the Gzip modules for Nginx come with an instruction which basically tells the server: “If you find a pre-compressed file, send it to the browser”.

As seen on Arne’s slides on page 11:

/etc/nginx/sites-available/mywebiste.conflocation / {    gzip_static on;    brotli_static on;    try_files $uri $uri/ @php;}location @php {    gzip on;    brotli on;    fastcgi_pass unix:/var/run/php-fpm.sock;    include fastcgi_params;    fastcgi_param SCRIPT_FILENAME /var/www/php/index.php;}

Let’s say we have a file called /assets/styles.css, our main stylesheet. Once the browser tries to load it, gzip_static on; tells Nginx to look for a file called /assets/styles.css.gz and if present it will simply send it to the visitor. If Nginx can’t find neither the pre-compressed nor the static resource, well then it’s probably something dynamic, so the request is passed along to PHP, which is compressed dynamically as indicated by gzip on;. The precedence of gzip over brotli or vice versa is up to the Browsers’ Accept-Encoding header.

A very easy and convenient way of doing this, is by adding a step inside a Github Actions Workflow (more on this in part 4):

.github/workflows/deploy.yaml    # pre-compress static assets with gzip and brotli    - name: Compress Files      uses: stefh/ghaction-CompressFiles@v1      with:        path: ./wordpress        depth: 5        extensions: '.css,.html,.js,.map,.ttf,.otf,.eot,.svg,.woff,.woff2,.xml,.txt,.md'        tools: 'brotli,gzip'

If you don’t or don’t want to use Github Actions, you could also use grunt – or npm compress.

Here an example using Grunt.

module.exports = function(grunt) {    grunt.initConfig({        pkg: grunt.file.readJSON('package.json'),        compress: {            css_gz: {                options: {                  mode: 'gzip',                  level: 6                },                expand: true,                cwd: 'wordpress/',                dest: 'wordpress/',                extDot: 'last',                src: ['**/*.css'],                ext: '.css.gz'            },            css_br: {                options: {                  mode: 'brotli',                  brotli: {                    mode: 1,                    level: 9                  }                },                expand: true,                cwd: 'wordpress/',                dest: 'wordpress/',                extDot: 'last',                src: ['**/*.css'],                ext: '.css.br'            },            ...        }    });    grunt.loadNpmTasks('grunt-contrib-compress');    grunt.registerTask('default', [        'compress:css_gz',        'compress:css_br',        ...    ]);

This example includes two example sub task called css_gz and css_br, of course you might want to define more, for all static assets of your project. In my opinion one should provide both formats, since the decision which format is being used is done by the browser. The more formats you provide, the safer it is that the browser will receive a compressed version.

This is how you would install the Nginx Brotli module manually. It’s important to download the same version of Nginx that you are currently running.

cd ~wget https://nginx.org/download/nginx-1.19.9.tar.gztar -xzf nginx-1.19.9.tar.gzgit clone https://github.com/google/ngx_brotli.git ~/ngx_brotlicd ~/nginx-1.19.9./configure --with-compat --add-dynamic-module=../ngx_brotlimakecp ~/nginx-1.19.9/objs/ngx_http_brotli_filter_module.so /usr/share/nginx/modulescp ~/nginx-1.19.9/objs/ngx_http_brotli_static_module.so /usr/share/nginx/modules

If you’re using Ansible to provision your servers, the folling task does the job, too.

### Brotli Install- name: Gather the apt package facts  package_facts:    manager: auto- name: Get Nginx version and set variable  set_fact:    nginx_version: "{{ ansible_facts.packages['nginx'][0].version | regex_search('^[0-9.]*') }}"- name: Check if module is present (and if so skip nginx compiling)  stat:    path: /usr/lib/nginx/modules/ngx_http_brotli_filter_module.so  become: yes  register: module_exists  - name: Download and unarchive Nginx source code of version {{ nginx_version }}  unarchive:    src: https://nginx.org/download/nginx-{{ nginx_version }}.tar.gz    dest: ~/    remote_src: yes  when: not module_exists.stat.exists- name: Git clone Brotli module (google/ngx_brotli)  git:    repo: https://github.com/google/ngx_brotli.git    dest: ~/ngx_brotli  when: not module_exists.stat.exists- name: Configure Brotli module  command:    chdir: ~/nginx-{{ nginx_version }}    cmd: ./configure --with-compat --add-dynamic-module=../ngx_brotli  when: not module_exists.stat.exists- name: Make Brotli module (Dynamically loaded)  make:    chdir: ~/nginx-{{ nginx_version }}    target: modules  when: not module_exists.stat.exists- name: Copy compiled Brotli module to /usr/share/nginx/modules  copy:     remote_src: yes    src: "{{ item.src }}"    dest: "{{ item.dest }}"    owner: "{{ ansible_user }}"    group: "{{ ansible_user }}"    mode: 0644  with_items:    - { src: "~/nginx-{{ nginx_version }}/objs/ngx_http_brotli_filter_module.so", dest: "/usr/share/nginx/modules" }    - { src: "~/nginx-{{ nginx_version }}/objs/ngx_http_brotli_static_module.so", dest: "/usr/share/nginx/modules" }  when: not module_exists.stat.exists

And finally here’s my suggested list of filetypes for pre-compression, files used in a common WordPress/WooCommerce setup.

  • css
  • js
  • map
  • ttf
  • otf
  • eot
  • svg
  • woff
  • woff2
  • xml
  • txt
  • md

How do you verify it’s working? First you need to get the process ID of (one of your) Nginx Workers. Next we use strace
to report all internals and filter out only matches for gz or br:

ps ax | grep nginxstrace -p 144315 2>&1 | grep gzstrace -p 144315 2>&1 | grep br

Reload some random pages and you should see some output. If you don’t see any output, try with the process ID of the other worker. If you’re still not getting any output there might be an error or misconfiguration.

Also, find a simplified installation instruction in Mike MacCana’s excellent article about Brotli compression on Expedited Security’s Blog.