PERFORMANCE IS ONE PIPELINE
Users experience performance as one thing.
Your system is a pipeline:
Senior fullstack performance means you manage budgets across that pipeline.
Pipeline with budget allocations (example targets):
┌─────────────────────────────────────────────────────────────────────────────────┐
│ END-TO-END PERFORMANCE PIPELINE │
│ Target: TTFB p95 ≤ 400ms | LCP p75 ≤ 2.5s | INP p75 ≤ 200ms │
├─────────────────────────────────────────────────────────────────────────────────┤
│ │
│ [User] │
│ │ │
│ ▼ DNS (20ms) ──► TLS (30ms) ──► Request (10ms) │
│ │ │
│ ▼ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ CDN │────►│ Server │────►│ DB │ │
│ │ (cache?) │ │ (compute) │ │ (queries) │ │
│ │ ~0-50ms │ │ ~50-150ms │ │ ~20-80ms │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │ │
│ │ └────────────────────┴────────────────────┘ │
│ │ TTFB budget │
│ ▼ │
│ [Response] ──► Parse ──► Render ──► Hydrate ──► [Interact] │
│ ~20ms ~100ms ~80ms INP budget │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
When one stage exceeds its budget, downstream stages get less. Budget allocation is zero-sum.
LATENCY BUDGETING (THE MODEL)
Pick targets:
-
TTFB p95
-
LCP p75
-
INP p75
Then allocate budgets:
-
server compute
-
DB/query time
-
cache hit rate targets
-
client render/hydration
Concrete budget table example — Product page:
| Stage | Budget | Notes |
|---|---|---|
| DNS | 20ms | Use preconnect, keep connections warm |
| TLS | 30ms | Session resumption, HTTP/2 |
| Network RTT | 40ms | Assume ~20ms each way for typical user |
| CDN / edge | 0–50ms | 0 if cache hit, 50ms if miss + origin fetch |
| Server compute | 100ms | Auth, routing, template, serialization |
| DB / queries | 50ms | Sum of all queries for the request |
| TTFB total | 200ms | p95 target for product page |
| Parse + first paint | 80ms | HTML + critical CSS |
| LCP element render | 120ms | Hero image + above-fold content |
| LCP total | 400ms | From navigation start |
| Hydration | 80ms | Interactive components ready |
| INP | 150ms | Click/tap to response |
Senior rule:
If you don't budget, you will overspend unpredictably.
Break down your own critical path. If TTFB is 600ms and DB is 400ms, you know where to work.
WHAT TO MEASURE FIRST (ANTI-PLACEBO)
Start with:
-
end-to-end traces for slow requests
-
DB query timings + frequency
-
cache hit rates
-
payload sizes (JSON, images)
Avoid:
- "micro-optimizations" without a measured bottleneck
Tools by layer:
| Layer | Tool | Use case |
|---|---|---|
| End-to-end (synthetic) | Lighthouse, WebPageTest | CI, pre-release checks, lab conditions |
| End-to-end (real users) | RUM (e.g., Vercel Analytics, SpeedCurve) | p75/p95 in production |
| Server traces | OpenTelemetry, Datadog APM, Jaeger | Request spans, DB calls, cache hits |
| DB | pg_stat_statements, slow query logs, EXPLAIN ANALYZE | Query time, frequency, plans |
| Client | Chrome DevTools Performance, Web Vitals API | LCP, INP, long tasks, layout thrash |
RUM vs synthetic:
- Synthetic: Controlled environment, reproducible, good for CI. Misses real network, devices, and user behavior.
- RUM: Real conditions, real percentiles. Needs volume; noisy on low-traffic pages.
Senior rule:
Measure before you optimize. Fix the biggest bottleneck first.
CACHING HIERARCHY
Layers:
-
browser (assets)
-
CDN (cacheable GETs)
-
server/app cache (hot computed results)
-
DB/query cache (where appropriate)
Keys:
-
include auth scope
-
include tenantId
-
include version
Invalidation:
-
prefer event-driven invalidation for correctness-critical data
-
otherwise TTL + stale-while-revalidate
Stampede control:
-
request coalescing
-
jittered TTL
-
soft/hard TTL
Decision table — Data type → Cache layer → TTL → Invalidation:
| Data type | Cache layer | TTL | Invalidation strategy |
|---|---|---|---|
| Static assets (JS, CSS, images) | CDN + browser | 1y (immutable) | Cache-busting filename / version in URL |
| Product catalog (read-heavy) | CDN + app | 5–15 min | Event: product updated → purge key |
| User session / auth | App (Redis/Memcached) | Session lifetime | Event: logout → delete |
| Search results | App | 1–5 min | TTL + stale-while-revalidate |
| Dashboard aggregates | App | 30s–2 min | TTL; event-driven if real-time matters |
| User-specific lists (e.g., cart) | App, keyed by userId | Per-request or short TTL | Not CDN-cacheable; auth in key |
Senior rule:
Cache keys must encode everything that affects the response. Omit tenantId once and you have a data leak.
PAYLOAD BUDGETS (FRONTEND IS BACKEND TOO)
Large payloads kill:
-
mobile networks
-
render time
-
memory
Senior tactics:
-
pagination and windowing
-
field selection (don't ship unused fields)
-
compress responses
-
image resizing and modern formats
Concrete size targets:
| Payload type | Target | Rationale |
|---|---|---|
| Initial JS (gzipped) | < 200KB | Parse + compile cost; mobile CPUs |
| Critical CSS | < 50KB | Blocking render |
| API response (list) | < 50KB | Fast parse, low memory |
| API response (single entity) | < 20KB | Avoid over-fetching |
| Hero image (above fold) | < 100KB | LCP impact |
| Total above-fold | < 500KB | 3G-like conditions |
GraphQL and field selection:
- GraphQL lets clients request only needed fields. Use it.
- Default to a minimal "list view" fragment; expand only for detail views.
- Avoid
*or "fetch everything" patterns. Enforce field allowlists server-side. - Paginate connections:
first: 20, not unbounded.
Senior rule:
Every byte you send is a byte the user pays for. Ship only what the view needs.
RENDERING STRATEGIES (SSR/CSR/STREAMING)
Rules of thumb:
-
SSR for fast first content
-
CSR for highly interactive subtrees
-
streaming when you can progressively reveal content
But always measure real user metrics.
Tradeoff table:
| Strategy | First content | TTI / interactivity | SEO | Complexity | Use when |
|---|---|---|---|---|---|
| SSR | Fast | Slower (hydration) | Full | Medium | Marketing, product pages, content |
| CSR | Slower (fetch + render) | After load | Needs prerender/crawl | Lower | Dashboards, apps, auth-gated |
| Streaming (e.g., Suspense) | Fast + progressive | Progressive | Good | Higher | Long pages, feeds, modular content |
| ISR (Incremental Static Regeneration) | Fast | Fast (static) | Full | Medium | Content that can be stale 60s–1h |
ISR: Pre-render at build or on first request; revalidate in background. Best for product catalogs, blogs, config-driven pages.
Hydration cost:
- Hydration = running framework JS to attach event listeners and make SSR HTML interactive.
- Cost scales with DOM size and component count. Large trees = long main-thread blocks = bad INP.
- Mitigations: partial hydration (only hydrate above-fold or critical widgets), lazy hydration, islands architecture.
Senior rule:
Choose rendering strategy based on what you need first: content speed, interactivity, or both. Don't default to "full SSR" or "full CSR" without a reason.
PROFILING PLAYBOOK
-
Reproduce slow path (prod traces > local guessing)
-
Identify bottleneck category: IO, DB, CPU, serialization, client render
-
Fix the bottleneck (one change)
-
Measure again
Tools per layer:
| Layer | Tool | What to look for |
|---|---|---|
| Client | Chrome DevTools Performance | Long tasks (>50ms), layout thrash, forced reflows |
| Client | Lighthouse, Web Vitals | LCP, INP, TBT, CLS |
| Client | React DevTools Profiler, flamegraphs | Component render cost, unnecessary re-renders |
| Server | OpenTelemetry / APM traces | Span duration, DB vs compute vs external calls |
| DB | pg_stat_statements, slow query log | Total time, calls, mean time per query |
| DB | EXPLAIN ANALYZE | Missing indexes, seq scans, high cost |
| Network | DevTools Network, WebPageTest | Waterfall, TTFB, payload sizes |
Profiling decision tree:
[Request is slow]
│
┌───────────────┼───────────────┐
▼ ▼ ▼
[TTFB high?] [LCP slow?] [INP bad?]
│ │ │
▼ ▼ ▼
Check server Check: Check:
traces - LCP element - Long tasks
- DB spans? - Image size? - Event handlers
- Cache miss? - Render block? - Main thread
- Serialization? - Fonts? - Hydration
│ │ │
▼ ▼ ▼
[DB slow] → [Resource] → [JS] →
EXPLAIN, Optimize Code-split,
indexes delivery defer, lazy
Senior rule:
One change at a time. Measure. Then the next change.
PERFORMANCE REGRESSION PREVENTION
Budgets are useless if regressions ship. Automate.
CI performance budgets:
- Run Lighthouse (or similar) on every PR for critical routes.
- Fail the build if budgets are exceeded.
- Use
lighthouse-cior@lhci/clito enforce.
Lighthouse CI example:
# .github/workflows/perf.yml
- name: Run Lighthouse CI
run: |
npx @lhci/cli@0.12.x autorun
Configure lighthouserc.js with budgets:
// lighthouserc.js
module.exports = {
ci: {
assert: {
assertions: {
'first-contentful-paint': ['error', { maxNumericValue: 2000 }],
'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
'interactive': ['error', { maxNumericValue: 3500 }],
},
},
},
};
Bundle size tracking:
- Use
bundlesizeorsize-limitin CI. - Fail if JS/CSS bundles grow beyond thresholds.
- Track trends (e.g.,
bundlesizewith GitHub Actions).
p95 alerting:
- In production, alert when p95 TTFB, LCP, or INP exceeds budget.
- Use RUM (e.g., Vercel Analytics, SpeedCurve, custom) to trigger PagerDuty/Slack.
Senior rule:
If it's not in CI or alerting, it will regress. Guaranteed.
REAL-WORLD BUDGET EXAMPLE
Dashboard page — end-to-end budget:
| Metric | Budget | Allocation |
|---|---|---|
| TTFB p95 | 400ms | DNS 20 + TLS 30 + RTT 40 + CDN 50 + Server 150 + DB 110 |
| LCP p75 | 2.5s | TTFB 400 + HTML 100 + CSS 80 + JS 200 + Render 200 + LCP image 1520 |
| INP p75 | 200ms | Click → handler < 150ms + paint < 50ms |
| Initial JS | 180KB gzip | Framework 80 + app 70 + vendor 30 |
| API (dashboard data) | 45KB | Paginated, field selection |
Assumptions:
- Dashboard is auth-gated; no CDN for API. Server + DB must stay within 260ms.
- LCP element = chart or primary data table. Image/asset optimized.
- INP: table sorting, filters, modals. Handlers kept small; no 100ms+ sync work.
When budget is exceeded:
- TTFB > 400ms → Trace server; check DB query plan, cache hit rate, N+1.
- LCP > 2.5s → Reduce JS, defer non-critical, optimize LCP resource.
- INP > 200ms → Profile handlers; break up long tasks; consider Web Workers.
EXERCISES
-
Define budgets for one critical page (TTFB/LCP/INP).
-
List your caching layers and which data belongs in each.
-
Pick one endpoint and cut payload size by 30% (design how).
-
Add a Lighthouse CI budget to one route and make the build fail when exceeded.
-
Draw a profiling decision tree for your slowest page.