notAcalculator logo

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

The three budgets every web app hits: bundle size, API rate limits, and database index selectivity. Learn 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,DL)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