## Re-tested on 1.73.1: idle churn is fixed, but scroll is still ~13–17 fps (no virtualization was added)
## Re-tested on 1.73.1: idle churn is fixed, but scroll is still ~13–17 fps (no virtualization was added)
I re-ran my original measurements against Capacities 1.73.1 (Electron 42.9.0 / Chromium 148.0.7778.280), the version that auto-installed on my machine today. Same database, same 223-row table view, same hardware as my first report.
Summary: one item from my report is genuinely fixed. The one that matters for scroll perception is not.
### Results (223 rows, table view, window visible and focused, hardware accel on)
| Metric | 1.72.7 (original report) | 1.73.1 (this test) |
|---|---|---|
| Frame rate while scrolling | ~10 fps | 13.1 fps |
| Frames / duration | 61 / 6.2 s | 82 / 6.27 s |
| Avg / p95 / p99 frame | 101 / 195 / 344 ms | 76 / 129 / 151 ms |
| Long tasks (>50 ms) | 51 (3,998 ms) | 45 (3,334 ms) |
| DOM in rest state | 23,463 elements / 213,361 nodes | 8,390 elements / 22,840 nodes |
| Inline <svg> | 1,559 | 484 |
| Rows mounted | 220 | 223 (all of them) |
Repeat runs of the same scroll: 13.4 / 17.0 / 15.6 / 16.9 fps. Idle in the same window, measured immediately before: 59.6 fps. So this is not thermal throttling, not GPU, and not a busy machine (system CPU 0%).
### ✅ Fixed: idle DOM churn
| Situation | 1.72.7 | 1.73.1 |
|---|---|---|
| 3 s idle, no input | 4,297 insertBefore | 0 |
| — | 468 setAttribute | 0 |
| — | 1,603 removeChild | 0 |
Whatever was re-rendering rows on a timer is gone. Node counts per row also dropped a lot (23,463 → 8,390 elements at rest, 1,559 → 484 inline SVGs). Thank you — that's real work and it shows.
### ❌ Not fixed: no virtualization, rows still rebuilt on scroll
1. All 223 rows stay mounted. I stamped each row node with a property and scrolled to 25%, 60%, 100% and back. All 223 stamps were still present afterwards, unchanged. Nothing is unmounted, recycled, or windowed.
2. Scroll still mutates the row DOM. 3 wheel notches → 4,472 insertBefore, 379 setAttribute, 97 removeChild. My original hidden-row control still reproduces: with 193 of 223 rows set to display:none, the same 3 notches produce 109 insertions instead of 4,472. The churn scales with mounted row count, exactly as before.
3. No CSS mitigation was applied either. Zero elements in the document compute to content-visibility: auto; exactly one element has a non-none contain.
4. Main thread is still native-bound. In a scroll CPU profile: (program) 55.9%, GC 3%, and every named JS function below 3.3%. This is layout/paint, not app logic — consistent with the original trace.
### ⚠ New finding: thousands of insertions into detached nodes
While instrumenting insertBefore, I checked this.isConnected on the parent. Over 3 s of scrolling:
- 1,368 insertions into connected (live) parents
- 20,469 insertions into detached parents
Something is building subtrees off-document and then discarding them (or swapping them in). That's ~10× more work than the visible churn suggests, and none of it can produce a paint. Worth a look independent of virtualization — those allocations also explain the heap going 88 MB → 539 MB during a scroll session.
### The measurement that settles the design question
I A/B'd the two candidate approaches with real Input.dispatchMouseEvent wheel events, same session, same table:
| Variant | fps |
|---|---|
| Unmodified (baseline) | 12.7 |
| contain: layout style paint on row wrapper | 15.7 |
| content-visibility: auto only | 14.3 |
| contain: strict | 1.9 ⚠ |
| Render only rows near viewport (+ overscan) | 55.7 |
Repeated the windowing test three times: 55.8 / 49.0 / 57.4 fps, holding ~36 rows in the DOM.
So:
- CSS containment is worth about +3 fps — not a fix, and contain: strict makes it dramatically worse (p95 2,441 ms), so please don't reach for that as a stopgap.
- Windowing is worth ~4× and lands at effectively full frame rate. This matches my original observation and the hidden-row control: the cost is mounted rows invalidating layout, not off-screen painting.
To be fair to my own experiment: my windowing POC used display:none, which removes rows from layout and therefore collapses the scrollbar range (I measured ~5.7k px → ~1.1k px in the state I tested). A shippable version needs absolute positioning with top/bottom spacers and a measured row height (36 px here, but it changes with filtered views). I haven't built that, and I'm not going to ship a monkey-patch that manipulates row styling in a database app where a mis-targeted click could edit the wrong record.
### Suggested priority
1. Virtualize the table body (visible + overscan + spacer). Measured ceiling: 55+ fps vs 13. This is the only thing that moves the needle.
2. Investigate the detached-node insertions. 20,469 wasted insertions per 3 s of scroll is a bug on its own.
3. Everything else (contain, icon sprites) is now secondary — row node counts are already down 64%.
### How this was measured
CDP against the app with --remote-debugging-port, requestAnimationFrame deltas for frame timing, PerformanceObserver({ entryTypes: ['longtask'] }) for long tasks, Prototype patching of insertBefore/removeChild/setAttribute for churn counts, and Input.dispatchMouseEvent (type mouseWheel, deltaY: 120, 40 ms apart) for scrolling — same method as my first report. All patches were removed and the page reloaded before the headline numbers were taken, and the numbers above were reproduced on a fresh instance.
Happy to provide the raw profiles, the HTTP-DOM snapshots, and the A/B harness.
0 Comments
Sign in to comment
No comments yet. Be the first to share your thoughts!
