Channel list takes ~5s to appear on cold start, after the chat has already rendered #20

Open
opened 2026-09-01 16:52:55 +00:00 by mohlec · 1 comment
Owner

On cold launch the channel list is blank for roughly 5 seconds. The chat screen for the
last-viewed channel renders well before the list does, which is the reverse of what users
expect.

Ruled out: scale. Reported by a user with 40 channel memberships (20 public, 10 DM,
9 GM, 1 private), 3 sidebar categories and 1 unread channel, on a current flagship handset.
That should render in well under a second, so the number of channels is not the cause.

Verified mechanism: the sidebar is all-or-nothing gated. In
app/screens/home/channel_list/categories_list/categories/helpers/observe_flattened_categories.ts,
observeFlattenedCategoriesNormal builds:

combineLatest([combineLatest(categoryDataObservables), unreadsOnTop])

and each observeCategoryData is itself a combineLatest of nine observables (category
channels, notify props per channel, four preference queries, deactivated users, the DM
limit, category state). combineLatest emits nothing until every source has emitted at
least once, so the list stays empty until the slowest of roughly 30 local DB queries lands.
The channel screen reads from a much smaller observable set and is mounted first in the
navigation stack, which explains the ordering users see.

Possible contributing factor, unconfirmed. The affected user has no explicit
sidebar-channel placements stored server-side, so all of their channels are unassigned and
resolved into the default categories dynamically on each categories fetch rather than from a
stable stored mapping. That would put a network round-trip inside the gate above. Users who
have organised their sidebar do have stored placements; several who have not are also
candidates to reproduce.

Discriminating tests before writing code. The two candidate fixes are quite different,
so it is worth confirming which applies:

  1. Do other users with no explicit sidebar placements see the same delay? If it tracks that
    group, it is the categories round-trip inside the gate. If everyone sees it, it is the
    combineLatest gate alone, and the fix is to make the sidebar emit incrementally
    (render categories as they resolve, seed from last known state).
  2. Have an affected user favourite or move one channel into a category, then cold start. If
    the delay drops, that confirms the stored-placement path.
  3. Same handset on WiFi versus cellular, to rule the network round-trip in or out.

Reported: SM-S938U / Android 16, app v1.0.0+1, 2026-09-01.

On cold launch the channel list is blank for roughly 5 seconds. The chat screen for the last-viewed channel renders well before the list does, which is the reverse of what users expect. **Ruled out: scale.** Reported by a user with 40 channel memberships (20 public, 10 DM, 9 GM, 1 private), 3 sidebar categories and 1 unread channel, on a current flagship handset. That should render in well under a second, so the number of channels is not the cause. **Verified mechanism: the sidebar is all-or-nothing gated.** In `app/screens/home/channel_list/categories_list/categories/helpers/observe_flattened_categories.ts`, `observeFlattenedCategoriesNormal` builds: ``` combineLatest([combineLatest(categoryDataObservables), unreadsOnTop]) ``` and each `observeCategoryData` is itself a `combineLatest` of nine observables (category channels, notify props per channel, four preference queries, deactivated users, the DM limit, category state). `combineLatest` emits nothing until **every** source has emitted at least once, so the list stays empty until the slowest of roughly 30 local DB queries lands. The channel screen reads from a much smaller observable set and is mounted first in the navigation stack, which explains the ordering users see. **Possible contributing factor, unconfirmed.** The affected user has no explicit sidebar-channel placements stored server-side, so all of their channels are unassigned and resolved into the default categories dynamically on each categories fetch rather than from a stable stored mapping. That would put a network round-trip inside the gate above. Users who have organised their sidebar do have stored placements; several who have not are also candidates to reproduce. **Discriminating tests before writing code.** The two candidate fixes are quite different, so it is worth confirming which applies: 1. Do other users with no explicit sidebar placements see the same delay? If it tracks that group, it is the categories round-trip inside the gate. If everyone sees it, it is the `combineLatest` gate alone, and the fix is to make the sidebar emit incrementally (render categories as they resolve, seed from last known state). 2. Have an affected user favourite or move one channel into a category, then cold start. If the delay drops, that confirms the stored-placement path. 3. Same handset on WiFi versus cellular, to rule the network round-trip in or out. **Reported:** SM-S938U / Android 16, app v1.0.0+1, 2026-09-01.
Author
Owner

Upstream context: the rebase does not fix the mechanism, but it does move the ground

Checked this against upstream (55 commits since fork point 1e169c78).

The gate is unchanged upstream. Diffing
app/screens/home/channel_list/categories_list/categories/helpers/observe_flattened_categories.ts
between this fork and upstream shows exactly one line of difference, and it is our own
unreadsOnTop default flip. The combineLatest([combineLatest(categoryDataObservables), unreadsOnTop])
all-or-nothing gate described above is still present upstream, untouched. So a rebase will not fix
this, and any fix we write to that file will rebase cleanly given the one-line divergence.

But the surrounding render layer is rewritten. b6fc85385 (Expo Router / RN 0.83.9 / New
Architecture) rewrites channel_list.tsx, categories_list.tsx and categories.tsx. Those
changes are navigation wiring and FlashList v2 API churn (estimatedItemSize and
overrideItemLayout are gone in v2), not changes to the gating -- however, moving from
react-native-navigation to Expo Router changes screen mount ordering, and mount ordering is half
of the reported symptom (the chat rendering before the list).

Recommendation: re-measure after the rebase, before designing the fix. The mechanism will still
be there, but the observed 5 seconds and the render ordering may both shift, and the discriminating
tests listed above are cheap to re-run at that point. The incremental-emission fix (render
categories as they resolve rather than waiting on the slowest query) stays the likely answer.

### Upstream context: the rebase does not fix the mechanism, but it does move the ground Checked this against upstream (55 commits since fork point `1e169c78`). **The gate is unchanged upstream.** Diffing `app/screens/home/channel_list/categories_list/categories/helpers/observe_flattened_categories.ts` between this fork and upstream shows **exactly one line of difference**, and it is our own `unreadsOnTop` default flip. The `combineLatest([combineLatest(categoryDataObservables), unreadsOnTop])` all-or-nothing gate described above is still present upstream, untouched. So a rebase will not fix this, and any fix we write to that file will rebase cleanly given the one-line divergence. **But the surrounding render layer is rewritten.** `b6fc85385` (Expo Router / RN 0.83.9 / New Architecture) rewrites `channel_list.tsx`, `categories_list.tsx` and `categories.tsx`. Those changes are navigation wiring and FlashList v2 API churn (`estimatedItemSize` and `overrideItemLayout` are gone in v2), not changes to the gating -- **however**, moving from react-native-navigation to Expo Router changes screen mount ordering, and mount ordering is half of the reported symptom (the chat rendering before the list). Recommendation: **re-measure after the rebase, before designing the fix.** The mechanism will still be there, but the observed 5 seconds and the render ordering may both shift, and the discriminating tests listed above are cheap to re-run at that point. The incremental-emission fix (render categories as they resolve rather than waiting on the slowest query) stays the likely answer.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
slash/slash-mobile#20
No description provided.