Perf — Bundle size #1749
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |