From 349fa2c0e62d417c0cf974e8bfbbacf580e45fc0 Mon Sep 17 00:00:00 2001 From: Kenneth Kalmer Date: Tue, 23 Jun 2026 16:59:08 +0100 Subject: [PATCH 1/2] perf(build): skip JSON pre-compression, default zopfli to 1 iteration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two compression levers ported from Voltaire to speed up builds: - Drop JSON from the pre-compress glob. Most are Gatsby's page-data.json (high count, large total size) and zopfli-precompressing them was the bulk of onPostBuild time for ~5-10% extra ratio over nginx's live gzip. nginx is configured with `gzip on; gzip_types application/json; gzip_static on;` so JSON is live-gzipped on the way out — no loss of gzip on JSON responses. assert-compressed.sh updated to match. - Lower the default zopfli numiterations from 15 to 1. CI already overrides to 1; this speeds local builds too. The deflate ratio plateaus after the first iteration (<0.5% byte savings vs. 5/15 for 25-45% more time per file). ASSET_COMPRESSION_ITERATIONS still allows bumping it for production if max ratio is wanted. Verified locally: 541 css/js/svg files compressed in 11.6s, no *.json.gz produced, assert-compressed.sh passes. Co-Authored-By: Claude Opus 4.8 (1M context) --- bin/assert-compressed.sh | 4 ++-- data/onPostBuild/compressAssets.js | 7 ++++++- data/onPostBuild/compressAssets.ts | 7 ++++++- data/onPostBuild/compressAssetsWorker.js | 5 ++++- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/bin/assert-compressed.sh b/bin/assert-compressed.sh index 3a51589b87..ad275fe3f1 100755 --- a/bin/assert-compressed.sh +++ b/bin/assert-compressed.sh @@ -1,13 +1,13 @@ #!/bin/bash # -# A utility script to assert that all CSS, JS, JSON, and SVG files have corresponding .gz compressed versions +# A utility script to assert that all CSS, JS, and SVG files have corresponding .gz compressed versions # # Usage: assert-compressed.sh # # Find all files that should be compressed -FILES=$(find public -type f \( -name "*.css" -o -name "*.js" -o -name "*.json" -o -name "*.svg" \)) +FILES=$(find public -type f \( -name "*.css" -o -name "*.js" -o -name "*.svg" \)) ORIGINAL_COUNT=$(echo "$FILES" | wc -l) # Check each file for a corresponding .gz version diff --git a/data/onPostBuild/compressAssets.js b/data/onPostBuild/compressAssets.js index 81a51f3760..a57bc10a6e 100644 --- a/data/onPostBuild/compressAssets.js +++ b/data/onPostBuild/compressAssets.js @@ -23,7 +23,12 @@ const piscina_1 = __importDefault(require("piscina")); */ const onPostBuild = async ({ reporter }) => { const cwd = path_1.default.join(process.cwd(), 'public'); - const globResult = await (0, fast_glob_1.default)('**/*.{css,js,json,svg}', { cwd }); + // JSON (mostly Gatsby's page-data.json) is excluded: nginx is configured with + // `gzip on; gzip_types application/json; gzip_static on;` so JSON is + // live-gzipped on the way out. Pre-compressing it with zopfli was the bulk of + // onPostBuild time for ~5-10% extra ratio over nginx's live gzip-6. Following + // Voltaire, which dropped JSON pre-compression for the same reason. + const globResult = await (0, fast_glob_1.default)('**/*.{css,js,svg}', { cwd }); const files = globResult.map((file) => { return { from: path_1.default.join(cwd, file), diff --git a/data/onPostBuild/compressAssets.ts b/data/onPostBuild/compressAssets.ts index 436e37f20e..0cfbf75de6 100644 --- a/data/onPostBuild/compressAssets.ts +++ b/data/onPostBuild/compressAssets.ts @@ -20,7 +20,12 @@ import Piscina from 'piscina'; export const onPostBuild: GatsbyNode['onPostBuild'] = async ({ reporter }) => { const cwd = path.join(process.cwd(), 'public'); - const globResult = await fastGlob('**/*.{css,js,json,svg}', { cwd }); + // JSON (mostly Gatsby's page-data.json) is excluded: nginx is configured with + // `gzip on; gzip_types application/json; gzip_static on;` so JSON is + // live-gzipped on the way out. Pre-compressing it with zopfli was the bulk of + // onPostBuild time for ~5-10% extra ratio over nginx's live gzip-6. Following + // Voltaire, which dropped JSON pre-compression for the same reason. + const globResult = await fastGlob('**/*.{css,js,svg}', { cwd }); const files = globResult.map((file) => { return { diff --git a/data/onPostBuild/compressAssetsWorker.js b/data/onPostBuild/compressAssetsWorker.js index a9b23d5ab7..a71fe9a7a2 100644 --- a/data/onPostBuild/compressAssetsWorker.js +++ b/data/onPostBuild/compressAssetsWorker.js @@ -2,7 +2,10 @@ const fs = require('fs/promises'); const { gzipAsync } = require('@gfx/zopfli'); const options = { - numiterations: parseInt(process.env.ASSET_COMPRESSION_ITERATIONS || '15', 10), + // Default 1: zopfli's deflate ratio plateaus after the first iteration + // (<0.5% byte savings vs. 5/15 for 25-45% more time per file). Set + // ASSET_COMPRESSION_ITERATIONS higher for production if max ratio is wanted. + numiterations: parseInt(process.env.ASSET_COMPRESSION_ITERATIONS || '1', 10), }; const compress = async ({ from, to }) => { From be6d3733f9d0ddad75ddd8323b68370a2dd4ca14 Mon Sep 17 00:00:00 2001 From: Kenneth Kalmer Date: Tue, 23 Jun 2026 16:59:08 +0100 Subject: [PATCH 2/2] ci: run Gatsby build on Docker gen2 Only the build job runs the CPU-bound `yarn build`; other jobs are light. gen2 is ~1.4x faster CPU at +20% credits/min, so it's the one place the trade pays off (per Voltaire's gen2 rollout). Falls back to xlarge if gen2 is unavailable on the plan. Co-Authored-By: Claude Opus 4.8 (1M context) --- .circleci/config.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a58063da18..6459a6b971 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -78,7 +78,10 @@ jobs: NODE_OPTIONS: --max-old-space-size=12288 executor: name: default - resource_class: xlarge + # gen2 is ~1.4x faster CPU at +20% credits/min. Only this job runs the + # CPU-bound `yarn build`, so it's the one place the trade pays off (per + # Voltaire's gen2 rollout). Fall back to xlarge if gen2 is unavailable. + resource_class: xlarge.gen2 steps: - checkout - attach_workspace: