← Back to blog

AI Code Review for Performance Regressions: What a Reviewer Can Catch Before the p99 Graph Does

Performance bugs are correct code that is merely slow, which is why they sail through review. The six patterns an AI reviewer can catch on every PR, the measurements it cannot make, and the rules that keep the comments useful.

8 min read
AI Code Review for Performance Regressions: What a Reviewer Can Catch Before the p99 Graph Does

Performance bugs are the ones code review is worst at. A security hole has a shape a reviewer can learn; a failing test is red. A performance regression is usually correct code that is merely slow in a way nobody can see from the diff: a query inside a loop that was fine with ten rows in the fixture, a cache key that stopped matching, a list that is now sorted on every keystroke. It passes review, passes CI, ships, and shows up three weeks later as a p99 graph and a very polite message from the on-call engineer.

This post is about what an AI code reviewer can and cannot do about that. The short version: it can catch the structural patterns that cause most regressions, reliably and on every PR, and it cannot tell you how slow anything actually is. Knowing where that line sits is what makes the reviewer useful instead of noisy.

Why the diff hides performance problems

Most regressions are not in the changed lines; they are in the relationship between the changed lines and something else. The classic N+1 is a one-line change that adds a lookup inside a function, and the problem is that the function is called from a loop two files away. A removed index hint, a new await inside a for, a .filter().map().filter() chain over a collection that used to be small: each of these is innocuous in isolation. The reviewer has to know what calls what, how big the inputs are in practice, and what the code looked like before, and a reviewer that only sees the diff knows none of that. We covered the general version of this failure in why diff-only reviewers miss real bugs. Performance is where it bites hardest, because nothing in the diff is wrong.

The patterns a reviewer can catch

These are the things worth asking an AI reviewer to look for explicitly, in roughly the order they cause production incidents. Each is a pattern, which is what language models are good at spotting, rather than a measurement, which they cannot make.

1. Database access inside a loop

The N+1 query, in all its forms: an ORM relation accessed per item, a find or get call in a map, a SELECT built per row. The tell is a function that does I/O being called from inside an iteration, and the fix is almost always a batch load, a join, or a prefetch. A reviewer with repository context can follow the call: the diff adds user.organization.name to a serializer, and the serializer is used in a list endpoint. That is a review comment a human would be proud of and most humans would miss at 5 pm.

2. Sequential awaits that could be concurrent

for (const id of ids) { await fetch(id) } is correct and serial. Whether it should be Promise.all depends on the downstream service and rate limits, so the right comment is a question, not a demand: this runs one request at a time; is that intended? The same applies to the reverse, an unbounded Promise.all over a list the user controls, which is a different kind of outage.

3. Work moved into a hot path

A regex compiled inside a function that is called per request. A date formatter constructed per row. JSON parsed from the same config string on every call. A sort inside a React render, or a new object literal passed as a prop that defeats memoization. None of these are bugs; all of them are costs that used to be paid once and are now paid every time. A reviewer can see the hot path only if it knows what the hot path is, which is a strong argument for telling it: a project instruction such as “anything under src/handlers/ runs per request; flag allocations and compilation there” is a one-line rule with a long payoff.

4. Cache changes

Changing a cache key, a TTL, or what gets cached is the quietest way to double a database’s load. The regression is that the hit rate drops, which is invisible in code and only visible in a graph. A reviewer should flag any PR that touches cache key construction and ask whether the new key still matches between writer and reader, whether a parameter was added that makes every request unique, and whether the invalidation path was updated too.

5. Unbounded growth

A list that is appended to and never trimmed. A map used as a cache with no eviction. A subscription or listener added on every render and never removed. A query with no LIMIT on a table that is small today. These are the regressions that take weeks to appear, because memory and row counts grow slowly, and they are easy for a reviewer to spot once asked, because the pattern is “something grows and nothing shrinks it”.

6. Algorithmic complexity hiding in library calls

array.includes inside a loop over another array is quadratic. A Set would be linear. list.index(x) in Python, indexOf in JavaScript, Contains on a slice in Go: all fine for a hundred items, all problems at a hundred thousand. A reviewer can name the complexity and ask for the expected size; it is the asking for the size that matters, because the author usually knows and nobody else does.

What the reviewer cannot know

Honesty here keeps the review useful. A model reading code cannot know your row counts, your traffic shape, your cache hit rate, which endpoints are hot, or whether the serial awaits are serial on purpose because the downstream API rate-limits you. It cannot measure. A comment that says “this is an N+1” is a finding; a comment that says “this will add 400 ms” is a guess dressed as a finding, and you should be suspicious of any reviewer, human or model, that produces the second kind without a benchmark attached.

That is why the right output from a performance-focused review is a question with a specific location: this call is inside a loop over orders; how many orders does a typical request have? The author answers in ten seconds, and either the comment is resolved or it becomes a fix. The failure mode to avoid is the reviewer that flags every loop, which trains the team to ignore it; the trade-offs are in how to keep AI review signal above its noise.

Turning this into review rules

Generic reviewers produce generic performance comments. The useful ones come from telling the reviewer what matters in your codebase. In PURA that is project-level review instructions, and a set that works for most web backends looks like this:

  • Flag any database, HTTP or file I/O call that is reachable from inside a loop, and name the loop.
  • Flag await inside for or forEach; ask whether sequential execution is intended.
  • Flag changes to cache keys, TTLs or invalidation, and ask for the matching reader or writer.
  • In src/handlers/ and src/middleware/, flag regex compilation, JSON parsing of constants, and allocation of formatters inside the function body.
  • Flag collections that grow without a bound, listeners added without removal, and queries without a limit on tables listed in docs/large-tables.md.
  • Flag quadratic membership checks and suggest a set or map when the collection is not tiny.
  • Do not estimate timings. State the pattern and ask for the expected input size.

Pair the rules with an explicit budget decision: performance comments are advisory by default, and blocking only on the two or three patterns that have actually caused incidents for your team. We described how to make that choice in blocking vs advisory review, and how to write the rules themselves in custom review rules.

What this is a complement to, not a replacement for

A reviewer that asks the right structural questions on every PR raises the floor. It does not replace a benchmark in CI for the endpoints you care about, a slow-query log, or a p95 alert, because those measure, and review does not. The combination is what works: the review catches the pattern before merge, the benchmark catches the regression the pattern did not predict, and the alert catches the one both of them missed. Put the cheapest check earliest, and the cheapest one is a reviewer that never gets tired of asking how many rows there are.

Frequently asked questions

What performance problems can AI code review actually catch?
It can reliably spot structural patterns: database or HTTP calls inside loops (N+1), sequential awaits, regex or formatter construction in per-request code, changed cache keys, collections that grow without a bound, and quadratic membership checks. Those are visible in code, especially with repository-wide context to follow the call chain.
Can an AI reviewer tell me how slow a change will be?
No. A model reading code cannot know row counts, traffic shape, cache hit rates or which endpoints are hot, so it cannot estimate how slow a change will be. The useful output is a located question, such as pointing at a call inside a loop and asking for the expected input size, rather than a timing guess.
How do I stop performance review comments from becoming noise?
Tell the reviewer what matters in your codebase: which directories are hot paths, which tables are large, and which patterns have caused incidents. Keep most performance comments advisory and block only on the two or three patterns that have actually hurt you, so the signal stays above the noise.

Ready to put your AI review spend on rails?

Install PURA on your GitHub repos and start setting budgets in minutes — not months.

Install PURA for free