Web Performance on a Budget: Bundle Size, API Limits, and Database Indexes A developer's guide outlines three performance budgets every web app must manage: bundle size, API rate limits, and database index selectivity. It provides formulas, thresholds, and a PR checklist, along with companion calculators for measuring impact. The guide highlights common pitfalls like averaging that hide spikes and skews, and offers concrete examples such as reducing lodash bundle size by 8x through proper imports. The three budgets every web app hits: bundle size, API rate limits, and database index selectivity. Learn the formulas, thresholds, and PR checklist. 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. 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. 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. 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 — 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. Every number is reproducible with the three calculators that accompany it — the Bundle Size Impact Calculator https://notacalculator.com/calculator/bundle-size-impact-calculator , the API Rate Limit & Cost Calculator https://notacalculator.com/calculator/api-rate-limit-cost-calculator , and the SQL Index Selectivity Calculator https://notacalculator.com/calculator/sql-index-selectivity-calculator . For the network that connects them, the Bandwidth Calculator https://notacalculator.com/calculator/bandwidth-calculator sizes the pipe. 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. On a 1.6 Mbps 3G link that is 122 ms of transfer before first-paint ; on 9 Mbps 4G it is 22 ms. 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: where B is Mbps and RTT is round-trip in seconds. The budget share is S gzip / 244 × 100 . The Bundle Size Impact Calculator https://notacalculator.com/calculator/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 — the default lodash import is not shaken. Reference — popular packages minified+gzipped : | Package | Parsed kB | Gzip kB | % of 244 kB | |---|---|---|---| | lodash full | 71.0 | 24.4 | 10.0% | | lodash/get | 7.2 | 2.9 | 1.2% | | moment | 66.5 | 19.8 | 8.1% | | date-fns | 19.0 | 5.8 | 2.4% | | react-dom | 130.0 | 42.0 | 17.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. 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; OpenAI's Tier 1 is 500 per minute; Stripe's writes are 100 per second. All are token buckets: 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: Monthly cost 30-day month, 43,200 minutes : 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. 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. The API Rate Limit & Cost Calculator https://notacalculator.com/calculator/api-rate-limit-cost-calculator evaluates U , T , R , and C month for any per-second/minute/hour limit. The database budget is the most misread. Selectivity S = R / N rows returned / total rows decides if the planner uses the index. 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. 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. | Distinct C | Rows/value | Selectivity | Planner | |---|---|---|---| | 100,000 unique | 1 | 0.001% | Index | | 1,000 | 100 | 0.10% | Index | | 10 | 10,000 | 10% | Borderline | | 2 | 50,000 | 50% | Seq Scan | Selectivity % log — unique is 0.001% always indexed , 2 distinct 50% never alone . The SQL Index Selectivity Calculator https://notacalculator.com/calculator/sql-index-selectivity-calculator reports S , efficiency 1−S , and the verdict with cost. A single PR that adds a dependency, a new API call, and a migration can bust all three budgets at once. Check them together: | Budget | Check | Tool | Gate | |---|---|---|---| | Bundle | S gzip / 244 < 5% per new dep | | U = D/L < 80% sustained S = R/N < 10% for indexed filter Begginer: 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 https://notacalculator.com/calculator/bandwidth-calculator converts S gzip to wall-clock time on 3G/4G. 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 — 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 https://notacalculator.com/calculator/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. 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, so U = 200%, T = 500 throttled/min. With 30% retry, R = 150, Deff = 1,150. The API Rate Limit & Cost Calculator https://notacalculator.com/calculator/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. 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 https://notacalculator.com/calculator/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. 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 . 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. esbuild --metafile vs Bundlephobia differ by minifier — pick one. lodash/get vs lodash is 8× — the same holds for date-fns/format vs date-fns . p-limit queue at L eliminates throttling without retries. COUNT DISTINCT col < 100 on 100k rows → composite, not single-column. EXPLAIN rows, not just the index list. rows = S×N tells you the selectivity the planner actually used. budgets.json or lighthouse-budget.json so CI and humans share the same numbers — drift happens when the budget lives only in a comment. Q: What is the 244 kB bundle budget? A: 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. Q: Gzip or brotli for budgeting? A: Gzip for the conservative gate all CDNs support it , brotli for the optimistic check most modern CDNs serve brotli, ~15 percent smaller . Q: When should I retry a 429? A: Retry 20–30 percent once with exponential backoff starting from the Retry-After header. Retrying 100 percent on an overloaded bucket just re-throttles. Q: When does an index get ignored? A: 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. Q: Can I fix low selectivity with an index? A: 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. Q: How do I know my API's limit? A: 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. Q: Do I need all three calculators for every PR? A: 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. notAcalculator provides free online calculators and educational guides covering finance, fitness, mathematics, and everyday calculations.