Why we rebuilt it
The old kanban worked, but felt sluggish on accounts with 200+ deals. "Felt sluggish" is not a bug report, so the first week of the project was spent turning it into one.
We profiled the render path on a copy of a real account — 340 deals across nine columns — and found three culprits, none of which were the ones we would have guessed.
Rather than patch incrementally we rewrote the view around React 19's concurrent rendering primitives. That decision cost us four extra weeks and we would make it again.
Where the time actually went
The profile was unambiguous once we looked at it properly:
Every card re-rendered on every drag frame, because the drag position lived in the same context as the card data.
Currency and date formatting ran per render, per card — 340 cards times two formatters times sixty frames a second.
The column totals recomputed from the full deal list instead of from a running sum.
What changed
Drag-and-drop now uses optimistic updates with rollback: the card moves the moment you release it, and if the mutation fails it snaps back with a toast explaining why. In eight weeks of internal use the rollback has fired four times, all of them genuine permission errors.
Inline editing lets you skip the modal entirely for the common edits — amount, deadline, owner. The modal is still there for everything else, because a board is a bad place to read a long description.
Column totals are now maintained incrementally, and the formatters are hoisted out of the render path into memoised instances. Neither change is clever. Both were worth more than the rewrite.
The numbers
Average interaction latency dropped from 320ms to 45ms (P95) on the same 340-deal account, measured with the Long Animation Frames API over a scripted drag sequence.
The bundle got slightly bigger — 11KB gzipped — because the optimistic layer carries its own reducer. We took that trade without much debate.
The rewrite bought us maybe half the improvement. The other half was two afternoons of removing work from the render path — which we could have done at any point in the previous year.
How we measured it
Every number in this post comes from the same script: a Playwright run that drags eight cards across four columns on a seeded copy of the account, with the Long Animation Frames API recording every task over 50ms.
We run it on every pull request that touches the board, and it fails the build if P95 regresses more than 15% against the stored baseline. That gate has fired three times since May, twice correctly.
The reason for the script rather than a synthetic benchmark is boring: the slow path was never a single function. It was the interaction between drag state, formatting, and a context that re-rendered a subtree nobody was looking at.
What we deliberately left out
Virtualised columns. On a 340-deal board the naive render is now fast enough, and virtualisation would have broken drag-and-drop across a scroll boundary — a trade nobody asked for.
Real-time collaboration on the board itself. Two people dragging the same card is rare, the conflict is easy to explain after the fact, and the machinery to prevent it is not.
Offline support. We looked at it for a week, wrote down what "a queued move against a deal that no longer exists" should do, and closed the branch.
What is next
Bulk actions are coming next month: select a range, move or reassign in one mutation. After that, custom field columns and saved board layouts.
If you run boards larger than 500 deals, we would like to hear from you before we ship the next round — our largest test account is still smaller than we would like.
