Skip to content

Commit 248839e

Browse files
fix(table): drive cell typewriter with rAF so concurrent reveals stay smooth
The character-by-character reveal used a per-cell setInterval. When many cells reveal at once (a Run-all completing in waves), the independent interval callbacks fire at uncoordinated times and each forces its own render + layout/paint — O(cells) reflows over an un-virtualized grid, so it degrades as more cells fill. Switch to requestAnimationFrame: all cells' callbacks run before one paint, so React batches them into a single render + paint per frame regardless of cell count. Reveal length is derived from elapsed time, so a dropped frame catches up instead of slowing the animation.
1 parent c1a7142 commit 248839e

1 file changed

Lines changed: 19 additions & 7 deletions

File tree

  • apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,15 @@ const TYPEWRITER_MS_PER_CHAR = 15
296296
* value statically — animation fires only for subsequent updates, which in
297297
* practice means SSE-driven workflow completions arriving via
298298
* `useTableEventStream → applyCell()`.
299+
*
300+
* Driven by `requestAnimationFrame`, not `setInterval`: when many cells reveal
301+
* at once (a Run-all completing in waves), independent interval callbacks fire
302+
* at uncoordinated times and each forces its own React render + layout/paint —
303+
* O(cells) reflows over an un-virtualized grid, which degrades as more cells
304+
* fill. rAF callbacks for a frame all run before one paint, so React batches
305+
* every cell's update into a single render + paint per frame (~60fps,
306+
* independent of cell count). Reveal length is derived from elapsed time, so a
307+
* dropped frame catches up instead of slowing the animation.
299308
*/
300309
function useTypewriter(text: string | null): string | null {
301310
const [revealed, setRevealed] = useState<string | null>(text)
@@ -317,14 +326,17 @@ function useTypewriter(text: string | null): string | null {
317326
return
318327
}
319328

329+
const full = text
330+
const start = performance.now()
331+
let raf = 0
332+
const tick = (now: number) => {
333+
const chars = Math.min(full.length, Math.floor((now - start) / TYPEWRITER_MS_PER_CHAR))
334+
setRevealed(full.slice(0, chars))
335+
if (chars < full.length) raf = requestAnimationFrame(tick)
336+
}
320337
setRevealed('')
321-
let i = 0
322-
const id = window.setInterval(() => {
323-
i++
324-
setRevealed(text.slice(0, i))
325-
if (i >= text.length) window.clearInterval(id)
326-
}, TYPEWRITER_MS_PER_CHAR)
327-
return () => window.clearInterval(id)
338+
raf = requestAnimationFrame(tick)
339+
return () => cancelAnimationFrame(raf)
328340
}, [text])
329341

330342
return revealed

0 commit comments

Comments
 (0)