Tell Compose the transcript's rows are values, so it can leave them alone

A page of history landing recomposed every loaded row and everything
inside it -- 701 compositions for 148 rows in one of Iris's scrolls,
which is four rebuilds of the whole visible transcript, markdown and
all. That is what her `waited` at the 90th percentile was: the frame
could not start because the thread was rebuilding rows whose content had
not changed.

Nothing was stopping Compose skipping them except that it could not
prove it was safe to. Stability is inferred from a class's fields and a
`List` field makes it assume the worst, so `TranscriptItem`,
`TranscriptRow` and `ParsedReplies` were all treated as things that
might change underneath a composable at any moment. They are not: a row
is rebuilt from the transcript rather than edited, two rows describing
the same events are equal, and the parse cache is keyed on the text it
parsed. Saying so is the whole change.

Measured on the emulator over the same scroll: 139 row compositions, and
**15** of them rebuilt the message inside. The other 124 skipped
straight past the markdown, which is where the cost was. The
transcript's own draw came down with it, from 3.1ms mean to 1.2ms.

The promise these annotations make has to stay true -- nothing described
by them is mutated after it is built. It is not today, and the note on
`TranscriptRow` says so where somebody adding a field will read it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-08-31 03:03:21 -04:00
1 parent 76c51117d6
commit 5b9941cfaa
4 files changed
+19

No files matched your search

@@ -35,6 +35,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@@ -179,6 +180,7 @@ private fun Modifier.holdTopEdge(key: Any, held: TopEdgeHold, hold: (Int) -> Uni
* stream is the only data source -- opening this screen replays from seq 0, and a reconnect resumes
* from the last seq seen, so there is no separate history fetch to drift from it.
*/
@Immutable
sealed class TranscriptItem {
/**
* The transcript sequence number this row started at, and its identity on screen.