Static Asset Precompression for Faster Delivery and Lower Server Load
Why precompress static assets
A common nginx setup for compression looks like this:
http {
gzip on;
gzip_types application/atom+xml application/javascript application/json application/vnd.api+json application/rss+xml application/vnd.ms-fontobject application/x-font-opentype application/x-font-truetype application/x-font-ttf application/x-javascript application/xhtml+xml application/xml font/eot font/opentype font/otf font/truetype image/svg+xml image/vnd.microsoft.icon image/x-icon image/x-win-bitmap text/css text/javascript text/plain text/xml;
server {
// ...
}
}
The key line here is gzip on;, which enables dynamic compression. That means the server compresses files in real time and spends CPU and memory on every relevant response.
A straightforward alternative is to compress those files during the build process and serve the compressed output directly at runtime. That removes the compression workload from live requests.
How it works
The idea is simple: generate compressed versions of static files ahead of time, usually with a high compression level, and let the server return those files directly when the client supports the matching algorithm.
Typical naming conventions are:
<table> <thead> <tr> <th>Algorithm</th> <th>File suffix</th> <th>Common name</th> </tr> </thead> <tbody> <tr> <td>gzip</td> <td>.gz</td> <td>gzip</td> </tr> <tr> <td>brotli</td> <td>.br</td> <td>brotli</td> </tr> <tr> <td>zstandard</td> <td>.zst</td> <td>zstd</td> </tr> </tbody> </table>For example, 1.js.br is the Brotli-compressed version of 1.js.
As of February 12, 2026, zstd is still not part of Baseline. In practice, it is safer to deploy precompressed gzip and brotli files for now.
Benefits and trade-offs
Precompression brings several clear advantages:
- Lower memory usage on the server.
- Lower CPU usage on the server.
- Reduced bandwidth usage.
It also has a cost:
- More disk space is needed because multiple compressed variants are stored.
- Builds take more time and consume more CPU and memory, especially at high compression levels.
Which files are worth precompressing
A practical extension list for precompression is:
- Regex form:
/\.(atom|rss|xml|xhtml|js|mjs|ts|html|json|css|eot|otf|ttf|svg|ico|bmp|dib|txt|text|log|md)$/ - You can extend it for your own project, such as
conf,ini, andcfg.
The corresponding MIME types are:
application/atom+xml application/javascript application/json application/vnd.api+json application/rss+xml application/vnd.ms-fontobject application/x-font-opentype application/x-font-truetype application/x-font-ttf application/x-javascript application/xhtml+xml application/xml font/eot font/opentype font/otf font/truetype image/svg+xml image/vnd.microsoft.icon image/x-icon image/x-win-bitmap text/css text/javascript text/plain text/xml text/html
One nginx detail is easy to miss: according to the docs, text/html does not need to be listed in gzip_types, brotli_types, or zstd_types. Whether or not it appears in those directives, text/html is still dynamically compressed.
Build-time memory considerations
High compression levels can make builds memory-hungry. In constrained environments, the process may be killed by the OS before it finishes.
If that happens, increasing available virtual memory is often the simplest fix:
- On Windows, enlarge the pagefile.
- On Linux, add swap space.
For example, on GitHub Actions ubuntu-latest—which for public repositories provides 16 GB of memory and 4 GB of swap—you can add more swap near the start of steps to reduce the risk of build failures:
- name: Add swap space
run: |
SWAP_FILE="/tmp/github-actions-swap-$$"
MEMORY_LIMIT_GB=12
# Add swap space to help with memory issues during build
sudo fallocate -l ${MEMORY_LIMIT_GB}G "$SWAP_FILE"
sudo chmod 600 "$SWAP_FILE"
sudo mkswap "$SWAP_FILE"
sudo swapon "$SWAP_FILE"
echo "Added ${MEMORY_LIMIT_GB}GB swap at $SWAP_FILE"
free -h
The right swap size depends on the scale of the project and the compression settings you choose.
Build configuration examples
Vite
Use vite-plugin-compression2 to generate precompressed assets.
Installation:
npm install vite-plugin-compression2 -D
pnpm add vite-plugin-compression2 -D
yarn add vite-plugin-compression2 -D
Then configure vite.config.ts:
// vite.config.ts
import { constants } from "node:zlib";
import { compression, defineAlgorithm } from "vite-plugin-compression2";
export default defineConfig({
// ...
plugins: [
// ...
compression({
algorithms: [
// 设置为最大压缩等级 9
defineAlgorithm("gzip", { level: 9 }),
// 设置为最大压缩等级 11
defineAlgorithm("brotliCompress", {
params: {
[constants.BROTLI_PARAM_QUALITY]: 11,
},
}),
// Zstandard 最大压缩等级为 22,但为符合 RFC 9659 规范,实际最大可用值为 19,以防止在 Chrome(v123+)和 Firefox(v126+)中报错
// 详见 https://github.com/nonzzz/vite-plugin-compression/issues/93
defineAlgorithm("zstandard", {
params: {
[constants.ZSTD_c_compressionLevel]: 19,
},
}),
],
include: [
// 可按需补充其他后缀
/\.(atom|rss|xml|xhtml|js|mjs|ts|html|json|css|eot|otf|ttf|svg|ico|bmp|dib|txt|text|log|md|conf|ini|cfg)$/,
],
}),
],
});
One caveat: this plugin does not behave well with VitePress. It can introduce extra __VP_STATIC_START__ and __VP_STATIC_END__ markers into generated precompressed .js files.
Rsbuild
Use compression-webpack-plugin.
Installation:
npm install compression-webpack-plugin -D
pnpm add compression-webpack-plugin -D
yarn add compression-webpack-plugin -D
Then configure rsbuild.config.ts:
// rsbuild.config.ts
import { constants } from "node:zlib";
import CompressionPlugin from "compression-webpack-plugin";
export default defineConfig({
// ...
tools: {
rspack: {
plugins: [
new CompressionPlugin({
algorithm: "gzip",
include:
/\.(atom|rss|xml|xhtml|js|mjs|ts|html|json|css|eot|otf|ttf|svg|ico|bmp|dib|txt|text|log|md|conf|ini|cfg)$/,
// 设置为最大压缩等级 9
compressionOptions: { level: 9 },
// minRatio 配置可参考文档:https://github.com/webpack/compression-webpack-plugin#minratio
minRatio: Number.MAX_SAFE_INTEGER,
}),
new CompressionPlugin({
algorithm: "brotliCompress",
include:
/\.(atom|rss|xml|xhtml|js|mjs|ts|html|json|css|eot|otf|ttf|svg|ico|bmp|dib|txt|text|log|md|conf|ini|cfg)$/,
// 设置为最大压缩等级 11
compressionOptions: {
params: {
[constants.BROTLI_PARAM_QUALITY]: 11,
},
},
minRatio: Number.MAX_SAFE_INTEGER,
}),
new CompressionPlugin({
// 必须,否则此插件会将文件命名为 [path][base].gz 与 gzip 冲突
filename: "[path][base].zst",
algorithm: "zstdCompress",
include:
/\.(atom|rss|xml|xhtml|js|mjs|ts|html|json|css|eot|otf|ttf|svg|ico|bmp|dib|txt|text|log|md|conf|ini|cfg)$/,
// Zstandard 最大压缩等级为 22,但为符合 RFC 9659 规范,实际最大可用值为 19,以防止在 Chrome(v123+)和 Firefox(v126+)中报错
// 详见 https://github.com/nonzzz/vite-plugin-compression/issues/93
compressionOptions: {
params: {
[constants.ZSTD_c_compressionLevel]: 19,
},
},
minRatio: Number.MAX_SAFE_INTEGER,
}),
],
},
},
});
webpack
The webpack setup also uses compression-webpack-plugin.
Installation:
npm install compression-webpack-plugin -D
pnpm add compression-webpack-plugin -D
yarn add compression-webpack-plugin -D
Then configure webpack.config.js:
// webpack.config.js
const { constants } = require("zlib");
const CompressionPlugin = require("compression-webpack-plugin");
module.exports = {
// ...
plugins: [
new CompressionPlugin({
algorithm: "gzip",
include:
/\.(atom|rss|xml|xhtml|js|mjs|ts|html|json|css|eot|otf|ttf|svg|ico|bmp|dib|txt|text|log|md|conf|ini|cfg)$/,
// 设置为最大压缩等级 9
compressionOptions: { level: 9 },
// minRatio 配置可参考文档:https://github.com/webpack/compression-webpack-plugin#minratio
minRatio: Number.MAX_SAFE_INTEGER,
}),
new CompressionPlugin({
algorithm: "brotliCompress",
include:
/\.(atom|rss|xml|xhtml|js|mjs|ts|html|json|css|eot|otf|ttf|svg|ico|bmp|dib|txt|text|log|md|conf|ini|cfg)$/,
// 设置为最大压缩等级 11
compressionOptions: {
params: {
[constants.BROTLI_PARAM_QUALITY]: 11,
},
},
minRatio: Number.MAX_SAFE_INTEGER,
}),
new CompressionPlugin({
// 必须,否则此插件会将文件命名为 [path][base].gz 与 gzip 冲突
filename: "[path][base].zst",
algorithm: "zstdCompress",
include:
/\.(atom|rss|xml|xhtml|js|mjs|ts|html|json|css|eot|otf|ttf|svg|ico|bmp|dib|txt|text|log|md|conf|ini|cfg)$/,
// Zstandard 最大压缩等级为 22,但为符合 RFC 9659 规范,实际最大可用值为 19,以防止在 Chrome(v123+)和 Firefox(v126+)中报错
// 详见 https://github.com/nonzzz/vite-plugin-compression/issues/93
compressionOptions: {
params: {
[constants.ZSTD_c_compressionLevel]: 19,
},
},
minRatio: Number.MAX_SAFE_INTEGER,
}),
],
};
Deployment examples
nginx
nginx can serve precompressed files directly and fall back to dynamic compression when the matching static variant does not exist.
http {
# nginx 会根据 Accept-Encoding 决定提供哪种格式的文件,因此不同算法配置顺序不影响结果。
# 启用 gzip_static 模块以提供预压缩的 .gz 文件
gzip_static on;
# 如果找不到静态文件则回退到动态压缩
gzip on;
gzip_types application/atom+xml application/javascript application/json application/vnd.api+json application/rss+xml application/vnd.ms-fontobject application/x-font-opentype application/x-font-truetype application/x-font-ttf application/x-javascript application/xhtml+xml application/xml font/eot font/opentype font/otf font/truetype image/svg+xml image/vnd.microsoft.icon image/x-icon image/x-win-bitmap text/css text/javascript text/plain text/xml;
# 用于在 HTTP 响应头中添加 Vary: Accept-Encoding 字段
gzip_vary on;
# 让 nginx 也动态压缩反向代理的内容
gzip_proxied expired no-cache no-store private auth;
# 启用 brotli_static 以提供预压缩的 .br 文件
# 需要 ngx_brotli 模块: https://github.com/google/ngx_brotli
# 如果你使用 1Panel 面板:
# 可前往 /websites 页面,点击设置->模块->启用 ngx_brotli->构建,即可启用。
brotli_static on;
# 如果找不到静态文件则回退到动态压缩
brotli on;
brotli_types application/atom+xml application/javascript application/json application/vnd.api+json application/rss+xml application/vnd.ms-fontobject application/x-font-opentype application/x-font-truetype application/x-font-ttf application/x-javascript application/xhtml+xml application/xml font/eot font/opentype font/otf font/truetype image/svg+xml image/vnd.microsoft.icon image/x-icon image/x-win-bitmap text/css text/javascript text/plain text/xml;
# 启用 zstd_static 以提供预压缩的 .zst 文件
# 需要 zstd-nginx-module 模块: https://github.com/tokers/zstd-nginx-module
zstd_static on;
# 如果找不到静态文件则回退到动态压缩
zstd on;
zstd_types application/atom+xml application/javascript application/json application/vnd.api+json application/rss+xml application/vnd.ms-fontobject application/x-font-opentype application/x-font-truetype application/x-font-ttf application/x-javascript application/xhtml+xml application/xml font/eot font/opentype font/otf font/truetype image/svg+xml image/vnd.microsoft.icon image/x-icon image/x-win-bitmap text/css text/javascript text/plain text/xml;
server {
# 其他配置
listen 80;
server_name example.com;
root /var/www/html;
location / {
try_files $uri $uri/ /index.html;
}
}
}
A useful detail here: nginx chooses what to serve based on Accept-Encoding, so the order of algorithm-related configuration is not what determines the response format.
Apache
On Apache, the usual pattern is to combine fallback dynamic compression with rewrite rules for precompressed files and explicit response headers.
# 启用 mod_deflate 以实现回退动态压缩
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE application/atom+xml application/javascript application/json application/vnd.api+json application/rss+xml application/vnd.ms-fontobject application/x-font-opentype application/x-font-truetype application/x-font-ttf application/x-javascript application/xhtml+xml application/xml font/eot font/opentype font/otf font/truetype image/svg+xml image/vnd.microsoft.icon image/x-icon image/x-win-bitmap text/css text/javascript text/plain text/xml
</IfModule>
# 提供预压缩文件
<IfModule mod_rewrite.c>
RewriteEngine On
# 如果存在 .zst 文件且客户端支持 zstd,则提供该文件
RewriteCond %{HTTP:Accept-Encoding} zstd
RewriteCond %{REQUEST_FILENAME}.zst -f
RewriteRule ^(.*)$ $1.zst [L]
# 如果存在 .br 文件且客户端支持 brotli,则提供该文件
RewriteCond %{HTTP:Accept-Encoding} br
RewriteCond %{REQUEST_FILENAME}.br -f
RewriteRule ^(.*)$ $1.br [L]
# 如果存在 .gz 文件且客户端支持 gzip,则提供该文件
RewriteCond %{HTTP:Accept-Encoding} gzip
RewriteCond %{REQUEST_FILENAME}.gz -f
RewriteRule ^(.*)$ $1.gz [L]
</IfModule>
# 设置正确的 content-type 和 encoding headers
<FilesMatch "\.js\.gz$">
Header set Content-Type "application/javascript"
Header set Content-Encoding "gzip"
</FilesMatch>
<FilesMatch "\.css\.gz$">
Header set Content-Type "text/css"
Header set Content-Encoding "gzip"
</FilesMatch>
<FilesMatch "\.js\.br$">
Header set Content-Type "application/javascript"
Header set Content-Encoding "br"
</FilesMatch>
<FilesMatch "\.css\.br$">
Header set Content-Type "text/css"
Header set Content-Encoding "br"
</FilesMatch>
<FilesMatch "\.js\.zst$">
Header set Content-Type "application/javascript"
Header set Content-Encoding "zstd"
</FilesMatch>
<FilesMatch "\.css\.zst$">
Header set Content-Type "text/css"
Header set Content-Encoding "zstd"
</FilesMatch>
Halo CMS
In Halo CMS v2.22.14, .br files are picked up automatically, but .zst files are not.
How to verify that it is working
The quickest way to confirm precompression is actually being used:
- Make sure the browser supports the algorithm you want to use:
gzip,brotli, orzstd. - Open browser developer tools.
- Go to the Network tab.
- Reload the page and select the file you want to inspect, such as a
.jsasset. - Check the Headers section and look for
Content-Encoding.
If the value is br, gzip, or zstd, the matching compressed variant was delivered correctly.
There is one browser behavior worth noting. Testing on Edge 114 showed that on HTTPS sites and on 127.0.0.1, Accept-Encoding was gzip, deflate, br, zstd. On plain HTTP sites, it was only gzip, deflate.
That leads to a practical conclusion: if you want Brotli or Zstandard precompressed files to be used reliably, configure HTTPS for the site first.
Notes on zstd
Although Zstandard supports a maximum compression level of 22, a level of 19 is the highest practical choice here to stay within RFC 9659 and avoid errors in Chrome (v123+) and Firefox (v126+).
Closing note
If you are already shipping static assets through a build pipeline, precompression is one of the simplest ways to move work out of request time. The runtime savings are real, but the build and storage costs should be planned for, especially when enabling high-level Brotli or Zstandard compression in CI.