Performance & Scalability
How I measure, optimize, and validate performance across the portfolio. Load testing methodology, caching strategies, database query tuning, and frontend performance metrics.
Lighthouse Scores
The portfolio homepage scores well on all Lighthouse categories. No external fonts, no JavaScript frameworks, minimal CSS : the site loads fast by default.
- No JavaScript frameworks : entire site is ~15 lines of vanilla JS for the nav toggle
- System fonts : zero web font downloads, no FOIT/FOUT
- Single CSS file : under 4KB gzipped, no unused selectors
- Static HTML : served directly by Nginx with cache headers
- Semantic HTML : proper heading hierarchy, landmarks, skip links
Core Web Vitals
Google's Core Web Vitals measure real-world user experience. The portfolio passes all three metrics comfortably.
< 1.2s : No hero images or lazy-loaded resources. The largest element is the headline text, which renders on first paint.
< 10ms : Almost no JavaScript to block the main thread. Event listeners are attached synchronously in a tiny inline script.
0 : No dynamic content injection, no delayed font loading, no images without dimensions. Layout is stable from first paint.
Load Testing Methodology
I test API endpoints with wrk and hey to understand throughput
limits and identify bottlenecks before they become production incidents.
# Test price API with 50 concurrent connections for 30 seconds wrk -t4 -c50 -d30s https://jamieblair.co.uk/api/quote/CSCO # Results: # Requests/sec: 1,247 # Avg latency: 38ms # P99 latency: 112ms # Errors: 0
- Baseline first : test with a single connection to get the floor latency
- Ramp up gradually : 10 → 50 → 100 → 200 concurrent connections
- Measure percentiles : P50/P90/P99, not just averages
- Look for error rate : when do 5xx responses start appearing?
- Test after index changes : verify that DB optimizations work under load
- CPU-bound : yfinance parsing under heavy load; solved with response caching
- Disk I/O : SQLite WAL mode prevents write-lock contention
- Network : Nginx connection limits at 200 concurrent; sufficient for portfolio traffic
- Memory : Gunicorn workers capped at 4; ~50MB per worker
Caching Strategies
Multi-layer caching: Nginx serves static assets with 7-day headers, API responses are cached in-memory for short durations, and the browser respects ETags for conditional requests.
import time from functools import lru_cache _cache = {} def cached(ttl_seconds=10): """Simple TTL cache decorator. No external dependencies.""" def decorator(func): def wrapper(*args): key = (func.__name__, args) now = time.monotonic() if key in _cache: val, ts = _cache[key] if now - ts < ttl_seconds: return val result = func(*args) _cache[key] = (result, now) return result return wrapper return decorator @cached(ttl_seconds=10) def get_quote(symbol): # Expensive external API call return yfinance.Ticker(symbol).info
Nginx serves CSS/JS with Cache-Control: public, immutable and 7-day expiry.
Cache-busted via query strings (?v=2).
Stock quotes cached for 10 seconds : balances freshness with API rate limits. Cache invalidated on each new fetch, no stale data served.
As traffic grows, replace in-memory cache with Redis. Shared across workers, supports TTL natively, adds pub/sub for invalidation.
Database Query Optimization
See Database Architecture → Query Optimization for detailed EXPLAIN analysis and indexing examples.
- Composite indexes : reduced listing search from 142ms to 3ms
- WAL mode : SQLite write-ahead logging for concurrent access
- Connection pooling : Prisma pool capped at 10 connections
- Cursor pagination : constant-time page fetches vs. OFFSET degradation
- No N+1 queries : eager loading via Prisma
include - No SELECT * : explicit column selection always
- No OFFSET pagination : cursor-based pagination only
- No unindexed WHERE clauses : queries profiled in development