Bugs
Open

Table view with 220 rows scrolls at ~10 fps — the row DOM is rebuilt continuously (thousands of insertions/second), and every scroll notch re-lays-out the whole table

Bug found using deepseek v4.1 flash.

Title: Table view with 220 rows scrolls at ~10 fps — the row DOM is rebuilt continuously (thousands of insertions/second), and every scroll notch re-lays-out the whole table

App: Capacities 1.72.7 (Windows desktop) · Runtime: Electron 42.9.0 / Chromium 148.0.7778.280 · OS: Windows 10 Pro 22H2, build 19045.6466 · Hardware: i5-14600KF, 47.8 GB RAM, RTX 5060 Ti, 2560×1440 @ 125 %, D3D acceleration active

Steps to reproduce

  1. Open a database with ~200+ objects (mine: 220 rows).
  2. Switch to Table view → All tab.
  3. Scroll with the mouse wheel.

Expected: smooth 60 fps scrolling. Actual: ~10 fps; frames take 100–350 ms; a 6.2 s continuous scroll rendered only 61 frames.

Measurements (in-app over CDP; window visible and focused; CPU at 5.27 GHz, GPU idle < 4 %)

代码块

frames 61 in 6.2 s ≈ 10 fps
avg / p95 / p99 frame 101 / 195 / 344 ms
long tasks (> 50 ms) 51 (total 3,998 ms)
main thread TaskDuration 6.125 s / 6.2 s → 100 % busy
ScriptDuration 0.106 s → JS is only ~2 %
LayoutDuration 0.227 s
RecalcStyleDuration 0.745 s

Trace of a 5 s scroll (34,807 events): Paint 554 ms / 3,756 events · Layerize 657 ms / 112 · HitTest 510 ms / 158 · UpdateLayoutTree 359 ms · Commit 284 ms · RunMicrotasks 1,690 ms · V8 GC ~1,200 ms.

CPU profile during scrolling (self time): storing…js 99 ms, insertBefore 90 ms, Vue reactivity get 85 ms, getBoundingClientRect 74 ms, RenderIcon 70 ms, removeChild 63 ms, addEventListener 62 ms. The table mutates its own DOM while scrolling, and reads layout in the middle of it.

DOM of that view: 220 rows · 23,463 elements · 213,361 nodes · 1,559 inline <svg> (~7 per row) · ~106 elements per row · scrollHeight 8,005 px vs 964 px viewport · JS heap 268 MB (94 MB before scrolling) · renderer private ~780 MB. Scroll to 25 %, 60 %, 100 % and back to top: element count stays 23,463 — nothing is virtualized, recycled or unmounted.

The churn is continuous and scales with the number of rows (prototype instrumentation):

SituationinsertBeforesetAttributeremoveChildIdle, 3 s, no input at all4,2974681603 wheel notches7,5337083033 wheel notches, 190 of 220 rows hidden1761762,025

The view rebuilds row DOM even when nothing is happening (~1,400 insertions/s idle). With 220 rows mounted one wheel notch triggers ≈ 2,500 node insertions, each invalidating layout of a 213 k-node table — that is where the 100–350 ms frames come from.

What is not the cause: CPU frequency (verified 5.27 GHz, no throttling), GPU (< 4 %, hardware acceleration on, not SwiftShader), JS logic (ScriptDuration 0.106 s), the display (60 Hz is native).

Suggested fixes, by impact

  1. Virtualize the table body (visible rows + overscan + spacer).
  2. Stop re-creating row DOM on scroll / on a timer — 4,297 insertBefore in 3 s of idleness is a bug by itself. Suspects: a component re-rendering on a reactive scrollTop, a :key that changes, marquee/measure components (data-no-entity-marquee is on every row), or a v-for over a freshly computed array.
  3. Reduce nodes per row — 1,559 inline SVGs for 220 rows; use a sprite / <use> or icon font.
  4. Avoid layout reads inside scroll handlers (getBoundingClientRect is hot; 220 rows × 1 read per tick forces synchronous reflow).
  5. Stopgap while virtualization is built: content-visibility: auto; contain-intrinsic-size: auto <rowHeight> on the row wrapper. I tested this live in 1.72.7 and it did not help — the cost is the re-creation, not off-screen painting, so it only pays off together with #2.

Additional context: with only ~30 rows mounted (a filtered view) the same input produced 43× fewer DOM insertions, matching the subjective difference — that's the workaround I use today. I can provide the raw trace JSON, the CPU profile and the repro scripts.

0 Comments

Sign in to comment

No comments yet. Be the first to share your thoughts!