Jamie Software Lab
Home / Engineering / Performance & Scalability
Performance Caching Load Testing Lighthouse

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.

97
Performance
100
Accessibility
100
Best Practices
100
SEO
🏗️
Why the scores are high
  • 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.

LCP
Largest Contentful Paint

< 1.2s : No hero images or lazy-loaded resources. The largest element is the headline text, which renders on first paint.

FID
First Input Delay

< 10ms : Almost no JavaScript to block the main thread. Event listeners are attached synchronously in a tiny inline script.

CLS
Cumulative Layout Shift

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.

bash : Load test with wrk
# 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
1,247
Requests / sec
38ms
Avg latency
112ms
P99 latency
0
Errors
📊
Testing Approach
  • 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
🔍
Bottleneck Analysis
  • 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.

Caching Layers
Browser Cache
CSS/JS: 7 days
Nginx
static: immutable
App Cache
API: 10s TTL
Database
source of truth
python : Simple in-memory cache
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
🌐
CDN / Static

Nginx serves CSS/JS with Cache-Control: public, immutable and 7-day expiry. Cache-busted via query strings (?v=2).

API Cache

Stock quotes cached for 10 seconds : balances freshness with API rate limits. Cache invalidated on each new fetch, no stale data served.

🔮
Future: Redis

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.

📈
Key Optimizations
  • 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
📉
Anti-Patterns Avoided
  • 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