Table view: every scroll start/stop re-renders all visible rows (2.0.1 beta) — #141 is not fixed
Nobody has claimed this yet.
- Dominant language
- No language data
- Stars
- 28
- Forks
- 2
- PR merge metrics
- No merged PRs in 30d
Description
Summary
Scrolling a tag/class page rendered as Table View saturates the main thread. This is the same problem as #141 ("Scrolling horrifically slow with a table in view"), which was closed on 2025-05-19 as "addressed with the latest version". It is not fixed in 2.0.1. A user already reported on 2025-06-05 in that thread that it still reproduced; that comment received no reply.
The cost scales with rows × columns, matching the original report ("a few properties greater than 4 is choppy, at about 10 properties it's unusable").
Environment
- Logseq Desktop 2.0.1 beta (2026-07-13), AppImage, Electron 42 / Chromium 148
- Arch Linux, Hyprland / native Wayland (
xwayland: 0), Intel Iris Plus (Ice Lake) - Hardware acceleration confirmed:
ANGLE (Intel, Mesa Intel(R) Iris(R) Plus Graphics (ICL GT2), OpenGL ES 3.2)— not a software-rendering fallback devicePixelRatio1.25, honoured natively- DB graph: 76 pages, 22 tags, 37 properties — tiny graph, console clean, no exceptions
Reproduction
- Create a class with ~12 typed properties and ~60 objects (I imported via
@logseq/cli import-edn) - Open the class page — it renders as Table View, 12 columns
- Scroll
Measurements
Taken with CDP Performance.getMetrics (deltas over each window) and Profiler, on the 60 rows × 12 columns page.
Scrolling (programmatic wheel events, 3.2 s):
busy 92.8 % | script 46 % | style 20 % | layout 2.8 %
90 layouts, 165 style recalcs in 3.2 s
165 style recalcs in 3.2 s is roughly a full recalc every frame. Longest single blocking task observed: 153 ms. In a separate run, 40 wheel ticks over ~2 s produced 780–880 ms of total blocking time.
Mounting the view (navigate away and back), first 6 s:
busy 18.2 % | script 9.8 % | style 3.7 % | layout 0.8 % | 9 layouts, 18 recalcs
Steady state (view settled, no interaction) is clean:
busy 0.1 % | 0 layouts, 0 recalcs
So there is no permanent background loop — the problem is the scroll path (and, to a much lesser degree, the initial mount).
Scaling with cell count, sampled CPU during load/scroll windows:
| view | share of samples in (idle) |
|---|---|
| ordinary page | 91–92 % |
| tag page, no objects | 93–95 % |
tag Task, 9 rows × 4 columns |
56 % |
| class, 60 rows × 12 columns | 24 % (30 % in (program) = layout/paint) |
Suspected cause
src/main/frontend/components/views.cljs at tag 2.0.1, table-body (defined line 1951), lines 1966–1970:
:context {:scrolling scrolling?}
:is-scrolling set-scrolling!
:item-content (fn [idx _user ^js context]
(let [option (assoc option
:scrolling? (when context (.-scrolling context))
is-scrolling flips React state on every scroll start and stop. That produces a new Virtuoso context object, which invalidates item-content for every mounted row, and each row then re-renders all of its property-value cells. With 12 columns × ~26 rendered rows that is ~312 cell components re-rendered twice per scroll gesture.
is-scrolling occurs exactly once in the whole file — list-view and gallery-view do not pass it, and they are noticeably cheaper in practice.
Two other contributing factors in the same file:
- Table overscan is
:increase-viewport-by {:top 300 :bottom 300}(line 1957) vs{:top 64 :bottom 64}for list view (line 2018) — ~5× more rows kept mounted. - Grouped views disable virtualization entirely:
;; disabled virtualization for nested view→:disable-virtualized? true. With grouping on, every row of an expanded group is mounted.
The only consumers of the flag appear to be the placeholder branch in the row component and the fetch guard in lazy-item. Both look like they could use a ref / useSyncExternalStore, or the row could be memoised on [row-id, visible-columns], so that a context change does not cascade into every cell.
Not caused by the Rum → HSX migration
I checked views.cljs at d185ea58~1 (the commit before #12748): :context {:scrolling ...} and :is-scrolling are already present there, and the HSX diff for these components is a mechanical rum/defc … < rum/static → hsx/defc swap. HSX memoises by default (USE_MEMO), so memoisation was not dropped.
Ruled out
- Software rendering — real GPU confirmed
- Electron/Wayland launch flags —
--in-process-gpuand--enable-features=WaylandLinuxDrmSyncobjwere active during all measurements above and changed nothing - Fractional display scaling — DPR 1.25 is honoured natively; layout is only 2.8 % of scroll time
- Graph size or corruption — tiny graph,
logseq validateclean apart from two built-in properties, no console errors - CSS
content-visibility: autoon rows — measured, made it slightly worse (92.8 % → 94.8 % busy); the bottleneck is JS re-render, not paint skipping
Workarounds for users hitting this today
- Switch the view to List View (no
is-scrolling, smaller overscan, cheaper row fetch) - Hide unused columns via Columns visibility
- Turn Group by off, so virtualization stays enabled
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in src/main/frontend/components/views.cljs at tag 2.0.1, especially table-body around lines 1951-1970, and trace :context/:is-scrolling into the row component and lazy-item. Confirm which consumers require the scrolling flag, then measure the 60-row × 12-column view with the reported CDP metrics. Done means scroll start/stop no longer re-renders every visible row and scrolling blocking time improves.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- clojure, electron
- Domain
- frontend, performance
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100