perf(note types): second pass — fix more missing remember keys

Follow-up to the first perf pass. Same goal: cut allocation cost for
note rows that scroll inside LazyColumn feeds.

- AppDefinition: key the `remember { tags.toImmutableListOfLists() }`
  block by `note` so it actually invalidates when the note changes
- NIP90ContentDiscoveryResponse: drop the `remember(note) {
  Modifier.fillMaxWidth() }` wrapper — `Modifier.fillMaxWidth()` is a
  constant call
- PeopleList: key the `derivedStateOf` for `name` by `noteEvent`, and
  switch `LaunchedEffect(Unit)` to `LaunchedEffect(noteEvent)` so the
  participants reload when the underlying event changes
- PinList: replace `val pins by remember { mutableStateOf(noteEvent
  .pinnedEvents()) }` with `val pins = remember(noteEvent) { … }` —
  the `mutableStateOf` wrapper was unnecessary and the missing key
  meant `pins` could go stale on event updates
- LongForm: return the `topics` list as `ImmutableList` so Compose
  treats it as a stable parameter to the consuming `forEach`
- RelayList: drop the `mutableStateOf(RelayListCard(…))` wrap inside
  4 `remember` blocks (DisplayRelaySet, DisplayNIP65RelayList write/
  read, DisplayDMRelayList) — the value never changes after creation;
  also key by `noteEvent` rather than `baseNote`, and cache
  `noteEvent.description()`
- Torrent: wrap `noteEvent.title() + totalSizeBytes()`, content
  comparison and `files().toImmutableList()` in `remember(noteEvent)`
  so they don't recompute and reallocate on every recomposition
This commit is contained in:
Claude
2026-04-26 15:34:22 +00:00
parent bf540db557
commit 6aecfe016b
7 changed files with 47 additions and 41 deletions
@@ -216,7 +216,7 @@ fun RenderAppDefinition(
CreateTextWithEmoji( CreateTextWithEmoji(
text = it, text = it,
tags = tags =
remember { remember(note) {
(note.event?.tags ?: emptyArray()).toImmutableListOfLists() (note.event?.tags ?: emptyArray()).toImmutableListOfLists()
}, },
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
@@ -64,6 +64,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size5dp
import com.vitorpamplona.amethyst.ui.theme.grayText import com.vitorpamplona.amethyst.ui.theme.grayText
import com.vitorpamplona.amethyst.ui.theme.replyModifier import com.vitorpamplona.amethyst.ui.theme.replyModifier
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
import kotlinx.collections.immutable.toImmutableList
private const val WORDS_PER_MINUTE = 225 private const val WORDS_PER_MINUTE = 225
private val COVER_ASPECT_RATIO = 16f / 9f private val COVER_ASPECT_RATIO = 16f / 9f
@@ -92,7 +93,14 @@ fun LongFormHeader(
remember(noteEvent) { remember(noteEvent) {
noteEvent.summary()?.ifBlank { null } ?: noteEvent.content.take(200).ifBlank { null } noteEvent.summary()?.ifBlank { null } ?: noteEvent.content.take(200).ifBlank { null }
} }
val topics = remember(noteEvent) { noteEvent.topics().distinct().take(3) } val topics =
remember(noteEvent) {
noteEvent
.topics()
.distinct()
.take(3)
.toImmutableList()
}
val readingMinutes = remember(noteEvent) { estimateReadingMinutes(noteEvent.content) } val readingMinutes = remember(noteEvent) { estimateReadingMinutes(noteEvent.content) }
Column(MaterialTheme.colorScheme.replyModifier) { Column(MaterialTheme.colorScheme.replyModifier) {
@@ -54,7 +54,6 @@ fun RenderNIP90ContentDiscoveryResponse(
note = note, note = note,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
) { ) {
val modifier = remember(note) { Modifier.fillMaxWidth() }
val tags = val tags =
remember(note) { note.event?.tags?.toImmutableListOfLists() ?: EmptyTagList } remember(note) { note.event?.tags?.toImmutableListOfLists() ?: EmptyTagList }
@@ -62,7 +61,7 @@ fun RenderNIP90ContentDiscoveryResponse(
content = noteEvent.content, content = noteEvent.content,
canPreview = canPreview && !makeItShort, canPreview = canPreview && !makeItShort,
quotesLeft = quotesLeft, quotesLeft = quotesLeft,
modifier = modifier, modifier = Modifier.fillMaxWidth(),
tags = tags, tags = tags,
backgroundColor = backgroundColor, backgroundColor = backgroundColor,
id = note.idHex, id = note.idHex,
@@ -81,7 +81,7 @@ fun DisplayPeopleList(
members.take(3) members.take(3)
} }
val name by remember { derivedStateOf { "#${noteEvent.titleOrName() ?: noteEvent.dTag()}" } } val name by remember(noteEvent) { derivedStateOf { "#${noteEvent.titleOrName() ?: noteEvent.dTag()}" } }
Text( Text(
text = name, text = name,
@@ -95,7 +95,7 @@ fun DisplayPeopleList(
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
LaunchedEffect(Unit) { LaunchedEffect(noteEvent) {
accountViewModel.loadUsers(noteEvent.taggedUserIds()) { accountViewModel.loadUsers(noteEvent.taggedUserIds()) {
members = it members = it
} }
@@ -65,7 +65,7 @@ fun RenderPinListEvent(
) { ) {
val noteEvent = baseNote.event as? PinListEvent ?: return val noteEvent = baseNote.event as? PinListEvent ?: return
val pins by remember { mutableStateOf(noteEvent.pinnedEvents()) } val pins = remember(noteEvent) { noteEvent.pinnedEvents() }
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
@@ -75,12 +75,10 @@ fun DisplayRelaySet(
) { ) {
val noteEvent = baseNote.event as? RelaySetEvent ?: return val noteEvent = baseNote.event as? RelaySetEvent ?: return
val relays by val relays =
remember(noteEvent) { remember(noteEvent) {
mutableStateOf( RelayListCard(
RelayListCard( noteEvent.relays().toImmutableList(),
noteEvent.relays().toImmutableList(),
),
) )
} }
@@ -89,10 +87,12 @@ fun DisplayRelaySet(
noteEvent.tags.firstTagValueFor("title", "name") ?: "#${noteEvent.dTag()}" noteEvent.tags.firstTagValueFor("title", "name") ?: "#${noteEvent.dTag()}"
} }
val description = remember(noteEvent) { noteEvent.description() }
DisplayRelaySet( DisplayRelaySet(
relays, relays,
relayListName, relayListName,
noteEvent.description(), description,
backgroundColor, backgroundColor,
accountViewModel, accountViewModel,
nav, nav,
@@ -108,21 +108,17 @@ fun DisplayNIP65RelayList(
) { ) {
val noteEvent = baseNote.event as? AdvertisedRelayListEvent ?: return val noteEvent = baseNote.event as? AdvertisedRelayListEvent ?: return
val writeRelays by val writeRelays =
remember(baseNote) { remember(noteEvent) {
mutableStateOf( RelayListCard(
RelayListCard( noteEvent.writeRelaysNorm() ?: emptyList(),
noteEvent.writeRelaysNorm() ?: emptyList(),
),
) )
} }
val readRelays by val readRelays =
remember(baseNote) { remember(noteEvent) {
mutableStateOf( RelayListCard(
RelayListCard( noteEvent.readRelaysNorm() ?: emptyList(),
noteEvent.readRelaysNorm() ?: emptyList(),
),
) )
} }
@@ -154,12 +150,10 @@ fun DisplayDMRelayList(
) { ) {
val noteEvent = baseNote.event as? ChatMessageRelayListEvent ?: return val noteEvent = baseNote.event as? ChatMessageRelayListEvent ?: return
val relays by val relays =
remember(baseNote) { remember(noteEvent) {
mutableStateOf( RelayListCard(
RelayListCard( noteEvent.relays(),
noteEvent.relays(),
),
) )
} }
@@ -153,19 +153,24 @@ fun RenderTorrent(
) { ) {
val noteEvent = note.event as? TorrentEvent ?: return val noteEvent = note.event as? TorrentEvent ?: return
val name = (noteEvent.title() ?: TorrentEvent.ALT_DESCRIPTION) val title =
val size = " (" + countToHumanReadableBytes(noteEvent.totalSizeBytes()) + ")" remember(noteEvent) {
val name = noteEvent.title() ?: TorrentEvent.ALT_DESCRIPTION
val description = val size = " (" + countToHumanReadableBytes(noteEvent.totalSizeBytes()) + ")"
if (noteEvent.content != name) { name + size
noteEvent.content
} else {
null
} }
val description =
remember(noteEvent) {
val name = noteEvent.title() ?: TorrentEvent.ALT_DESCRIPTION
if (noteEvent.content != name) noteEvent.content else null
}
val files = remember(noteEvent) { noteEvent.files().toImmutableList() }
DisplayFileList( DisplayFileList(
noteEvent.files().toImmutableList(), files,
name + size, title,
description, description,
noteEvent::toMagnetLink, noteEvent::toMagnetLink,
backgroundColor, backgroundColor,