Every text message subscribes to one global translation atom → timeline-wide re-render storms #39

Closed
opened 2026-09-12 01:51:07 -04:00 by jared · 0 comments
Owner

Severity: medium · Type: performance · Confidence: high

Location: src/app/hooks/useMessageTranslation.ts:50,53-56,76-93, src/app/state/translation.ts:16-36,79, src/app/components/message/MsgTypeRenderers.tsx:195

Problem

TranslatableBody mounts for every m.text/m.emote/m.notice in the timeline, and useMessageTranslation does useAtom(translationCacheAtom) on the single global array. Translating one message rewrites that array, so all mounted messages re-render. Each render also recomputes makeCacheKey (an O(len) fingerprint of the body) and findTranslation (a linear scan of up to 300 entries). Separately, msgTranslationActiveAtomFamily(eventId) creates one atom per event id and is never .remove()d, so the family grows monotonically for the session as you scroll. With autoTranslate on, the effect at :76-93 fires engine.detect(text) for every message that mounts, and its autoTriedText guard is per-hook-instance — so virtualised scrolling re-runs detection on every pass over the same messages.

How to trigger

Open a busy room with autoTranslate on and scroll; or translate one message in a room with 50+ rendered text messages and profile the render.

Suggested fix

Replace the array atom with a keyed atomFamily/selectAtom so a message only subscribes to its own entry; memoise the cache key per (eventId, text); hoist the auto-detect "already tried" set to module scope; and evict msgTranslationActiveAtomFamily entries on room change.


Filed from the September 2026 client audit (branch lotus @ 4bea4895).

**Severity:** medium · **Type:** performance · **Confidence:** high **Location:** `src/app/hooks/useMessageTranslation.ts:50,53-56,76-93`, `src/app/state/translation.ts:16-36,79`, `src/app/components/message/MsgTypeRenderers.tsx:195` ### Problem `TranslatableBody` mounts for **every** `m.text`/`m.emote`/`m.notice` in the timeline, and `useMessageTranslation` does `useAtom(translationCacheAtom)` on the single global array. Translating one message rewrites that array, so all mounted messages re-render. Each render also recomputes `makeCacheKey` (an O(len) fingerprint of the body) and `findTranslation` (a linear scan of up to 300 entries). Separately, `msgTranslationActiveAtomFamily(eventId)` creates one atom per event id and is never `.remove()`d, so the family grows monotonically for the session as you scroll. With `autoTranslate` on, the effect at :76-93 fires `engine.detect(text)` for every message that mounts, and its `autoTriedText` guard is per-hook-instance — so virtualised scrolling re-runs detection on every pass over the same messages. ### How to trigger Open a busy room with `autoTranslate` on and scroll; or translate one message in a room with 50+ rendered text messages and profile the render. ### Suggested fix Replace the array atom with a keyed `atomFamily`/`selectAtom` so a message only subscribes to its own entry; memoise the cache key per `(eventId, text)`; hoist the auto-detect "already tried" set to module scope; and evict `msgTranslationActiveAtomFamily` entries on room change. --- _Filed from the September 2026 client audit (branch `lotus` @ 4bea4895)._
jared added this to the Audit 2026-09 · Medium & Low milestone 2026-09-12 01:51:07 -04:00
jared added the bugpriority: mediumperformancearea: messaging labels 2026-09-12 01:51:07 -04:00
jared self-assigned this 2026-09-12 01:51:07 -04:00
jared closed this issue 2026-09-15 21:32:09 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
2026-11-14
Dependencies

No dependencies set.

Reference: LotusGuild/cinny#39