Performance Best Practices

Practical, high-leverage rules for writing fast code across the Code2b stack: Go services, Postgres, and the Next.js frontend. Each item is short on purpose. Apply the cheap wins first, measure, then optimize the hot path.

Rule zero: measure before you optimize. Profile, find the real bottleneck, fix that. Guessing wastes time and adds complexity for no gain.


General principles

  1. The fastest work is work you don't do. Cache, skip, batch, or precompute before you micro-optimize.
  2. Optimize the hot path, not the whole codebase. 5% of the code causes 95% of the slowness. Leave the rest readable.
  3. Reduce round trips. One query, one request, one allocation beats N of them. N+1 is the number-one real-world killer.
  4. Do work once. Compute at boot or write time, not on every read.
  5. Keep data close. Memory beats cache beats disk beats network. Move work down that list.
  6. Readable first, fast second, unless proven hot. A clever line that saves a nanosecond in cold code is a net loss.

Variables and memory

Tip Why
Preallocate slices/maps with known size: make([]T, 0, n) Avoids repeated reallocation and copying as they grow
Reuse buffers instead of allocating in a loop Each allocation costs GC pressure
Pass large structs by pointer, small ones by value Copying big structs is wasted work; tiny copies are free
Prefer value types for small, short-lived data Stack allocation, no GC involvement
Avoid interface{} / any on hot paths Boxing forces heap allocation and kills inlining
Scope variables tightly Smaller scope helps the compiler keep values in registers
Use strings.Builder, never += in a loop String concatenation allocates a new string every time
// Bad: reallocates on every append, unknown final size
var ids []uuid.UUID
for _, lead := range leads {
    ids = append(ids, lead.ID)
}
 
// Good: one allocation
ids := make([]uuid.UUID, 0, len(leads))
for _, lead := range leads {
    ids = append(ids, lead.ID)
}

Functions

Tip Why
Keep hot functions small Small functions get inlined, removing call overhead
Return early, avoid deep nesting Skips unnecessary work and reads cleaner
Accept context.Context and honor cancellation Stop work the caller no longer wants
Take slices/interfaces, return concrete types Fewer allocations at call sites
Don't pass more than you use Passing whole structs when you need one field couples and copies needlessly
Batch APIs over per-item APIs SaveAll(items) beats calling Save(item) in a loop
// Bad: one DB round trip per lead
for _, lead := range leads {
    repo.UpdateStatus(ctx, lead.ID, "DONE")
}
 
// Good: one round trip
repo.UpdateStatusBulk(ctx, ids, "DONE")

Loops and algorithms

Tip Why
Pick the right data structure A map lookup is O(1); scanning a slice is O(n)
Hoist invariant work out of loops Don't recompute the same value every iteration
Break/continue early Stop as soon as the answer is known
Avoid nested loops over the same data O(n²) explodes; build a map/set once and look up
Watch hidden costs len() is free; a regex compile or JSON marshal in a loop is not
// Bad: O(n*m), scans allowed list for every lead
for _, lead := range leads {
    for _, id := range allowedIDs {
        if lead.ID == id { keep(lead) }
    }
}
 
// Good: O(n+m), set lookup
allowed := make(map[uuid.UUID]struct{}, len(allowedIDs))
for _, id := range allowedIDs { allowed[id] = struct{}{} }
for _, lead := range leads {
    if _, ok := allowed[lead.ID]; ok { keep(lead) }
}

Concurrency (Go)

Tip Why
Parallelize independent I/O Waiting on 4 APIs serially wastes 3 API-times
Bound concurrency with a worker pool or semaphore Unbounded goroutines exhaust memory, DB, and rate limits
Use errgroup for fan-out with error handling Clean cancellation when one task fails
Never share a struct across goroutines without a lock or channel Data races corrupt state and crash under -race
Don't spawn a goroutine for trivial work Goroutines are cheap, not free; scheduling still costs
// Bound fan-out to LLM_CONCURRENCY (see docvalidation card)
sem := make(chan struct{}, concurrency)
g, ctx := errgroup.WithContext(ctx)
for _, dim := range dimensions {
    dim := dim
    sem <- struct{}{}
    g.Go(func() error {
        defer func() { <-sem }()
        return audit(ctx, dim)
    })
}
if err := g.Wait(); err != nil { return err }

Database (Postgres)

The single biggest source of real slowness. Fix here first.

Tip Why
Kill N+1 queries Fetch related rows in one query with a join or WHERE id = ANY($1)
Index columns you filter, join, or sort on A missing index turns a lookup into a full table scan
SELECT only the columns you need SELECT * ships and parses data you throw away
Paginate; never fetch unbounded rows One SELECT returning a million rows OOMs the service
Batch inserts/updates One statement with many rows beats N statements
Use EXPLAIN ANALYZE on slow queries Shows the real plan: seq scan vs index scan
Move filtering into SQL, not Go The database filters faster and ships less data
Reuse a connection pool; set sane limits Opening connections per request is expensive
-- Bad: N+1, one query per job
SELECT * FROM leads WHERE lead_job_id = $1;  -- run once per job
 
-- Good: one query for all jobs
SELECT * FROM leads WHERE lead_job_id = ANY($1);
-- Index the columns you filter and join on
CREATE INDEX IF NOT EXISTS idx_leads_job_id ON leads (lead_job_id);

Caching

Tip Why
Cache expensive, rarely changing reads Turns repeated work into one lookup
Load config/pricing once at boot enrichabl loads its pricing cache at startup; restart after changes
Set a TTL or clear invalidation rule A cache with no eviction becomes a stale-data bug
Cache the result, not the raw inputs Store the computed answer so reads are free
Don't cache user-specific data in a shared key One user sees another's data: correctness bug, not just perf

API and network

Tip Why
Return only the fields the client needs Smaller payloads parse and transfer faster
Paginate list endpoints Protects both server and client from huge responses
Enable gzip/brotli compression Text payloads shrink 5-10x on the wire
Set cache headers on static/derived responses Lets browsers and CDNs skip the request entirely
Reuse HTTP clients (keep-alive) New TLS handshakes per request are slow
Prefer one batch endpoint over chatty calls Each request pays latency; combine where possible

Frontend (Next.js / React)

Tip Why
Render on the server (RSC/SSG) where possible Ships less JS; content is ready on first paint
Code-split and lazy-load heavy components Users download only what the current view needs
Memoize expensive renders and computations React.memo, useMemo, useCallback skip needless re-renders
Give lists stable keys Wrong keys force React to re-render and remount rows
Virtualize long lists Render the visible rows, not 10,000 DOM nodes
Optimize images (next/image, right sizes) Images are usually the largest bytes on a page
Debounce/throttle input handlers Don't fire a search or fetch on every keystroke
Fetch in parallel, avoid request waterfalls Sequential awaits stack latency
// Bad: recomputed on every render
const sorted = leads.sort((a, b) => a.score - b.score)
 
// Good: recomputed only when leads change
const sorted = useMemo(
  () => [...leads].sort((a, b) => a.score - b.score),
  [leads],
)

Measuring

You cannot fix what you cannot see. Reach for these before optimizing.

Tool Use for
go test -bench + -benchmem Micro-benchmark a function and see allocations
pprof (CPU + heap) Find the real hot path and allocation sources
go test -race Catch data races before they hit production
EXPLAIN ANALYZE Diagnose a slow query's plan
Chrome DevTools Performance / Lighthouse Frontend render, bundle size, and load timing
React DevTools Profiler Find components that re-render too often

Checklist for a performance pass

  • Profiled and confirmed the actual bottleneck (not guessed)
  • No N+1 queries; related data fetched in one round trip
  • Filter, sort, and join columns are indexed
  • List endpoints and queries are paginated / bounded
  • Slices and maps preallocated where size is known
  • No allocations or expensive calls inside hot loops
  • Independent I/O runs in parallel, with bounded concurrency
  • Expensive, stable reads are cached with a clear invalidation rule
  • Frontend: heavy components lazy-loaded, long lists virtualized, expensive work memoized
  • Re-measured after the change to confirm a real gain

When in doubt, measure first and copy the pattern already used in enrichabl (pricing cache, bounded fan-out) or docvalidation (concurrent fan-out with LLM_CONCURRENCY).