notAcalculator logo

Web Performance on a Budget: Bundle Size, API Limits, and Database Indexes

Learn the three budgets every web app hits: bundle size, API rate limits, and database index selectivity — with the formulas, thresholds, and PR checklist.

Introduction

Every web app has three budgets, and only one of them lives in your code. The bundle budget lives in the browser — 244 kB gzip is where Lighthouse starts penalizing Time to Interactive[chrome-lighthouse]. The API budget lives at the provider — 5,000 requests per hour on GitHub, 500 per minute on OpenAI Tier 1, 100 writes per second on Stripe[github-rate-limits]. The database budget lives in the planner — an index with 0.5 selectivity will be ignored and the query will seq scan 50,000 rows[use-the-index-luke]. Exceed any of the three and the user pays: slower paint, 429 Too Many Requests, or a query that times out.

The budgets are linked by the same habit that breaks them: averaging. A bundle that is 71 kB lodash full on average is 7.2 kB lodash/get when imported correctly[bundlephobia] — the average hides the choice. An API that is 20 requests per minute on average is 800 per minute during a burst deploy — the average hides the spike. A table where status has two values has selectivity 0.5 on average but 0.95 for active and 0.05 for pending — the average hides the skew.

This guide makes the three budgets explicit, with the formulas, the thresholds, and a single PR checklist that covers frontend, backend, and database. It is code-heavy on purpose: every snippet is copyable via copy and every number is reproducible with the three calculators that accompany it — the Bundle Size Impact Calculator, the API Rate Limit & Cost Calculator, and the SQL Index Selectivity Calculator. For the network that connects them, the Bandwidth Calculator sizes the pipe.

1. Bundle Size — The Frontend Budget

The smallest budget is the one the user downloads. A parsed package of 71.0 kB lodash full is 24.4 kB gzip and 20.1 kB brotli[bundlephobia]. On a 1.6 Mbps 3G link that is 122 ms of transfer before first-paint; on 9 Mbps 4G it is 22 ms[grigorik-hpn]. The same utility as lodash/get is 7.2 kB parsed, 2.9 kB gzip, 15 ms on 3G — an 8× saving from a one-line import change.

The formula is linear:

Sgzip=Sparsed×rgzipS_{\text{gzip}} = S_{\text{parsed}} \times r_{\text{gzip}}
T=Sgzip×8B×1000+RTTT = \frac{S_{\text{gzip}} \times 8}{B \times 1000} + \text{RTT}

where B is Mbps and RTT is round-trip in seconds. The budget share is S_gzip / 244 × 100[chrome-lighthouse]. The Bundle Size Impact Calculator evaluates all three plus brotli.

Code — the 8× mistake and the fix:

  • import _ from 'lodash'; // 71.0 kB parsed
  • import get from 'lodash/get'; // 7.2 kB parsed
  • import { get } from 'lodash-es'; // tree-shaken, 7.2 kB

Bundlers like webpack only tree-shake lodash-es and named ESM imports[webpack-code-splitting] — the default lodash import is not shaken.

Reference — popular packages (minified+gzipped)[bundlephobia]:

lodash (full)71.024.410.0%
lodash/get7.22.91.2%
moment66.519.88.1%
date-fns19.05.82.4%
react-dom130.042.017.2%
Gzip sizes — single import choice moves 21.5 kB (107 ms on 3G). Replace moment with date-fns and save another 14 kB (70 ms).

Tip: set a CI budget of 244 kB gzip total, 130 kB per route, and paste the calculator's Budget% into the PR description — future reviewers see the cost without rebuilding.

2. API Rate Limits — The Backend Budget

If the bundle budget is about bytes, the API budget is about tokens per minute. GitHub's 5,000 per hour is 83.3 per minute[github-rate-limits]; OpenAI's Tier 1 is 500 per minute; Stripe's writes are 100 per second. All are token buckets[use-the-index-luke]: refill at a fixed rate up to a burst capacity. You can burst to capacity instantly, but sustained throughput cannot exceed refill.

Let L = limit per minute, D = demand per minute, r = retry fraction, p = price per request:

U=D/LU = D / L
T=max⁡(0,D−L)T = \max(0, D - L)
R=T×rR = T \times r

Monthly cost (30-day month, 43,200 minutes):

Cmonth=(D+R)×43200×pC_{\text{month}} = (D + R) \times 43200 \times p

At $2 per 1K ($0.002 per request), 500 per minute limit with 800 per minute burst and 30% retry gives U = 160%, T = 300 throttled/min, R = 90 extra/min, and Deff = 890/min during the burst — $3.56 for two minutes, or $76k per month if sustained. The fix is not more retries but a queue that smooths the burst[use-the-index-luke].

Code — queue instead of retry:

  • import pLimit from 'p-limit'; const limit = pLimit(10); // 10/min
  • await Promise.all(urls.map(url => limit(() => fetch(url))));

Batch where the API allows it — GitHub GraphQL and Stripe batches turn N requests into 1[github-rate-limits].

The API Rate Limit & Cost Calculator evaluates U, T, R, and C_month for any per-second/minute/hour limit.

3. SQL Index Selectivity — The Database Budget

The database budget is the most misread. Selectivity S = R / N (rows returned / total rows) decides if the planner uses the index[postgres-indexes]. At S = 0.5 (50% of rows) the index is ignored; at S = 0.001% (1 row) it is always used. The threshold is around 5–20% with default costs[use-the-index-luke].

S=R/NS = R / N
Cseq=N×1.0,Cidx=R×4.0C_{\text{seq}} = N \times 1.0,\quad C_{\text{idx}} = R \times 4.0

If C_idx < C_seq the planner chooses index scan. For status = 'active' on 100k rows where 95k are active, S = 0.95, C_idx = 380k vs C_seq = 100k — seq scan wins. For email = 'a@b.com' with one row, S = 0.00001, C_idx = 4 vs 100k — index wins 25,000×.

Code — check before indexing:

  • SELECT COUNT(DISTINCT status) FROM users; -- C
  • EXPLAIN SELECT * FROM users WHERE status='active'; -- rows, cost

If C < 100 on 100k rows, a single-column index on that column will be ignored for most values — use a composite (status, created_at) instead[use-the-index-luke].

100,000 (unique)10.001%Index
1,0001000.10%Index
1010,00010%Borderline
250,00050%Seq Scan
Selectivity % (log) — unique is 0.001% (always indexed), 2 distinct 50% (never alone).

The SQL Index Selectivity Calculator reports S, efficiency 1−S, and the verdict with cost.

4. Putting It Together — The PR Checklist

A single PR that adds a dependency, a new API call, and a migration can bust all three budgets at once. Check them together:

BundleS_gzip / 244 < 5% per new depBundle Size ImpactCI 244 kB
APIU = D/L < 80% sustainedAPI Rate LimitAlert at 80%
DatabaseS = R/N < 10% for indexed filterSQL Index SelectivityEXPLAIN rows

Novato: paste the three calculator outputs into the PR description — reviewers see the numbers without pulling the branch. Senior: add the three gates to CI — bundle via Lighthouse CI, API via X-RateLimit-Remaining header check, index via EXPLAIN in migration tests. Sensei: make the checklist a required GitHub PR template so every service that ships JS, calls an API, and migrates a table is measured the same way.

For the network that connects them, the Bandwidth Calculator converts S_gzip to wall-clock time on 3G/4G.

5. Case Study — One PR, Three Budgets

A real pull request that adds a dashboard with a chart, an analytics API call, and a new filter illustrates how the budgets interact.

The PR: Add recharts for a new RevenueChart component, fetch /api/revenue?range=30d on page load, and add WHERE status = 'active' AND region = 'EU' to the revenue query.

Bundle check: recharts is 130 kB parsed, 42 kB gzip[bundlephobia] — 17.2% of the 244 kB budget. The existing bundle is 180 kB gzip, so the new total is 222 kB (91%). The Bundle Size Impact Calculator shows 42 kB + 3G 210 ms. The fix is code-splitting: move RevenueChart to React.lazy so the initial route stays at 180 kB and the chart chunk loads on demand[webpack-code-splitting].

API check: The dashboard polls every 30 seconds, so 2 requests per minute per user. With 500 concurrent users, D = 1,000 per minute. The API limit is 500 per minute[github-rate-limits], so U = 200%, T = 500 throttled/min. With 30% retry, R = 150, Deff = 1,150. The API Rate Limit & Cost Calculator shows the cost and the need for a queue. The fix is caching the GET for 60 seconds and batching the poll to 1 per minute via SWR.

Database check: The new filter WHERE status='active' on 500k rows where 80% are active has S = 0.80, efficiency 20% — the planner will seq scan[use-the-index-luke]. Adding AND region='EU' where EU is 10% of rows makes the composite S = 0.80 × 0.10 = 0.08 (8%) — now selective enough for a composite index (region, status) or (status, region) depending on which column is more selective. The SQL Index Selectivity Calculator confirms S = 8% → Index Scan, cost 32k vs seq 500k.

The PR ships when all three checks are green: bundle under 244 kB initial, API U < 80%, and EXPLAIN shows Index Scan. One budget in the red is a revert.

6. Monitoring After Ship

Budgets are not one-time checks; they drift. Bundle size grows with every npm install, API load grows with every new replica, and selectivity shifts as the table fills. Track the three on the same dashboard: Lighthouse CI for bundle (S_gzip trend), X-RateLimit-Remaining header sampling for API (U over time), and pg_stat_user_indexes scans for database (S per index)[postgres-indexes]. When any of the three crosses 80% of its budget, the next feature PR should include a budget-reducing change — a lazy chunk, a cache, or a composite index — before new functionality.

7. Practical Tips

  1. Measure with the same tool you ship with. esbuild --metafile vs Bundlephobia differ by minifier — pick one[webpack-code-splitting].
  2. Import the file, not the barrel. lodash/get vs lodash is 8× — the same holds for date-fns/format vs date-fns[bundlephobia].
  3. Queue bursts, don't retry bursts. A p-limit queue at L eliminates throttling without retries[use-the-index-luke].
  4. Check cardinality before indexing. COUNT(DISTINCT col) < 100 on 100k rows → composite, not single-column[postgres-indexes].
  5. Read EXPLAIN rows, not just the index list. rows = S×N tells you the selectivity the planner actually used[postgres-indexes].
  6. Version your budgets today. Keep the three thresholds (244 kB, 80% API, 10% selectivity) in a budgets.json or lighthouse-budget.json so CI and humans share the same numbers — drift happens when the budget lives only in a comment.

Frequently Asked Questions

❓ What is the 244 kB bundle budget?
✅ A common Lighthouse threshold where total gzip over ~244 kB starts penalizing Time to Interactive. Use it as a reference; your product's budget may be 170 or 500 kB.
❓ Gzip or brotli for budgeting?
✅ Gzip for the conservative gate (all CDNs support it), brotli for the optimistic check (most modern CDNs serve brotli, ~15 percent smaller).
❓ When should I retry a 429?
✅ Retry 20–30 percent once with exponential backoff starting from the Retry-After header. Retrying 100 percent on an overloaded bucket just re-throttles.
❓ When does an index get ignored?
✅ When selectivity is high — roughly above 10 percent with default costs. On 100k rows, keeping 10k rows (10 percent) is borderline; keeping 50k (50 percent) is always a seq scan.
❓ Can I fix low selectivity with an index?
✅ Alone, no — a single-column index on a 2-value column is 50 percent selective. As part of a composite (status, created_at) that raises cardinality, yes.
❓ How do I know my API's limit?
✅ Check the docs (GitHub 5,000/hour, OpenAI 500/minute, Stripe 100 writes/second) and the response headers X-RateLimit-Limit/Remaining/Reset. The calculator handles per second/minute/hour.
❓ Do I need all three calculators for every PR?
✅ No — check the budget you touch. New dep → bundle, new API call → rate limit, new WHERE → selectivity. If a PR touches all three, check all three.
❓ Where do I learn more about each budget?
✅ Bundle: webpack code-splitting guide and Bundlephobia; API: GitHub/OpenAI rate-limit docs and token-bucket article; Database: Use The Index Luke and PostgreSQL index docs.

References

  1. [1]Bundlephobia. Find the cost of adding a npm package to your bundle.
  2. [2]webpack. Code Splitting Guide.
  3. [3]Chrome for Developers. Lighthouse Performance Scoring.
  4. [4]GitHub Docs. Rate limits for the REST API.
  5. [5]Winand, Markus. Use The Index, Luke! A Guide to Database Performance.
  6. [6]PostgreSQL Documentation. Indexes.
  7. [7]Grigorik, Ilya. High Performance Browser Networking. O'Reilly Media.Buy on Amazon
Give us your feedback! Was this useful?
1b

UnByte — Independent Software Engineering

All reference data cites its sources — Editorial policy