flarum / flarum/framework

Unloading earlier posts in PostStream shifts the content that is on screen (CLS ~1.0 per trim)

Open
#5,008 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.