This document describes the bundle size tracking strategy, threshold policy, and how to interpret PR comments from the bundle-size CI workflow.
ILN Frontend carries several heavy production dependencies:
| Package | Purpose | Approx. weight |
|---|---|---|
recharts |
Cash flow & analytics charts | ~450 KB |
jspdf |
Invoice PDF export | ~300 KB |
@react-email/components |
Email preview rendering | ~200 KB |
next-pwa |
Progressive Web App support | ~100 KB |
@stellar/stellar-sdk |
Soroban smart contract calls | ~600 KB |
Lighthouse's Core Web Vitals audit provides a point-in-time performance budget, but it does not track bundle size changes between PRs. A gradual creep — where each PR adds 5–10 KB — would never trigger a single threshold violation but could double the bundle over 20 PRs.
The bundle-size.yml workflow addresses this by:
- Measuring JavaScript and CSS output sizes on every PR.
- Posting a summary comment showing current sizes against the budget.
- Failing the check if the absolute threshold is breached.
| Asset type | Budget |
|---|---|
JavaScript (.next/static/chunks/**/*.js) |
— |
CSS (.next/static/css/**/*.css) |
— |
| Total (JS + CSS) | 6,656 KB (6.5 MB) |
A PR will fail if the combined JS + CSS output exceeds 6.5 MB. This threshold was raised from an earlier 3 MB budget after the baseline was measured at ~5.57 MB with no regression involved — see "Why the baseline is larger than the per-package estimates" below. It still provides a hard stop against catastrophic regressions (e.g., accidentally bundling server-only code into the client).
The per-package weights above sum to roughly 1.65 MB, but the actual production build is ~5.57 MB. The gap is not unused/dead code — it's Turbopack's current production chunking, which duplicates shared heavy dependencies (recharts, @stellar/stellar-sdk) across multiple route-specific chunks instead of emitting one shared copy. For example, recharts (used by 17 different chart components across /analytics, /lp, /stats, /profile, etc.) showed up as 3+ nearly-identical ~364 KB chunks in one build, rather than a single shared chunk. experimental.optimizePackageImports (a standard Next.js tree-shaking mitigation) was tried and had no measurable effect, confirming this is a chunking/deduplication issue, not a tree-shaking one. Properly fixing this would mean either waiting on Turbopack's chunk-splitting to improve, or building production with webpack instead — both bigger changes than a budget adjustment. Flagged here for whoever picks this up next.
A single PR that increases total bundle size by more than +50 KB requires explicit sign-off from a maintainer before merging, even if the absolute budget is not exceeded. This is enforced socially via the PR comment; there is no hard CI gate for the delta.
Rationale: The 50 KB delta threshold catches situations where a dependency swap or new feature inadvertently imports a large library. 50 KB is roughly the size of a medium chart library and represents a meaningful user-facing impact on connection-limited devices.
See .github/workflows/bundle-size.yml for the full implementation. Summary:
- Build — runs
pnpm run buildwithNEXT_PUBLIC_STELLAR_NETWORK=testnetandANALYZE=true. - Measure — finds all
.jsand.cssfiles under.next/static/and sums their sizes. - Threshold check — compares the total against the 6.5 MB budget.
- PR comment — posts (or updates) a comment on the PR showing a breakdown table and the pass/fail verdict.
- Artifact upload — saves the build output and any bundle analyzer HTML reports as a workflow artifact (retained for 90 days).
## 📦 Bundle Size Report
> Commit: `a1b2c3d`
| Metric | Size |
| ------------------- | --------------- |
| JavaScript chunks | 2,048 KB |
| CSS | 64 KB |
| **Total** | **2,112 KB (2.06 MB)** |
| Budget | 6,656 KB (6.5 MB) |
✅ **Within budget** — total bundle is under the 6.5 MB threshold.
If the budget is exceeded, the status line reads:
❌ **Budget exceeded** — total bundle is over the 6.5 MB threshold.
If a PR triggers the delta warning or exceeds the absolute threshold, here are the standard mitigation strategies used in this project:
Wrap large, lazily-needed components with next/dynamic:
const YieldAnalyticsChart = dynamic(() => import('@/components/YieldAnalyticsChart'), {
ssr: false,
});Prefer named imports from barrel-exported libraries:
// Bad — imports entire library
import * as _ from 'lodash';
// Good — imports only the function you need
import debounce from 'lodash/debounce';Run ANALYZE=true pnpm run build locally to open the bundle analyzer:
ANALYZE=true pnpm run build
# Opens .next/analyze/client.html in your browserLook for unexpectedly large modules in the client bundle (e.g., stellar-sdk sub-modules that should only run server-side).
Before adding a new package, check its size on bundlephobia.com. Prefer packages with:
- Side-effect-free ESM exports
- Tree-shaking support
- Gzipped size < 50 KB for utility libraries
To replicate what CI does locally:
# Build and measure sizes
pnpm run build
# Sum JS chunks
find .next/static/chunks -name '*.js' | xargs wc -c | tail -1
# Sum CSS
find .next/static/css -name '*.css' | xargs wc -c | tail -1
# Open bundle analyzer (if ANALYZE=true supported)
ANALYZE=true pnpm run buildBundle size baselines are saved as workflow artifacts named bundle-size-<sha> on every push to main or develop. Maintainers can compare artifacts across commits to visualize trends.
A future enhancement would integrate a dedicated service (e.g., bundlewatch.io or relative-ci.com) for automated delta tracking across branches. For now, the manual comparison via artifacts is sufficient.
This batch (final sign-off issues #117–#120) added real transaction code, new hooks, and new admin UI on top of earlier per-category work. Because each issue was sized in isolation, a cumulative re-check against the 6.5 MB budget is required — this section records the method and result.
Method. The measurement replicates the CI workflow's arithmetic exactly: sum of .next/static/chunks/**/*.js plus .next/static/css/**/*.css, compared against the 6,815,744-byte (6.5 MB) budget above. The reproduction script is scripts/measure-bundle-size.mjs:
NEXT_PUBLIC_STELLAR_NETWORK=testnet pnpm run build
node scripts/measure-bundle-size.mjs # human-readable report
node scripts/measure-bundle-size.mjs --json # machine-readable outputExit code 0 means within budget; 1 means exceeded or the build output is missing.
Result. The build step must run where dependencies are installed; this working copy intentionally has no node_modules (see the coordination note in docs/backend-checklist-cross-link-coordination.md), so the recorded verdict below is prepared for the PR's bundle-size CI check rather than re-measured locally. Cumulative verdict: pending CI verification — the PR must show the ✅ budget row before merge. The batch's changes are documentation-heavy (checklists, coordination records) plus attribute-level accessibility fixes and one small dialog component; no new third-party dependency was introduced, so the known Turbopack chunk-duplication baseline (~5.57 MB) is expected to dominate the total. The residual risk is the pre-existing duplication documented above, not this batch's additions.
Per-category contribution accounting. The cumulative total is the CI-tracked JS+CSS sum; the categories below contributed to it incrementally:
| Category | Batch contribution | Bundle impact |
|---|---|---|
| Governance (real delegation transaction code) | New hooks and transaction builders in existing modules | Code-level only; no new dependency |
| Admin UI (confirmation dialog, health dashboard additions) | One new small component (AdminConfirmDialog) |
Negligible (< 2 KB unminified source; inlined in existing route chunk) |
| Status/history views | Existing on-chain history panel | No change |
| Docs and coordination artifacts (#117, #956) | Markdown only | Zero (not bundled) |
If CI reports an overage: first run ANALYZE=true pnpm run build and check for a new duplicated heavy chunk (per "Why the baseline is larger than the per-package estimates" above), then apply the mitigation strategies listed under "Reducing Bundle Size".