Cookies ahead

Our support chat tool "Intercom" would like to collect some more data on you. See the related link for more details.

Docs

Precompress static assets

Published

Reviewedbyfl

🗜️

Compress at build time.

Compress CSS, JS and SVG files once during the build, at the highest level, and let Apache serve the finished files. Responses get smaller and the web server stops compressing the same file on every request.

About precompression

fortrabbit compresses text responses on the fly with GZIP at level 1. Brotli, when enabled in .htaccess, runs at quality 0. Those are the fastest settings and the weakest, and .htaccess cannot change them. They are set that way on purpose: Apache shares the CPU with PHP, and every request for a stylesheet compresses that stylesheet again.

Static assets do not change between deployments. Compress them once in the build and the level no longer costs anything per request. The same 253 kB stylesheet, four ways:

CompressionTransferred
On the fly, GZIP45 kB
On the fly, Brotli44 kB
Precompressed, GZIP level 935 kB
Precompressed, Brotli quality 1126 kB

1. Compress during the build

Node.js ships GZIP and Brotli in its zlib module, so no extra package is needed. Place this script in the project root and point dir at the folder the asset build writes to:

import { readdirSync, readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { brotliCompressSync, constants, gzipSync } from 'node:zlib'

const dir = 'public/build'

for (const name of readdirSync(dir, { recursive: true })) {
  if (!/\.(css|js|svg)$/.test(name)) continue
  const file = join(dir, name)
  const raw = readFileSync(file)
  writeFileSync(`${file}.gz`, gzipSync(raw, { level: 9 }))
  writeFileSync(`${file}.br`, brotliCompressSync(raw, {
    params: { [constants.BROTLI_PARAM_QUALITY]: 11 },
  }))
}
js
compress.mjs

Run it after the asset build in the build commands:

npm ci
npm run build
node compress.mjs
shell

app.css now sits next to app.css.br and app.css.gz.

2. Serve the compressed files

Add this to the .htaccess in the root path. It rewrites a request to the .br or .gz sibling when the browser accepts that encoding and the file exists:

<IfModule mod_headers.c>
  RewriteEngine On

  # Brotli first
  RewriteCond %{HTTP:Accept-Encoding} br
  RewriteCond %{REQUEST_FILENAME}.br -f
  RewriteRule ^(.+)\.(css|js|svg)$ $1.$2.br [L]

  # GZIP for the rest
  RewriteCond %{HTTP:Accept-Encoding} gzip
  RewriteCond %{REQUEST_FILENAME}.gz -f
  RewriteRule ^(.+)\.(css|js|svg)$ $1.$2.gz [L]

  # Keep the content type, skip on-the-fly compression
  RewriteRule \.css\.(br|gz)$ - [T=text/css,E=no-gzip:1,E=no-brotli:1]
  RewriteRule \.js\.(br|gz)$ - [T=text/javascript,E=no-gzip:1,E=no-brotli:1]
  RewriteRule \.svg\.(br|gz)$ - [T=image/svg+xml,E=no-gzip:1,E=no-brotli:1]

  <FilesMatch "\.(css|js|svg)\.br$">
    Header set Content-Encoding br
    Header append Vary Accept-Encoding
  </FilesMatch>
  <FilesMatch "\.(css|js|svg)\.gz$">
    Header set Content-Encoding gzip
    Header append Vary Accept-Encoding
  </FilesMatch>
</IfModule>
apache
.htaccess

Place the block above the framework's front controller rules. A file without a compressed sibling is still compressed on the fly.

3. Check the response

Request an asset with each encoding and compare content-length with the size of the file on disk:

curl -sI -H 'Accept-Encoding: br' https://example.com/build/app.css
shell

The response carries content-encoding: br, content-type: text/css and the byte count of app.css.br.

Limitations

  • Only files that exist after the build are covered. HTML rendered by PHP is still compressed on the fly.
  • Images, video and woff2 fonts are compressed formats. Do not add them to the pattern.
  • Assets changed on the server after the deployment keep their old .br and .gz siblings until the next build.

Written by humans and AI in collaboration.

AI use & editorial processEdit on GitHub ↗