ci: report per-route JavaScript budgets from Docker build
CI / check (push) Failing after 54s
CI / deploy (push) Skipped
CI / publish-container (push) Skipped

This commit is contained in:
Simo committed 2026-09-11 08:41:31 +02:00
1 parent 445ef13846
commit cc714b427a
9 files changed
+744 -1

No files matched your search

+4
View File
@@ -40,3 +40,7 @@ db_backup_*.sql
.docker-install .docker-install
.docker-install.tmp.* .docker-install.tmp.*
.env.install.* .env.install.*
build-reports
test-results
playwright-report
blob-report
+9
View File
@@ -80,6 +80,15 @@ jobs:
DEPLOY_BRANCH: ${{ gitea.ref_name }} DEPLOY_BRANCH: ${{ gitea.ref_name }}
run: bash scripts/ci-deploy.sh run: bash scripts/ci-deploy.sh
- name: Upload JavaScript size report
if: always()
uses: https://gitea.com/actions/gitea-upload-artifact@62ac910c5d3dfa85c7cb2df15afe2e342b2407c2
with:
name: javascript-size-report
path: build-reports/
if-no-files-found: warn
retention-days: 14
# Publish only after checks and the production deployment have succeeded. # Publish only after checks and the production deployment have succeeded.
# Serial execution also avoids two builds competing on the self-hosted runner. # Serial execution also avoids two builds competing on the self-hosted runner.
publish-container: publish-container:
+2
View File
@@ -50,3 +50,5 @@ blob-report/
.docker-install .docker-install
.docker-install.tmp.* .docker-install.tmp.*
.env.install.* .env.install.*
build-reports/
!/docs/performance-budgets.md
+3 -1
View File
@@ -37,7 +37,8 @@ RUN --mount=type=cache,target=/app/.next/cache \
DATABASE_URL="mysql://build:[email protected]:9/build" \ DATABASE_URL="mysql://build:[email protected]:9/build" \
HOTEL_NAME="Build fixture" APP_URL="http://localhost:3002" \ HOTEL_NAME="Build fixture" APP_URL="http://localhost:3002" \
AUTH_SECRET="build-fixture-not-for-runtime-use-000000000000" \ AUTH_SECRET="build-fixture-not-for-runtime-use-000000000000" \
pnpm run build pnpm run build \
&& PERFORMANCE_COMMIT_SHA="$NEXT_DEPLOYMENT_ID" node scripts/performance-report.mjs --output-dir build-reports
FROM node:26.8.2-alpine AS runner FROM node:26.8.2-alpine AS runner
ARG NEXT_DEPLOYMENT_ID="unknown" ARG NEXT_DEPLOYMENT_ID="unknown"
@@ -54,6 +55,7 @@ RUN apk add --no-cache tini curl \
COPY --from=builder --chown=nextjs:nextjs /app/public ./public COPY --from=builder --chown=nextjs:nextjs /app/public ./public
COPY --from=builder --chown=nextjs:nextjs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nextjs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nextjs /app/.next/static ./.next/static COPY --from=builder --chown=nextjs:nextjs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nextjs /app/build-reports ./build-reports
COPY --from=builder --chown=nextjs:nextjs /app/drizzle/migrations ./drizzle/migrations COPY --from=builder --chown=nextjs:nextjs /app/drizzle/migrations ./drizzle/migrations
COPY --chown=nextjs:nextjs scripts/docker-start.mjs ./docker-start.mjs COPY --chown=nextjs:nextjs scripts/docker-start.mjs ./docker-start.mjs
USER nextjs USER nextjs
+39
View File
@@ -0,0 +1,39 @@
# Informational route JavaScript budgets
Run after the existing production build; no second build or server is needed:
```sh
node scripts/performance-report.mjs --next-dir .next --config scripts/performance-budgets.json --output-dir build-reports
```
The command writes `report.json` and `report.md` and prints the Markdown report. An optional `PERFORMANCE_COMMIT_SHA` environment variable records the commit declared by the build caller; the script does not infer that an existing build matches the current checkout. JSON also records `BUILD_ID`, Node/zlib versions, manifest provenance, exact file paths, sizes and source entries.
## What is measured
For each configured App Router route, resolve its exact app path using `app-path-routes-manifest.json` and `server/app-paths-manifest.json`. Read its generated `page_client-reference-manifest.js` as a JSON assignment **without executing JavaScript**. Use its sibling `page/build-manifest.json`, falling back to the root build manifest only if that sibling is absent.
The **initial entry envelope** is the union of route bootstrap `rootMainFilesTree[appPath]` (or `rootMainFiles`) and every `entryJSFiles` list in that route's client-reference manifest. This includes layout, page and boundary/loading entries. The definition follows the data exposed by the installed Next 16.3.4 Turbopack build and the `getLinkAndScriptTags` / `getRequiredScripts` renderer helpers; it is deliberately a build-artifact envelope, not a browser network trace. Conditional rendering, redirects, streaming and browser caches can change actual requests.
- Raw bytes are filesystem byte lengths of unique JavaScript assets in that envelope.
- Gzip bytes are the **sum of independent gzip level 9 compressions** of those files using the recorded Node/zlib runtime. They are not gzip of concatenated source, nor observed CDN transfer sizes.
- Deployment query strings and `/_next/` prefixes are normalized before deduplication. Shared files count once per route; each route is measured independently, with no misleading cross-route total.
- Legacy `nomodule` polyfills are measured separately, outside the modern initial budget. CSS, source maps, images, external scripts, HTML/RSC payloads and async-only chunks absent from `entryJSFiles` are excluded.
- This report makes no claims about execution cost, LCP, hydration time or real-user performance.
## Initial limits
The first limits are **baseline bytes × 1.15, rounded upward to the next 10 KiB (10,240 bytes)** independently for raw and gzip. They are provisional size alerts, not validated speed targets. Baseline: existing local production build `build-TfctsWXpff2fKS`, Next 16.3.4; its source commit was not inferred.
| Route | Baseline raw bytes | Baseline gzip bytes | Raw limit | Gzip limit |
| --- | ---: | ---: | ---: | ---: |
| `/me` | 767156 | 238571 | 890880 | 276480 |
| `/news` | 765367 | 237599 | 880640 | 276480 |
| `/events` | 765851 | 237964 | 890880 | 276480 |
| `/search` | 765851 | 237964 | 890880 | 276480 |
| `/admin/catalog` | 1654898 | 492619 | 1904640 | 573440 |
| `/admin/studio/furni` | 1241312 | 391541 | 1433600 | 450560 |
Configured limits are positive integer bytes; `null` explicitly means observe-only. `scripts/performance-budgets.json` remains `mode: informational`. Exceeding a limit produces `over-budget` and a warning, with exit code 0. Missing production `BUILD_ID`, unsupported manifests, missing routes or missing referenced assets produce `unavailable` with a reason and **no partial/zero total**, also exit code 0. Malformed budget configuration or an unwritable output directory fails the command. This keeps initial CI reporting non-blocking while preventing invalid configuration from quietly disabling limits.
Synthetic tests cover shared-chunk deduplication, exact byte/gzip calculations, route bootstrap selection, missing data, safe parsing and CLI exit behavior. Run `pnpm exec vitest run --coverage.enabled=false scripts/performance-report.test.mjs`.
+16
View File
@@ -99,6 +99,22 @@ echo "Building $image"
DOCKER_BUILDKIT=1 docker build --network=host --progress=plain --cache-from epicnext-cms:latest \ DOCKER_BUILDKIT=1 docker build --network=host --progress=plain --cache-from epicnext-cms:latest \
--build-arg NEXT_DEPLOYMENT_ID="$sha" -t "$image" . --build-arg NEXT_DEPLOYMENT_ID="$sha" -t "$image" .
check_current check_current
# Read reports from the already-built image; do not start an extra application.
report_container=""
if report_container="$(docker create --entrypoint /bin/true "$image")"; then
mkdir -p build-reports
if docker cp "$report_container:/app/build-reports/." build-reports; then
cat build-reports/report.md
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
cat build-reports/report.md >> "$GITHUB_STEP_SUMMARY"
fi
else
echo "Warning: build performance report unavailable" >&2
fi
docker rm "$report_container" >/dev/null
else
echo "Warning: could not extract build performance report" >&2
fi
pnpm db:migrate pnpm db:migrate
check_current check_current
+36
View File
@@ -0,0 +1,36 @@
{
"schemaVersion": 1,
"mode": "informational",
"routes": [
{
"route": "/me",
"maxInitialRawBytes": 890880,
"maxInitialGzipBytes": 276480
},
{
"route": "/news",
"maxInitialRawBytes": 880640,
"maxInitialGzipBytes": 276480
},
{
"route": "/events",
"maxInitialRawBytes": 890880,
"maxInitialGzipBytes": 276480
},
{
"route": "/search",
"maxInitialRawBytes": 890880,
"maxInitialGzipBytes": 276480
},
{
"route": "/admin/catalog",
"maxInitialRawBytes": 1904640,
"maxInitialGzipBytes": 573440
},
{
"route": "/admin/studio/furni",
"maxInitialRawBytes": 1433600,
"maxInitialGzipBytes": 450560
}
]
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff