[HowTo] Serve precompressed matomo.js / piwik.js with Brotli & gzip (Apache)

For high-traffic Matomo installations, you can squeeze out a small but meaningful performance win by serving precompressed tracking JS (matomo.js / piwik.js) instead of compressing it on every request on the fly.

In our tests this reduced the transfer size of matomo.js by roughly ~10% compared to on-the-fly Brotli, and also saves CPU because the file is compressed once and then reused.

This guide shows how to:

  1. Generate matomo.js.gz and matomo.js.br (and the same for piwik.js)

  2. Keep them in sync after Matomo updates

  3. Configure Apache to automatically serve the precompressed versions where supported


1) Precompression script for matomo.js and piwik.js

Create a small script, e.g.:

sudo nano /usr/local/bin/matomo-precompress.sh

Content:

#!/bin/bash

# Path to your Matomo installation (directory where matomo.js / piwik.js lives)
MATOMO_DIR="/var/www/piwik"   # <-- adjust to your environment

cd "$MATOMO_DIR" || exit 1

# Files to precompress
FILES=("matomo.js" "piwik.js")

for FILE in "${FILES[@]}"; do
    # Skip if the JS file does not exist
    if [ ! -f "$FILE" ]; then
        continue
    fi

    # Create / update GZIP
    if [ ! -f "$FILE.gz" ] || [ "$FILE" -nt "$FILE.gz" ]; then
        echo "  -> Creating/updating $FILE.gz"
        gzip -k -f -9 "$FILE"
        # -k: keep original; -9: max compression
    fi

    # Create / update Brotli
    if [ ! -f "$FILE.br" ] || [ "$FILE" -nt "$FILE.br" ]; then
        echo "  -> Creating/updating $FILE.br"
        brotli -f -q 11 "$FILE"
        # ensure .br has the same mtime as the original JS
        touch -r "$FILE" "$FILE.br"
    fi
done

Make it executable:

sudo chmod +x /usr/local/bin/matomo-precompress.sh

What this does:

  • For each of matomo.js and piwik.js (if present), it:

    • generates/updates *.js.gz using gzip -9

    • generates/updates *.js.br using brotli -q 11

  • It only regenerates the compressed files if the original .js is newer, so it’s safe to run frequently.


2) Keep compressed assets up-to-date via cron

Run the script periodically as the webserver user (here: apache, adjust as needed):

sudo -u apache crontab -e

Example cron entry (every 5 minutes):

0,5,10,15,20,25,30,35,40,45,50,55 * * * * /usr/local/bin/matomo-precompress.sh >/dev/null 2>&1

You can also use */30 or @hourly; Matomo’s JS doesn’t change very often.

Whenever Matomo (or a plugin) changes matomo.js, the precompressed files will be updated shortly after.


3) Apache config to serve precompressed matomo.js / piwik.js

In your Apache vhost config, add a <Directory> block for your Matomo root, e.g.:

<Directory "/var/www/piwik">
    Options FollowSymLinks
    AllowOverride None
    Require all granted

    # Serve precompressed Matomo/Piwik JS
    RewriteEngine On

    # JS: prefer .br if client supports Brotli and file exists
    RewriteCond %{REQUEST_URI} \.(?:m?js)$ [NC]
    RewriteCond %{HTTP:Accept-Encoding} \bbr\b
    RewriteCond "%{REQUEST_FILENAME}.br" -f
    RewriteRule ^(.+\.(?:m?js))$ $1.br [L]

    # JS: fallback to .gz if client supports gzip and file exists
    RewriteCond %{REQUEST_URI} \.(?:m?js)$ [NC]
    RewriteCond %{HTTP:Accept-Encoding} \bgzip\b
    RewriteCond "%{REQUEST_FILENAME}.gz" -f
    RewriteRule ^(.+\.(?:m?js))$ $1.gz [L]

    # Headers for JS (+ precompressed variants)
    <IfModule mod_headers.c>
      <FilesMatch "\.(?:m?js)(?:\.(?:br|gz))?$">
        Header append Vary "Accept-Encoding"
        Header set Content-Type "text/javascript; charset=UTF-8"
        Header always set X-Content-Type-Options "nosniff"
      </FilesMatch>
    </IfModule>

    # Avoid double compression for already-compressed files
    <IfModule mod_deflate.c>
      SetEnvIfNoCase Request_URI "\.(?:br|gz)$" no-gzip=1
    </IfModule>
    <IfModule mod_brotli.c>
      SetEnvIfNoCase Request_URI "\.(?:br|gz)$" no-brotli=1
    </IfModule>

    # MIME/encoding mappings for .js.br and .js.gz
    <IfModule mod_mime.c>
      AddType application/javascript .js
      AddType application/javascript .js.gz
      AddType application/javascript .js.br
      AddEncoding gzip .gz
      AddEncoding br .br
    </IfModule>
</Directory>

Reload Apache:

apachectl -t && systemctl reload httpd
# or the equivalent command on your system

How it behaves:

  • Requests for matomo.js / piwik.js:

    • if browser supports Brotli and matomo.js.br exists → serve that

    • else if browser supports gzip and matomo.js.gz exists → serve that

    • else → serve the plain matomo.js

  • Vary: Accept-Encoding and X-Content-Type-Options: nosniff are set

  • mod_deflate / mod_brotli will not try to recompress already compressed files

From the browser’s view the URL is still /matomo.js, but the response has e.g.:

  • Content-Encoding: br

  • Content-Type: text/javascript; charset=UTF-8

  • Vary: Accept-Encoding


4) Note about Matomo file integrity check

One caveat with this approach in Matomo right now:

The file integrity check reports the precompressed files (matomo.js.br, matomo.js.gz, piwik.js.br, piwik.js.gz) as unexpected and suggests deleting them.

It would be useful if Matomo provided a configuration option or whitelist to ignore these specific files in the integrity check so admins can use precompressed assets without a permanent warning.


Hope this helps others running Matomo on higher-traffic setups who want a simple, safe micro-optimization for tracking JS.

2 Likes