cd /news/developer-tools/web-performance-on-a-budget-bundle-s… Β· home β€Ί topics β€Ί developer-tools β€Ί article
[ARTICLE Β· art-109743] src=dev.to β†— pub= topic=developer-tools verified=true sentiment=Β· neutral

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.

read9 min views1 publishedAug 25, 2026

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, the API Rate Limit & Cost Calculator, and the SQL Index Selectivity Calculator. For the network that connects them, the 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 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 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%

sustainedS = R/N < 10%

for indexed filterBegginer: 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.

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 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 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 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.

── more in #developer-tools 4 stories Β· sorted by recency
── more on @lighthouse 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/web-performance-on-a…] indexed:0 read:9min 2026-08-25 Β· β€”