TRM Labs · 2023 – 2026
Making an investigation graph interactive at scale
Large investigations froze TRM Forensics' graph. A profiling-led push of 20+ small PRs took plot matching on 1,600 elements from 15.5 s to 14 ms.
- Role
- Led the performance push
- Stack
- React, TypeScript, KeyLines, ReGraph, Chrome DevTools
- graph plot matching on 1,600 elements
- 15.5 s → 14 ms
- PRs in the graph performance push
- 20+
Context
TRM Forensics helps investigators at government agencies and financial institutions trace funds across blockchain addresses. The investigation graph is the heart of the product, and the biggest investigations are the most important ones. They were also the ones that froze: plot matching on 1,600 elements took 15.5 seconds, during which the whole page was unresponsive.
Constraints
- The graph is rendered by KeyLines, a licensed library. Swapping the renderer was not an option for a fix investigators needed now.
- Features built on top of the graph (combos, grouping, selections) could not regress.
- Some government customers run the product inside locked-down, browser-isolated environments, so “works on my machine” was not enough.
Decisions
Profile before changing anything. Profiling pointed at plot matching: to find where each incoming
element belonged, an inner loop scanned every element already plotted. With n elements that is up to n
steps per element, quadratic work on the main thread. I memoized each element’s index in a Map as it was
plotted, so every lookup became O(1). That change alone took 1,600 elements from 15.5 s to 14 ms, with
identical output.
Many small PRs, not one rewrite. I led a series of more than 20 PRs. Each was reviewable, measurable and revertable on its own, which kept the risk low on the product’s most critical surface.
The same class of bug, earlier. When the graph still ran on ReGraph, in late 2025, a plotting
step built its result in a reduce that spread the accumulator into a fresh copy on every iteration.
Reusing the same accumulator reference removed a full copy per element.
Fix the interaction, not just the algorithm. Separately from plotting, opening the sidebar resized the graph while the sidebar was also rendering. Rather than redesign it as an overlay, I deferred the sidebar’s work until the transition finished, which removed the jank without changing the design.
Close the loop with real customers. One government customer couldn’t plot certain addresses. Comparing icon variants showed that SVGs with embedded Base64 data failed after a change in their locked-down, browser-isolated environment; removing that content fixed it.
Impact
- Plot matching on 1,600 elements: 15.5 s → 14 ms, roughly 1,100× faster.
- Large investigations stay interactive instead of freezing the page.
What I’d do next
I prototyped a viewport-based graph renderer to test whether owning the rendering layer could outperform the licensed library at scale. It was an exploration, handed over for evaluation; the real question it raises is long-term maintenance cost versus the licence, and that is a team decision, not a benchmark.
The homepage re-creates this fix live: the “broken” plot uses the same scan-inside-a-loop pattern, calibrated to freeze for a second or two on your device.