perf(desktop): stop re-rendering the app shell on every keystroke; fix two latent cache races it unmasked (#1692)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Noah
2026-07-10 11:47:18 -04:00
committed by GitHub
parent 715df25375
commit 129c91a2ba
5 changed files with 179 additions and 51 deletions

View File

@@ -43,6 +43,7 @@ import { applyEditTagOverlay } from "@/features/messages/lib/applyEditTagOverlay
import {
emptyChannelWindowStore,
flattenChannelWindowEvents,
mapChannelWindowEvents,
mergeLiveChannelWindowEvent,
mergeLiveThreadSummary,
replaceNewestChannelWindow,
@@ -769,23 +770,33 @@ export function useEditMessageMutation(channel: Channel | null) {
return;
}
// Apply-on-success cache update: reflect the edit's new content and
// imeta tag set immediately, so the local cache matches what the
// receiver overlay (formatTimelineMessages) will produce when the
// edit event arrives back from the relay. (Not a true optimistic
// update — runs in onSuccess, not onMutate. Worth bearing the cost
// only because the edit event round-trip can lag perceptibly.)
const applyEdit = (message: RelayEvent): RelayEvent => {
if (message.id !== eventId) return message;
const nextTags = mediaTags
? applyEditTagOverlay(message.tags, mediaTags)
: message.tags;
return { ...message, content, tags: nextTags };
};
// The WINDOW STORE is the source of truth: every live merge
// re-flattens it over `channelMessagesKey`, so patching only the
// flattened array gets reverted by the next live event (see
// mapChannelWindowEvents). Update the store first, then keep the
// flattened cache in step for immediate paint.
queryClient.setQueryData<ChannelWindowStore>(
channelWindowKey(channel.id),
(current) =>
current ? mapChannelWindowEvents(current, applyEdit) : current,
);
queryClient.setQueryData<RelayEvent[]>(
channelMessagesKey(channel.id),
(current = []) =>
current.map((message) => {
if (message.id !== eventId) return message;
// Apply-on-success cache update: reflect the edit's new content
// and imeta tag set immediately, so the local cache matches
// what the receiver overlay (formatTimelineMessages) will
// produce when the edit event arrives back from the relay.
// (Not a true optimistic update — runs in onSuccess, not
// onMutate. Worth bearing the cost only because the edit event
// round-trip can lag perceptibly.)
const nextTags = mediaTags
? applyEditTagOverlay(message.tags, mediaTags)
: message.tags;
return { ...message, content, tags: nextTags };
}),
(current = []) => current.map(applyEdit),
);
},
});

View File

@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
import test from "node:test";
import {
appendOlderChannelWindow,
mapChannelWindowEvents,
channelWindowThreadSummaries,
emptyChannelWindowStore,
flattenChannelWindowEvents,
@@ -342,3 +343,61 @@ test("live summaries survive scrollback pages and still win for their root", ()
store = appendOlderChannelWindow(store, tail);
assert.equal(channelWindowThreadSummaries(store).get(older.id).replyCount, 5);
});
test("mapChannelWindowEvents rewrites the event everywhere it lives", () => {
const target = event("target", 100);
const bystander = event("bystander", 90);
const auxEvent = event("aux-target", 95, 40003);
let store = replaceNewestChannelWindow(
emptyChannelWindowStore(),
page(null, [target, bystander], { aux: [auxEvent], hasMore: false }),
);
store = mergeLiveChannelWindowEvent(store, event("live-target", 110));
const mapped = mapChannelWindowEvents(store, (candidate) =>
candidate.id === target.id
? { ...candidate, content: "edited" }
: candidate,
);
assert.notEqual(mapped, store);
const flattened = flattenChannelWindowEvents(mapped);
assert.equal(
flattened.find((candidate) => candidate.id === target.id).content,
"edited",
);
// Untouched events keep identity (no churn for memo consumers).
assert.equal(
flattened.find((candidate) => candidate.id === bystander.id),
bystander,
);
});
test("mapChannelWindowEvents returns the same store when nothing matches", () => {
const store = replaceNewestChannelWindow(
emptyChannelWindowStore(),
page(null, [event("a", 100)], { hasMore: false }),
);
assert.equal(
mapChannelWindowEvents(store, (candidate) => candidate),
store,
);
});
test("mapChannelWindowEvents rewrites live overlay events", () => {
const liveEvent = event("live", 110);
const store = mergeLiveChannelWindowEvent(
emptyChannelWindowStore(),
liveEvent,
);
const mapped = mapChannelWindowEvents(store, (candidate) =>
candidate.id === liveEvent.id
? { ...candidate, content: "edited-live" }
: candidate,
);
assert.equal(
flattenChannelWindowEvents(mapped).find(
(candidate) => candidate.id === liveEvent.id,
).content,
"edited-live",
);
});

View File

@@ -215,6 +215,47 @@ export function mergeLiveChannelWindowEvent(
};
}
/**
* Apply a per-event transform across every event the store holds (page rows,
* page aux, live overlay, live aux), returning the same store reference when
* nothing changed.
*
* Local writes MUST go through this rather than patching the flattened
* `channelMessagesKey` array alone: the window store is the source of truth,
* and every live merge re-flattens it into `channelMessagesKey`
* (`flattenChannelWindowEvents`), so an update applied only to the flattened
* array is silently reverted by the next live event. The message-edit
* apply-on-success update hit exactly that: the edit rendered, then the next
* live merge re-flattened the un-edited store over it. (Masked on busy
* screens because unrelated re-renders kept re-deriving state; exposed once
* per-keystroke app-shell renders were removed.)
*/
export function mapChannelWindowEvents(
store: ChannelWindowStore,
map: (event: RelayEvent) => RelayEvent,
): ChannelWindowStore {
let changed = false;
const mapEvent = (event: RelayEvent) => {
const next = map(event);
if (next !== event) changed = true;
return next;
};
const pages = store.pages.map((page) => {
const rows = page.rows.map((row) => {
const event = mapEvent(row.event);
return event === row.event ? row : { ...row, event };
});
const aux = page.aux.map(mapEvent);
return rows.every((row, index) => row === page.rows[index]) &&
aux.every((event, index) => event === page.aux[index])
? page
: { ...page, rows, aux };
});
const liveOverlay = store.liveOverlay.map(mapEvent);
const liveAux = store.liveAux.map(mapEvent);
return changed ? { ...store, pages, liveOverlay, liveAux } : store;
}
/** Raw events in the chronological order expected by the existing renderer. */
export function flattenChannelWindowEvents(store: ChannelWindowStore) {
const byId = new Map<string, RelayEvent>();

View File

@@ -197,21 +197,42 @@ export function usePresenceSession(pubkey?: string) {
React.useState<PresencePreference>(() =>
readStoredPresencePreference(normalizedPubkey),
);
const [lastActivityAt, setLastActivityAt] = React.useState(() => Date.now());
const [statusClock, setStatusClock] = React.useState(() => Date.now());
const [isDocumentHidden, setIsDocumentHidden] = React.useState(() =>
typeof document === "undefined" ? false : document.hidden,
// Activity is tracked in a REF, and React state holds only the DERIVED
// status. The previous shape stored raw `Date.now()` timestamps in state
// and bumped them from a capture-phase `keydown`/`pointerdown` listener —
// which re-rendered this hook's host (AppShell, the app root) on every
// keystroke the user typed anywhere in the app. Presence only needs a
// render when the automatic status actually flips (online <-> away), so
// activity updates the ref and re-derives; setState fires on transitions
// only (see typing-latency.perf.ts / keystroke input-to-paint).
const lastActivityAtRef = React.useRef(Date.now());
const [automaticStatus, setAutomaticStatus] = React.useState<PresenceStatus>(
() =>
resolveAutomaticPresenceStatus(
typeof document === "undefined" ? false : document.hidden,
lastActivityAtRef.current,
Date.now(),
),
);
const automaticStatusRef = React.useRef(automaticStatus);
const skipNextSyncRef = React.useRef<PresenceStatus | null>(null);
React.useEffect(() => {
const now = Date.now();
setPresencePreference(readStoredPresencePreference(normalizedPubkey));
setLastActivityAt(now);
setStatusClock(now);
setIsDocumentHidden(
const reevaluateAutomaticStatus = React.useEffectEvent(() => {
const next = resolveAutomaticPresenceStatus(
typeof document === "undefined" ? false : document.hidden,
lastActivityAtRef.current,
Date.now(),
);
if (next !== automaticStatusRef.current) {
automaticStatusRef.current = next;
setAutomaticStatus(next);
}
});
React.useEffect(() => {
setPresencePreference(readStoredPresencePreference(normalizedPubkey));
lastActivityAtRef.current = Date.now();
reevaluateAutomaticStatus();
}, [normalizedPubkey]);
React.useEffect(() => {
@@ -219,9 +240,8 @@ export function usePresenceSession(pubkey?: string) {
}, [normalizedPubkey, presencePreference]);
const recordActivity = React.useEffectEvent(() => {
const now = Date.now();
setLastActivityAt(now);
setStatusClock(now);
lastActivityAtRef.current = Date.now();
reevaluateAutomaticStatus();
});
React.useEffect(() => {
@@ -238,17 +258,16 @@ export function usePresenceSession(pubkey?: string) {
}
function handleFocus() {
setIsDocumentHidden(false);
recordActivity();
}
function handleVisibilityChange() {
const hidden = document.hidden;
setIsDocumentHidden(hidden);
if (!hidden) {
if (!document.hidden) {
recordActivity();
return;
}
// Going hidden can flip the automatic status to away.
reevaluateAutomaticStatus();
}
window.addEventListener("pointerdown", handleUserActivity, true);
@@ -270,23 +289,13 @@ export function usePresenceSession(pubkey?: string) {
}
const intervalId = window.setInterval(() => {
setStatusClock(Date.now());
reevaluateAutomaticStatus();
}, PRESENCE_STATUS_TICK_INTERVAL_MS);
return () => {
window.clearInterval(intervalId);
};
}, [normalizedPubkey]);
const automaticStatus = React.useMemo(
() =>
resolveAutomaticPresenceStatus(
isDocumentHidden,
lastActivityAt,
statusClock,
),
[isDocumentHidden, lastActivityAt, statusClock],
);
const currentStatus =
normalizedPubkey.length === 0
? "offline"
@@ -305,12 +314,8 @@ export function usePresenceSession(pubkey?: string) {
status === "online" ? "auto" : status;
if (nextPreference === "auto") {
const now = Date.now();
setLastActivityAt(now);
setStatusClock(now);
setIsDocumentHidden(
typeof document === "undefined" ? false : document.hidden,
);
lastActivityAtRef.current = Date.now();
reevaluateAutomaticStatus();
}
setPresencePreference(nextPreference);

View File

@@ -490,7 +490,19 @@ export function useUpdateProfileMutation() {
return useMutation({
mutationFn: (input: UpdateProfileInput) => updateProfile(input),
onSuccess: (profile: Profile) => {
onMutate: async () => {
// Discard any in-flight profile fetch: a background refetch started
// before the update (e.g. by a route mount) can resolve AFTER
// onSuccess writes the fresh profile below and clobber it with the
// pre-update snapshot — the avatar/name then silently reverts until
// some later refetch. (Masked historically by users-batch refetch
// churn from per-keystroke app renders; exposed when those renders
// were removed.)
await queryClient.cancelQueries({ queryKey: profileQueryKey });
},
onSuccess: async (profile: Profile) => {
// Cancel again: a refetch may have started while mutationFn awaited.
await queryClient.cancelQueries({ queryKey: profileQueryKey });
queryClient.setQueryData(profileQueryKey, profile);
const relayUrl = activeWorkspace?.relayUrl ?? "";
const pubkey = identityQuery.data?.pubkey ?? profile.pubkey;