What actually went wrong
The handler did the obvious thing: mark the clicked row as selected, then re-render the
table so the highlight shows up. Re-rendering meant building all 516 rows again. That
alone is O(N) work for an O(1) change, which is wasteful but
survivable.
What made it 13 seconds instead of 30 milliseconds was the second mistake: each row's size was read back out of the layout engine immediately after it was written. Writing invalidates layout; reading forces it to be recomputed on the spot. Do both in a loop and the browser recalculates the whole document once per row — 516 full layout passes for one click.
Drag rows up and run it again. The red bar should grow with the row count — roughly quadratically, since the per-row cost also rises as the document gets bigger. The green bar should barely move: it does the same two class changes whether there are 64 rows or 2048.
An honest note on the numbers. The original 13.0 s → 7.5 ms was measured in a Python desktop UI, not in a browser, so the absolute figures here will not match — your machine, your engine, and a much lighter row are all different. What reproduces is the shape: an O(1) change priced as O(N), amplified by synchronous layout reads, and a fix that is subtraction rather than cleverness.