backend-perf
Installation
SKILL.md
Backend performance checklist
I walk every check below against the code in hand and state a verdict on each one. Applied, clean, or not applicable and why. A check I did not name is a check I did not run.
Checks marked [pg] are PostgreSQL-specific. I skip them on other stores rather than translating them.
- Parallel async. Independent async calls run together in
Promise.all, never awaited one by one. - N+1 queries. No query inside a sequential
fororfor awaitloop. Batch withINclauses or a join instead.Promise.all(items.map(q => oneQuery(q)))over a few index-friendly queries runs in parallel and is not a sequential N+1. Cap the fan-out: an unbounded items list gets an explicit bound or concurrency limit, since N parallel queries still open N connections. Keep the parallel shape unlessEXPLAINshows the batched join runs faster, because batching often defeats the per-row index. - Pagination counts. Count and rows come from one query: a
count(*) OVER ()window or a single CTE, not a separateCOUNT(*)next to the data query. - Index coverage
[pg]. Every productionWHEREclause needs a matching index.similarity(col, ...),@@ to_tsvector(...), orLIKE '%pattern%'needs the matchinggin (col gin_trgm_ops)orgin (to_tsvector('lang', col))index in the same PR, or a one-line comment citing the ADR or issue that accepts the sequential scan. Prefer the indexable%operator over a rawsimilarity() > thresholdcomparison, but only with a matching threshold:%tests against the sessionpg_trgm.similarity_threshold(default 0.3), so set it to the query's threshold and confirm result-set equivalence, and EXPLAIN-verify that the planner picks the index. - STABLE function reuse
[pg].pg_trgm.similarityandts_rankare STABLE, not IMMUTABLE, so PostgreSQL does not reliably memoize duplicate calls in one SELECT. A value needed twice, for example once in the projection and once inGREATEST(...), is computed once in aWITH ... AS MATERIALIZEDCTE. A plain subquery alias does not guarantee this. The planner can flatten it and evaluate per row. - Select only needed columns. List endpoints name their columns instead of selecting the whole row.
- Reuse fetched rows. A row fetched for an ownership check is the row the handler returns. No second fetch by id.
- EXPLAIN before rewriting
[pg]. Any query restructure, batching, joining, indexing, or replacement of parallel-N with batched-1 shipsEXPLAIN (ANALYZE, BUFFERS)numbers for current versus proposed shape on representative data. Keep the faster shape. A rewrite that looks faster in the diff can run 10-100× slower on real data. - Race conditions. When 2+ writers can touch one row, use a transaction. Async completion order is not source order. Order explicitly with
awaitorPromise.allinstead of relying on accidental serialization. A check plus a write that must stay atomic usesSELECT ... FOR UPDATEon[pg]or a unique constraint, not application-level locking.