# Precompress static assets

Source: https://docs.fortrabbit.com/dev/htaccess/precompress
Published: 2026-10-11
Reviewed: 2026-10-11

> Compress CSS, JS and SVG once during the build and let Apache serve the finished .br and .gz files. Smaller responses, no compression work per request.


## About precompression

fortrabbit compresses text responses on the fly with [GZIP](/dev/htaccess/gzip) at level 1. Brotli, when [enabled in `.htaccess`](/dev/htaccess/gzip#brotli), 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:

| Compression | Transferred |
| -- | --: |
| On the fly, GZIP | 45 kB |
| On the fly, Brotli | 44 kB |
| Precompressed, GZIP level 9 | 35 kB |
| Precompressed, Brotli quality 11 | 26 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:



```js [compress.mjs]
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 },
  }))
}
```

Run it after the asset build in the [build commands](/platform/deployment/build-commands):



```shell
npm ci
npm run build
node compress.mjs
```

`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:



```apache [.htaccess]
<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>
```

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:



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

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.

---

- [GZIP with .htaccess](/dev/htaccess/gzip)
- [Build commands](/platform/deployment/build-commands)
- [Page weight](/dev/performance/page-weight)
