Skip to content

Perf — Bundle size #1749

Perf — Bundle size

Perf — Bundle size #1749

name: Perf — Bundle size
# Every 2 hours we build the prod bundle and measure the total gzipped size
# of the JS chunks in dist/assets. If it blows the budget, the reusable
# auto-issue workflow files/updates an issue.
#
# Budget derivation:
# At branch point (2026-04-10) the total gzipped JS size was ~2,930,697
# bytes (~2.80 MB). PERF_BUDGET_BUNDLE_GZIP_BYTES is set ~10% above that
# (3,225,000 bytes ≈ 3.08 MB) so normal feature work has headroom but a
# genuine bloat regression (new heavy dep, missed code-split) trips it.
# Re-baseline by updating the env var below after a deliberate bump.
on:
schedule:
- cron: '15 */2 * * *'
workflow_dispatch: {}
permissions:
contents: read
actions: read
issues: write
concurrency:
group: perf-bundle-size
cancel-in-progress: true
env:
PERF_SIGNAL: bundle-gzip-bytes
# ~10% headroom over the 2026-05-21 baseline of 3,975,413 bytes.
PERF_BUDGET_BUNDLE_GZIP_BYTES: "4375000"
jobs:
bundle-size:
name: JS bundle gzipped size
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: web/package-lock.json
- name: Install dependencies
working-directory: web
run: npm ci
- name: Build
working-directory: web
env:
NODE_OPTIONS: '--max-old-space-size=6144'
run: npm run build
- name: Resolve last successful SHA
id: last-success
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
LAST=$(gh run list \
--workflow="perf-bundle-size.yml" \
--branch=main \
--status=success \
--limit=1 \
--json headSha \
--jq '.[0].headSha // ""' || echo '')
echo "sha=${LAST}" >> "$GITHUB_OUTPUT"
- name: Measure and gate bundle size
id: measure
env:
LAST_SUCCESSFUL_SHA: ${{ steps.last-success.outputs.sha }}
run: |
set -euo pipefail
TOTAL=$(find web/dist/assets -name '*.js' -exec gzip -c {} + | wc -c)
# wc -c emits leading whitespace on macOS / some BSDs — strip it.
TOTAL=$(echo "$TOTAL" | tr -d '[:space:]')
BUDGET="${PERF_BUDGET_BUNDLE_GZIP_BYTES}"
echo "Total gzipped JS: ${TOTAL} bytes (budget ${BUDGET})"
RUN_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
cat > web/perf-result.json <<EOF
{
"signal": "bundle-gzip-bytes",
"displayName": "Total gzipped JS bundle size",
"value": ${TOTAL},
"budget": ${BUDGET},
"unit": "bytes",
"context": {
"runId": "${GITHUB_RUN_ID}",
"runUrl": "${RUN_URL}",
"headSha": "${GITHUB_SHA}",
"lastSuccessfulSha": "${LAST_SUCCESSFUL_SHA}"
}
}
EOF
if [ "$TOTAL" -gt "$BUDGET" ]; then
echo "::error::Bundle size ${TOTAL} exceeds budget ${BUDGET}"
exit 1
fi
- name: Upload perf result artifact
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: perf-result-bundle-gzip-bytes
path: web/perf-result.json
if-no-files-found: warn
retention-days: 14
notify:
name: Notify on regression
needs: bundle-size
if: failure()
# Job-level permissions for the reusable workflow. A reusable workflow can
# never receive MORE permissions than its caller job, so this block is the
# documented way to guarantee `issues: write` reaches
# `_perf-regression-issue.yml` even if the workflow-level default is later
# tightened (e.g. to `read-all`). Without this, the auto-issue creator
# silently no-ops with a permission denied. See #6170.
permissions:
contents: read
issues: write
actions: read
uses: ./.github/workflows/_perf-regression-issue.yml
with:
result-path: web/perf-result.json
signal: bundle-gzip-bytes