{"slug": "web-performance-on-a-budget-bundle-size-api-limits-and-database-indexes", "title": "Web Performance on a Budget: Bundle Size, API Limits, and Database Indexes", "summary": "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.", "body_md": "*The three budgets every web app hits: bundle size, API rate limits, and database index selectivity. Learn the formulas, thresholds, and PR checklist.*\n\nEvery 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`\n\n, or a query that times out.\n\nThe budgets are linked by the same habit that breaks them: averaging. A bundle that is 71 kB `lodash`\n\nfull on average is 7.2 kB `lodash/get`\n\nwhen 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`\n\nhas two values has selectivity 0.5 on average but 0.95 for `active`\n\nand 0.05 for `pending`\n\n— the average hides the skew.\n\nThis 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.\n\nThe smallest budget is the one the user downloads. A parsed package of 71.0 kB `lodash`\n\nfull 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`\n\n; on 9 Mbps 4G it is 22 ms. The same utility as `lodash/get`\n\nis 7.2 kB parsed, 2.9 kB gzip, 15 ms on 3G — an 8× saving from a one-line import change.\n\nThe formula is linear:\n\nwhere B is Mbps and RTT is round-trip in seconds. The budget share is `S_gzip / 244 × 100`\n\n. The [Bundle Size Impact Calculator](https://notacalculator.com/calculator/bundle-size-impact-calculator) evaluates all three plus brotli.\n\n**Code — the 8× mistake and the fix:**\n\n`import _ from 'lodash'; // 71.0 kB parsed`\n\n`import get from 'lodash/get'; // 7.2 kB parsed`\n\n`import { get } from 'lodash-es'; // tree-shaken, 7.2 kB`\n\nBundlers like webpack only tree-shake `lodash-es`\n\nand named ESM imports — the default `lodash`\n\nimport is not shaken.\n\n**Reference — popular packages (minified+gzipped):**\n\n| Package | Parsed (kB) | Gzip (kB) | % of 244 kB |\n|---|---|---|---|\n| lodash (full) | 71.0 | 24.4 | 10.0% |\n| lodash/get | 7.2 | 2.9 | 1.2% |\n| moment | 66.5 | 19.8 | 8.1% |\n| date-fns | 19.0 | 5.8 | 2.4% |\n| react-dom | 130.0 | 42.0 | 17.2% |\n\n*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).*\n\n**Tip:** set a CI budget of 244 kB gzip total, 130 kB per route, and paste the calculator's `Budget%`\n\ninto the PR description — future reviewers see the cost without rebuilding.\n\nIf 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.\n\nLet L = limit per minute, D = demand per minute, r = retry fraction, p = price per request:\n\nMonthly cost (30-day month, 43,200 minutes):\n\nAt $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.\n\n**Code — queue instead of retry:**\n\n`import pLimit from 'p-limit'; const limit = pLimit(10); // 10/min`\n\n`await Promise.all(urls.map(url > limit(() => fetch(url))));`\n\nBatch where the API allows it — GitHub GraphQL and Stripe batches turn N requests into 1.\n\nThe [API Rate Limit & Cost Calculator](https://notacalculator.com/calculator/api-rate-limit-cost-calculator) evaluates `U`\n\n, `T`\n\n, `R`\n\n, and `C_month`\n\nfor any per-second/minute/hour limit.\n\nThe database budget is the most misread. Selectivity `S = R / N`\n\n(rows returned / total rows) decides if the planner uses the index. At `S = 0.5`\n\n(50% of rows) the index is ignored; at `S = 0.001%`\n\n(1 row) it is always used. The threshold is around 5–20% with default costs.\n\nIf `C_idx < C_seq`\n\nthe planner chooses index scan. For `status = 'active'`\n\non 100k rows where 95k are active, `S = 0.95`\n\n, `C_idx = 380k`\n\nvs `C_seq = 100k`\n\n— seq scan wins. For `email = 'a@b.com'`\n\nwith one row, `S = 0.00001`\n\n, `C_idx = 4`\n\nvs `100k`\n\n— index wins 25,000×.\n\n**Code — check before indexing:**\n\n`SELECT COUNT(DISTINCT status) FROM users; -- C`\n\n`EXPLAIN SELECT * FROM users WHERE status='active'; -- rows, cost`\n\nIf `C < 100`\n\non 100k rows, a single-column index on that column will be ignored for most values — use a composite `(status, created_at)`\n\ninstead.\n\n| Distinct (C) | Rows/value | Selectivity | Planner |\n|---|---|---|---|\n| 100,000 (unique) | 1 | 0.001% | Index |\n| 1,000 | 100 | 0.10% | Index |\n| 10 | 10,000 | 10% | Borderline |\n| 2 | 50,000 | 50% | Seq Scan |\n\n*Selectivity % (log) — unique is 0.001% (always indexed), 2 distinct 50% (never alone).*\n\nThe [SQL Index Selectivity Calculator](https://notacalculator.com/calculator/sql-index-selectivity-calculator) reports `S`\n\n, efficiency `1−S`\n\n, and the verdict with cost.\n\nA single PR that adds a dependency, a new API call, and a migration can bust all three budgets at once. Check them together:\n\n| Budget | Check | Tool | Gate |\n|---|---|---|---|\n| Bundle |\n`S_gzip / 244 < 5%` per new dep |\n|\n\n`U = D/L < 80%`\n\nsustained`S = R/N < 10%`\n\nfor 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`\n\nvia Lighthouse CI, `API`\n\nvia `X-RateLimit-Remaining`\n\nheader check, `index`\n\nvia `EXPLAIN`\n\nin 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.\n\nFor the network that connects them, the [Bandwidth Calculator](https://notacalculator.com/calculator/bandwidth-calculator) converts `S_gzip`\n\nto wall-clock time on 3G/4G.\n\nA real pull request that adds a dashboard with a chart, an analytics API call, and a new filter illustrates how the budgets interact.\n\n**The PR:** Add `recharts`\n\nfor a new `RevenueChart`\n\ncomponent, fetch `/api/revenue?range=30d`\n\non page load, and add `WHERE status = 'active' AND region = 'EU'`\n\nto the revenue query.\n\n**Bundle check:** `recharts`\n\nis 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`\n\nto `React.lazy`\n\nso the initial route stays at 180 kB and the chart chunk loads on demand.\n\n**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`\n\nfor 60 seconds and batching the poll to 1 per minute via `SWR`\n\n.\n\n**Database check:** The new filter `WHERE status='active'`\n\non 500k rows where 80% are active has S = 0.80, efficiency 20% — the planner will seq scan. Adding `AND region='EU'`\n\nwhere EU is 10% of rows makes the composite `S = 0.80 × 0.10 = 0.08`\n\n(8%) — now selective enough for a composite index `(region, status)`\n\nor `(status, region)`\n\ndepending 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.\n\nThe PR ships when all three checks are green: bundle under 244 kB initial, API U < 80%, and `EXPLAIN`\n\nshows Index Scan. One budget in the red is a revert.\n\nBudgets are not one-time checks; they drift. Bundle size grows with every `npm install`\n\n, 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`\n\ntrend), `X-RateLimit-Remaining`\n\nheader sampling for API (`U`\n\nover time), and `pg_stat_user_indexes`\n\nscans for database (`S`\n\nper 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.\n\n`esbuild --metafile`\n\nvs Bundlephobia differ by minifier — pick one.`lodash/get`\n\nvs `lodash`\n\nis 8× — the same holds for `date-fns/format`\n\nvs `date-fns`\n\n.`p-limit`\n\nqueue at `L`\n\neliminates throttling without retries.`COUNT(DISTINCT col)`\n\n< 100 on 100k rows → composite, not single-column.`EXPLAIN`\n\nrows, not just the index list.`rows = S×N`\n\ntells you the selectivity the planner actually used.`budgets.json`\n\nor `lighthouse-budget.json`\n\nso 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?**\n\nA: *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.*\n\n**Q: Gzip or brotli for budgeting?**\n\nA: *Gzip for the conservative gate (all CDNs support it), brotli for the optimistic check (most modern CDNs serve brotli, ~15 percent smaller).*\n\n**Q: When should I retry a 429?**\n\nA: *Retry 20–30 percent once with exponential backoff starting from the Retry-After header. Retrying 100 percent on an overloaded bucket just re-throttles.*\n\n**Q: When does an index get ignored?**\n\nA: *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.*\n\n**Q: Can I fix low selectivity with an index?**\n\nA: *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.*\n\n**Q: How do I know my API's limit?**\n\nA: *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.*\n\n**Q: Do I need all three calculators for every PR?**\n\nA: *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.*\n\n**notAcalculator** provides free online calculators and educational guides covering finance, fitness, mathematics, and everyday calculations.", "url": "https://wpnews.pro/news/web-performance-on-a-budget-bundle-size-api-limits-and-database-indexes", "canonical_source": "https://dev.to/apeder/web-performance-on-a-budget-bundle-size-api-limits-and-database-indexes-44c6", "published_at": "2026-08-25 05:09:22+00:00", "updated_at": "2026-08-25 05:43:43.376150+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Lighthouse", "GitHub", "OpenAI", "Stripe", "lodash", "moment", "date-fns", "react-dom"], "alternates": {"html": "https://wpnews.pro/news/web-performance-on-a-budget-bundle-size-api-limits-and-database-indexes", "markdown": "https://wpnews.pro/news/web-performance-on-a-budget-bundle-size-api-limits-and-database-indexes.md", "text": "https://wpnews.pro/news/web-performance-on-a-budget-bundle-size-api-limits-and-database-indexes.txt", "jsonld": "https://wpnews.pro/news/web-performance-on-a-budget-bundle-size-api-limits-and-database-indexes.jsonld"}}