perf: reduce chat rerenders during streaming
Stabilizes unchanged chat turns while messages stream Keeps static chat history from rerendering unnecessarily Adds coverage for turn record reuse
This commit is contained in:
@@ -86,4 +86,36 @@ describe('projectTurnRecords', () => {
|
||||
expect(projection.turns).toHaveLength(0);
|
||||
expect(projection.ungroupedMessageIds.has('s1')).toBe(true);
|
||||
});
|
||||
|
||||
test('reuses unchanged turn records from the previous projection', () => {
|
||||
const user1 = createMessageEntry({ id: 'u1', role: 'user', createdAt: 1 });
|
||||
const assistant1 = createMessageEntry({ id: 'a1', role: 'assistant', parentID: 'u1', createdAt: 2 });
|
||||
const user2 = createMessageEntry({ id: 'u2', role: 'user', createdAt: 3 });
|
||||
const assistant2 = createMessageEntry({ id: 'a2', role: 'assistant', parentID: 'u2', createdAt: 4 });
|
||||
const initial = projectTurnRecords([user1, assistant1, user2, assistant2]);
|
||||
const updatedAssistant2 = {
|
||||
...assistant2,
|
||||
parts: [{ type: 'text', text: 'stream update' } as Part],
|
||||
};
|
||||
|
||||
const next = projectTurnRecords([user1, assistant1, user2, updatedAssistant2], {
|
||||
previousProjection: initial,
|
||||
});
|
||||
|
||||
expect(next.turns[0]).toBe(initial.turns[0]);
|
||||
expect(next.turns[1]).not.toBe(initial.turns[1]);
|
||||
});
|
||||
|
||||
test('reuses the whole turns array when every turn is unchanged', () => {
|
||||
const user = createMessageEntry({ id: 'u1', role: 'user', createdAt: 1 });
|
||||
const assistant = createMessageEntry({ id: 'a1', role: 'assistant', parentID: 'u1', createdAt: 2 });
|
||||
const initial = projectTurnRecords([user, assistant]);
|
||||
|
||||
const next = projectTurnRecords([user, assistant], {
|
||||
previousProjection: initial,
|
||||
});
|
||||
|
||||
expect(next.turns).toBe(initial.turns);
|
||||
expect(next.turns[0]).toBe(initial.turns[0]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -90,6 +90,61 @@ const DEFAULT_OPTIONS: ProjectTurnRecordsOptions = {
|
||||
showTextJustificationActivity: false,
|
||||
};
|
||||
|
||||
const areSameMessageRefs = (left: ChatMessageEntry[], right: ChatMessageEntry[]): boolean => {
|
||||
if (left === right) {
|
||||
return true;
|
||||
}
|
||||
if (left.length !== right.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let index = 0; index < left.length; index += 1) {
|
||||
if (left[index] !== right[index]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const canReusePreviousTurn = (previous: TurnRecord, next: TurnRecord): boolean => {
|
||||
return previous.userMessage === next.userMessage
|
||||
&& previous.headerMessageId === next.headerMessageId
|
||||
&& areSameMessageRefs(previous.assistantMessages, next.assistantMessages);
|
||||
};
|
||||
|
||||
const stabilizeTurnRecords = (
|
||||
turns: TurnRecord[],
|
||||
previousProjection?: TurnProjectionResult | null,
|
||||
): TurnRecord[] => {
|
||||
if (!previousProjection || previousProjection.turns.length === 0 || turns.length === 0) {
|
||||
return turns;
|
||||
}
|
||||
|
||||
let canReuseTurnArray = previousProjection.turns.length === turns.length;
|
||||
let reusedAnyTurn = false;
|
||||
|
||||
const nextTurns = turns.map((turn, index) => {
|
||||
const previousTurn = previousProjection.indexes.turnById.get(turn.turnId);
|
||||
if (previousTurn && canReusePreviousTurn(previousTurn, turn)) {
|
||||
reusedAnyTurn = true;
|
||||
if (previousProjection.turns[index] !== previousTurn) {
|
||||
canReuseTurnArray = false;
|
||||
}
|
||||
return previousTurn;
|
||||
}
|
||||
|
||||
canReuseTurnArray = false;
|
||||
return turn;
|
||||
});
|
||||
|
||||
if (canReuseTurnArray && reusedAnyTurn) {
|
||||
return previousProjection.turns;
|
||||
}
|
||||
|
||||
return reusedAnyTurn ? nextTurns : turns;
|
||||
};
|
||||
|
||||
export const projectTurnRecords = (
|
||||
messages: ChatMessageEntry[],
|
||||
options?: Partial<ProjectTurnRecordsOptions>,
|
||||
@@ -179,7 +234,8 @@ export const projectTurnRecords = (
|
||||
turn.durationMs = turn.stream.durationMs;
|
||||
});
|
||||
|
||||
const projection = projectTurnIndexes(turns);
|
||||
const stableTurns = stabilizeTurnRecords(turns, effectiveOptions.previousProjection);
|
||||
const projection = projectTurnIndexes(stableTurns);
|
||||
const ungroupedMessageIds = new Set<string>();
|
||||
messages.forEach((message) => {
|
||||
if (resolveMessageRole(message) === 'assistant') {
|
||||
|
||||
Reference in New Issue
Block a user