# Drupal Brotli compression on production sites in 2026

Turn on Drupal 11.4 aggregate compression, install ext-brotli, and wire nginx or Apache so .br files beat gzip for CSS and JS on museum and catalogue stacks.

- Date: 2026-11-15
- Category: Drupal

## Table of contents

- Why Drupal Brotli compression matters after 11.4.0
- How Drupal Brotli compression works in core
- Drupal Brotli compression production checklist
- What breaks when Drupal Brotli compression is misconfigured
- How to measure Drupal Brotli compression on live catalogues
- Related Drupal catalogue work on alanvo.com
- FAQ
  - Does Drupal Brotli compression replace CDN gzip?
  - Where do I enable Drupal Brotli compression in the admin UI?
  - Which PHP and nginx pieces does Drupal Brotli compression need?
  - Should I upgrade to Drupal 11.4 only for Brotli on aggregates?

Drupal Brotli compression in core 11.4 is the first time I can shrink aggregated CSS and JavaScript on a museum or regulator site without bolting on AdvAgg for static encodings alone. I am Alan Vo, a Gold Coast web developer with 18 years shipping Drupal catalogues, and I treat Drupal Brotli compression as an infrastructure plus core upgrade task, not a checkbox in the Performance UI. When aggregation is already on, 11.4 can write `.br` siblings beside `.gz` files under `sites/default/files`, and modern browsers pick the smaller payload automatically if the web server is configured to serve it. This note is what I run after a Drupal 11.4.x deploy on stacks that already aggregate assets in production.

Readers are lead developers and sysadmins on Drupal 11.3.x or 11.4.x who host on nginx or Apache behind a CDN. It pairs with [Drupal HTMX on production catalogues](https://alanvo.com/blog/drupal-htmx-production/) because both wins land in the same 11.4 maintenance window, and with [Drupal JSON:API on live catalogues](https://alanvo.com/blog/drupal-jsonapi-catalogues/) because JSON responses are unaffected while theme asset weight still dominates first paint on hybrid pages. Facts below come from the [Brotli change record](https://www.drupal.org/node/3526344), the [Drupal 11.4.0 announcement](https://www.drupal.org/blog/drupal-11-4-0), and the [aggregation guide](https://www.drupal.org/docs/administering-a-drupal-site/managing-site-performance-and-scalability/aggregate-css-and-js-files-in-drupal-core).

## Why Drupal Brotli compression matters after 11.4.0

Drupal Brotli compression matters after 11.4.0 because core now pre-compresses aggregated theme and module assets at dump time instead of leaving every byte to the origin gzip module alone. The change record cites typical 15-25% smaller files than gzip for the same CSS and JS aggregates, which is consistent with what Brotli achieves on text assets elsewhere on the web. That is not a Largest Contentful Paint guarantee on its own, but on catalogue sites where a single page still ships hundreds of kilobytes of aggregated admin-adjacent and public libraries, shaving a fifth off static weight reduces transfer time on mobile networks editors and visitors actually use.

Drupal unifies the old `system.performance.css.gzip` and `system.performance.js.gzip` flags into `css.compress` and `js.compress`. When compression is enabled and the PHP `ext-brotli` extension is present, `AssetDumper` writes both `.gz` and `.br` variants. Apache sites pick up updated `.htaccess` rules that prefer Brotli when the client advertises support. Nginx does not get that for free: you must add `gzip_static` and `brotli_static` with a `try_files` chain on the `sites/.../files/css` and `js` paths, as documented in the change record. I plan nginx edits in the same change request as the core bump so QA does not falsely conclude Brotli is broken.

![Drupal Brotli compression documentation open beside a laptop](../../images/blog/drupal-brotli-asset-compression-1.jpg)

The feature only applies to aggregated files Drupal writes to the public files directory. It does not replace dynamic `Content-Encoding` on HTML, and it does not compress arbitrary uploaded media. Decoupled front ends that skip Drupal aggregates still need their own pipeline. For the majority of my Drupal catalogue themes, aggregates remain the bulk of JavaScript and CSS weight, so this is worth the ops attention.

## How Drupal Brotli compression works in core

Drupal Brotli compression works at aggregate generation time through the asset dumper, not at request time inside PHP-FPM. When `css.preprocess` and `js.preprocess` are true and `css.compress` / `js.compress` are true, Drupal hashes library definitions into stable filenames, writes the raw `.css` or `.js`, then emits compressed derivatives if extensions exist. Gzip continues for older clients; Brotli is preferred where both are offered and the server configuration maps `Accept-Encoding` to the `.br` file.

The [11.4.0 release post](https://www.drupal.org/blog/drupal-11-4-0) states the dependency explicitly: without `ext-brotli` on PHP, behavior reverts to gzip-only generation even after upgrade. I verify with `php -m | grep -i brotli` on each app container image before enabling compression in production config exports.

Settings migration is automatic via `system_post_update_migrate_compress_setting()`. Sites that had gzip enabled keep compression enabled under the new keys. Sites that disabled gzip stay off until someone turns compress on deliberately. Module authors who set `css.gzip` in install hooks must update to `css.compress`, as noted in the change record, or their overrides silently miss Brotli.

On Apache, the committed `.htaccess` fragment negotiates Brotli ahead of gzip for aggregate paths. On nginx, the change record example uses separate location blocks for css and js under `sites/*/files/`, with `try_files $uri.br $uri.gz $uri =404`. The `ngx_brotli` module supplies `brotli_static`; it is not bundled in every distribution package, so I confirm the module is loaded in staging before promoting config.

CDN behavior is the third leg. Some providers recompress origin responses and ignore precompressed static files unless you disable that feature for aggregate paths or origin-pull the `.br` object directly. I test one aggregate URL with `curl -H 'Accept-Encoding: br' -I` through the public hostname, not only loopback, because a misconfigured edge strips the benefit while logs still show 200 responses.

## Drupal Brotli compression production checklist

A Drupal Brotli compression production checklist starts with core version and PHP extensions, because everything else is wasted if aggregates never gain a `.br` file on disk.

![Team workshop around a table planning a CMS rebuild](../../images/blog/drupal-brotli-asset-compression-2.jpg)

1. **Upgrade path.** Target Drupal 11.4.0 or newer on supported 11.4.x security releases. Run database updates so the compress setting migration executes once.
2. **Aggregation on.** Confirm `css.preprocess` and `js.preprocess` are true in `system.performance`. The Performance administration screen labels these as aggregate options; compression without aggregation has nothing to compress.
3. **Compression flags.** Ensure `css.compress` and `js.compress` are true. If config is environment-split, set them in `settings.php` on production only, mirroring patterns from the [caching documentation](https://www.drupal.org/docs/develop/development-tools/disabling-and-debugging-caching) for preprocess overrides.
4. **PHP Brotli.** Install and enable `ext-brotli` on every PHP worker that runs web requests and drush cache rebuilds that regenerate aggregates.
5. **Rebuild aggregates.** After deploy, run a full cache rebuild so `AssetDumper` rewrites css and js under `sites/default/files`. I delete stale aggregates only when checksum mismatches appear, not on every deploy.
6. **Web server static mapping.** Apply nginx `brotli_static` rules from the change record or rely on Apache `.htaccess` updates shipped with core. Reload the web tier and spot-check file existence on disk (`*.css.br`).
7. **CDN and cache headers.** Align long-cache headers on aggregate paths with immutable fingerprints already in filenames. Avoid purging entire zones when only aggregates changed.
8. **Staging parity.** Match production PHP modules and nginx modules on staging so developers do not assume Brotli works based on local Docker images that lack `ext-brotli`.
9. **Rollback plan.** Document how to set `css.compress` and `js.compress` false and rebuild if a rare library breaks under aggregation, independent of Brotli.
10. **Monitor transfer size.** Capture median aggregate byte size gzip versus Brotli on three representative catalogue templates before and after.

## What breaks when Drupal Brotli compression is misconfigured

What breaks when Drupal Brotli compression is misconfigured is usually silent fallback to larger gzip files or 404s on aggregate URLs, not a PHP stack trace. Missing `ext-brotli` means no `.br` files appear; the site still works, but you do not get the marketed win and may wonder why Lighthouse transfer size barely moved.

Nginx without `brotli_static` serves the uncompressed `.css` even when `.br` exists on disk, which inflates responses for every browser. The fix is server config, not another drush cron run. Conversely, enabling `brotli_static` without installing `ngx_brotli` prevents nginx from starting, which is why I validate config with `nginx -t` in CI for infrastructure repos.

Wrong `try_files` ordering or overly broad location regexes can send HTML error pages with `text/css` types, breaking styles site-wide. I keep css and js locations scoped to `sites/.*/files/(css|js)/` as in the change record rather than reusing a single catch-all static block that also catches private stream wrappers.

![Library stacks used as a stand-in for structured content](../../images/blog/drupal-brotli-asset-compression-3.jpg)

Developers who still set deprecated `css.gzip` in custom install code may leave compression off after upgrade if their hook runs after the migrator and resets config. Grep the codebase for `gzip` keys before deploy.

Asset aggregation itself can surface JavaScript redeclaration bugs when a contrib library assumes it loads alone. Brotli does not cause that, but enabling compression often coincides with turning aggregation on in lower environments. The [aggregation guide](https://www.drupal.org/docs/administering-a-drupal-site/managing-site-performance-and-scalability/aggregate-css-and-js-files-in-drupal-core) documents opting a library out with `preprocess: false` in libraries.yml when needed.

Multi-site installs must confirm each site’s files directory receives regenerated aggregates. A shared container image with Brotli but a read-only files mount on one vhost produces inconsistent behavior across hostnames on the same codebase.

## How to measure Drupal Brotli compression on live catalogues

How to measure Drupal Brotli compression on live catalogues combines on-disk checks, response headers, and field metrics tied to template types. I pick one hashed css URL and one js URL from view-source on a heavy exhibition listing or property search page, then request them twice: with `Accept-Encoding: gzip` and with `Accept-Encoding: br`. Content-Length should drop on Brotli for the same logical file. If lengths match, the edge or origin is not serving `.br`.

WebPageTest or Lighthouse transfer size on mobile throttling gives a page-level read after Brotli is live. I compare the same URL before and after on staging with identical content, because production editor activity shifts payloads. Drupal.org quotes 15-25% better compression ratios; I log actual percentages per theme on client reports without treating vendor figures as my portfolio analytics.

Server-side, watch PHP worker CPU during the first cache rebuild after enablement. Compression at dump time spikes CPU briefly; it should not repeat on every request. If CPU stays high, something may be regenerating aggregates continuously, which points to cache misconfiguration rather than Brotli specifically.

For collections with many languages, aggregate filenames still dedupe by library definition, but different enabled modules per language domain can change counts. Measure per domain when Drupal multisite or domain modules split features.

Editor experience rarely changes when only static aggregates compress. I still run smoke tests on admin themes after deploy because Gin and Claro both ship sizable CSS stacks that flow through the same dumper.

## Related Drupal catalogue work on alanvo.com

Related Drupal catalogue work on this site includes [Royal Ontario Museum](https://alanvo.com/work/royal-ontario-museum/), where collection discovery pages mix rich media with searchable indexes, and [NDIS Commission](https://alanvo.com/work/ndis-commission/), where accessibility and predictable performance matter as much as marketing polish. [Immobel property platform](https://alanvo.com/work/immobel/) is another stack where aggregated front-end assets and CDN caching decisions stack together. Those projects informed how I sequence core upgrades, PHP extension installs, and nginx edits so Drupal Brotli compression actually shows up in transfer metrics instead of only in release notes.

![Government-style office desks and screens](../../images/blog/drupal-brotli-asset-compression-4.jpg)

## FAQ

### Does Drupal Brotli compression replace CDN gzip?

Drupal Brotli compression complements CDN gzip rather than replacing it. Core writes precompressed `.br` and `.gz` files on the origin for aggregated CSS and JS. The CDN or browser still negotiates encoding via `Accept-Encoding`. You want the edge to serve the `.br` object when supported so the origin does not compress on the fly repeatedly. HTML and uncached responses may still use dynamic Brotli or gzip at the CDN layer separately from this Drupal feature.

### Where do I enable Drupal Brotli compression in the admin UI?

Drupal Brotli compression does not have its own checkbox on the Performance form. You enable CSS and JavaScript aggregation under Configuration then Development then Performance, then ensure `css.compress` and `js.compress` are true in configuration, often via config export or `settings.php` because the gzip toggles moved out of the UI into unified compress flags in 11.4. Without aggregation preprocess enabled, compress has no aggregate files to process.

### Which PHP and nginx pieces does Drupal Brotli compression need?

Drupal Brotli compression needs the PHP `ext-brotli` extension on application servers so `AssetDumper` can write `.br` files. Apache sites use updated core `.htaccess` rules. Nginx sites need the `ngx_brotli` module and `brotli_static on` in location blocks for `sites/.../files/css` and `js`, with `try_files` preferring `$uri.br` before `$uri.gz`, as documented on the [change record](https://www.drupal.org/node/3526344). Missing either PHP or nginx pieces leaves gzip fallback or uncompressed static files.

### Should I upgrade to Drupal 11.4 only for Brotli on aggregates?

You should upgrade to Drupal 11.4 when your security and support window allows, not in isolation for Brotli alone. The same release line ships entity loading and query improvements that matter on JSON:API heavy catalogues, which often matters more than static bytes. If you are already planning 11.4 for those reasons, schedule Brotli verification in the same deploy window so PHP extensions and nginx edits are tested once.


![Editor typing in a CMS during Drupal Brotli compression work](../../images/blog/drupal-brotli-asset-compression-5.jpg)


HTML version: https://alanvo.com/blog/drupal-brotli-asset-compression/
