Unloading earlier posts in PostStream shifts the content that is on screen (CLS ~1.0 per trim)
- Dominant language
- PHP
- Stars
- 6.7k
- Forks
- 883
- Avg merge
- 15h 16m
- Merged PRs (30d)
- 73
Description
### Current Behavior
When you scroll deep into a discussion, `PostStreamState#_loadNext()` unloads the posts two pages back:
https://github.com/flarum/framework/blob/2.x/framework/core/js/src/forum/states/PostStreamState.ts#L187-L204
```js
const twoPagesAway = start - PostStreamState.loadCount * 2;
if (twoPagesAway > this.visibleStart && twoPagesAway >= 0) {
this.visibleStart = twoPagesAway + PostStreamState.loadCount + 1;
...
}
```
`loadPage()` wraps the resulting redraw in `anchorScroll()`, and the anchor element and arithmetic are correct — `scrollY` really does move by the height of the removed posts. But `anchorScroll` measures, runs `m.redraw.sync()`, and *then* sets `scrollTop`:
https://github.com/flarum/framework/blob/2.x/framework/core/js/src/common/utils/anchorScroll.js
The correction therefore lands after the redraw's layout, so the browser still records the movement. The result is a layout shift of **~1.0 every two pages of scrolling**, attributed to the post items that are on screen at the time — plus a jump that visibly costs the reader their place.
Captured at the moment of a trim (mobile viewport 412x823):
```
range 0-60 -> 41-80, DOM items 60 -> 39, scrollY 17160 -> 6090
v=1.0
DIV.PostStream-item[i=55] prev y=0 h=0 -> cur y=0 h=400
DIV.PostStream-item[i=56] prev y=0 h=0 -> cur y=400 h=228
DIV.PostStream-item[i=57] prev y=0 h=0 -> cur y=628 h=195
```
Reading one 85-post thread end to end accumulates CLS 1.018. A 752-post thread accumulates ~18, one shift per trim. Desktop is much less affected (~0.003 on the same thread) because the moved area is a smaller fraction of a large viewport.
### Steps to Reproduce
1. Open a discussion with 80+ posts on a phone-sized viewport (412x823), throttled (~1.6 Mbps, 150 ms latency).
2. Scroll down continuously past the third page of posts, until `visibleStart` moves forward.
3. Observe the jump, and a `layout-shift` entry of ~1.0 whose sources are the on-screen `PostStream-item`s.
Measured with a buffered `PerformanceObserver` on `layout-shift` (`hadRecentInput` excluded), scrolling with real wheel input via CDP `Input.dispatchMouseEvent`, headless Chrome 140, 4x CPU throttle. Reproduced identically on two rc.8 installs.
### Expected Behavior
Trimming the stream should not move what the reader is looking at.
Options, in the order I'd rank them:
1. **Keep the space instead of the nodes** — when posts are trimmed from the top, carry their measured height as padding on `.PostStream` and release it when `loadPrevious` restores them. Nothing below moves, no scroll manipulation is needed, and the DOM stays bounded.
Worth flagging: I tried this as a small patch and it does **not** work that way. Adding the padding on trim without symmetric handling in `loadPrevious` makes the stream thrash — the scroll position stops matching the loaded range, `loadPrevious` fires, posts are re-added while the padding stays, and it compounds (on a 752-post thread the padding ran away to 250,000 px and `visibleStart` oscillated 21→42→44→46→68→49…). Doing it properly means making "space above" a single consistent notion handled in both directions.
2. **Correct the scroll without an intermediate layout** — adjust the scroll position in the same layout pass as the removal rather than reading offsets around a sync redraw.
3. **Trim less often** — widen the retention window, ideally viewport-aware. Reduces the frequency but does not remove the shift.
I'm running option 3 as an extension override in production (keep loaded posts, hand back to core above 200) which takes the 85-post thread from 1.018 to 0.018, but that is a mitigation and not the fix.
### Environment
- Flarum version: 2.0.0-rc.8
- Website URL: https://pianoclack.com
- Webserver: nginx
- Hosting environment: VPS
- PHP version: 8.4.24
- Browser: Chrome 140 (mobile emulation, and reproduced on real Android Chrome field data)
Contributor guide
Research direction
Start with PostStreamState.ts at _loadNext() and loadPage(), then read common/utils/anchorScroll.js to trace the redraw and scroll correction. Reproduce on a 412x823 viewport using an 80+ post discussion and observe layout-shift entries while trimming. Done means scrolling no longer visibly jumps or records large shifts, while loading previous posts remains consistent with the retained range.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, typescript
- Domain
- frontend, performance
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100