📉 perf: start message queries before file map loads (#14188)

* feat(client): remove chat message waterfalls

* fix(client): guard sends during conversation detail loads

* fix(client): observe new chat message cache

* fix(client): preserve streaming messages during prefetch

* fix(client): keep pending regeneration during prefetch

* fix(client): refresh stale pending chat tails

* fix(client): skip message prefetch during active streams

* fix(client): clear observed message caches

* fix(client): support mocked message cache clients

* fix(client): preserve replacement pending tails during message prefetch

* perf(client): start message queries before file map loads
This commit is contained in:
Ravi Kumar L 2026-07-12 13:54:32 +02:00 committed by GitHub
parent 329ed48246
commit 55451ee75d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 470 additions and 35 deletions

View file

@ -1,14 +1,12 @@
/* eslint-disable @typescript-eslint/no-require-imports */
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import userEvent from '@testing-library/user-event';
import { MemoryRouter, useNavigate } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { RecoilRoot } from 'recoil';
import type t from 'librechat-data-provider';
import { render, screen, waitFor } from '@testing-library/react';
import { Constants, EModelEndpoint } from 'librechat-data-provider';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type t from 'librechat-data-provider';
import AgentDetail from '../AgentDetail';
// Mock dependencies
@ -148,6 +146,7 @@ describe('AgentDetail', () => {
(useQueryClient as jest.Mock).mockReturnValue({
getQueryData: jest.fn(),
setQueryData: jest.fn(),
removeQueries: jest.fn(),
invalidateQueries: jest.fn(),
});
@ -232,6 +231,7 @@ describe('AgentDetail', () => {
const mockQueryClient = {
getQueryData: jest.fn().mockReturnValue(null),
setQueryData: jest.fn(),
removeQueries: jest.fn(),
invalidateQueries: jest.fn(),
};

View file

@ -59,7 +59,7 @@ function ChatView({ index = 0, project }: { index?: number; project?: TChatProje
},
[fileMap],
),
enabled: !!fileMap,
enabled: !!conversationId && conversationId !== Constants.SEARCH,
},
{ isStreaming: isSubmitting },
);

View file

@ -31,6 +31,24 @@ type MessageRenderProps = {
'currentEditId' | 'setCurrentEditId' | 'siblingIdx' | 'setSiblingIdx' | 'siblingCount'
>;
export function areMessageFilesEqual(
prevFiles: TMessage['files'],
nextFiles: TMessage['files'],
): boolean {
if (prevFiles === nextFiles) {
return true;
}
const prevLength = prevFiles?.length ?? 0;
const nextLength = nextFiles?.length ?? 0;
if (prevLength !== nextLength) {
return false;
}
if (prevLength === 0) {
return true;
}
return prevFiles?.every((file, index) => file === nextFiles?.[index]) ?? true;
}
/**
* Custom comparator for React.memo: compares `message` by key fields instead of reference
* because `buildTree` creates new message objects on every streaming update for ALL messages,
@ -82,7 +100,7 @@ function areMessageRenderPropsEqual(prev: MessageRenderProps, next: MessageRende
prevMsg.endpoint === nextMsg.endpoint &&
prevMsg.iconURL === nextMsg.iconURL &&
prevMsg.feedback?.rating === nextMsg.feedback?.rating &&
(prevMsg.files?.length ?? 0) === (nextMsg.files?.length ?? 0) &&
areMessageFilesEqual(prevMsg.files, nextMsg.files) &&
(prevMsg.quotes?.length ?? 0) === (nextMsg.quotes?.length ?? 0)
);
}

View file

@ -0,0 +1,42 @@
import type { TFile } from 'librechat-data-provider';
import { areMessageFilesEqual } from '../MessageRender';
const file = (overrides: Partial<TFile> = {}): TFile =>
({
file_id: 'file-1',
filename: 'sample.pdf',
filepath: '/uploads/sample.pdf',
type: 'application/pdf',
bytes: 100,
embedded: false,
object: 'file',
usage: 1,
user: 'user-1',
...overrides,
}) as TFile;
describe('areMessageFilesEqual', () => {
it('detects when a raw message file is replaced by its hydrated file-map entry', () => {
const rawFile = file({ filename: 'raw.pdf', preview: undefined });
const hydratedFile = file({ filename: 'hydrated.pdf', preview: '/previews/sample.png' });
expect(areMessageFilesEqual([rawFile], [hydratedFile])).toBe(false);
});
it('keeps equivalent file entries memoized when buildTree creates a new array', () => {
const hydratedFile = file({ preview: '/previews/sample.png' });
expect(areMessageFilesEqual([hydratedFile], [hydratedFile])).toBe(true);
});
it('detects attachment additions and removals', () => {
const hydratedFile = file();
expect(areMessageFilesEqual([], [hydratedFile])).toBe(false);
expect(areMessageFilesEqual([hydratedFile], [])).toBe(false);
});
it('treats absent and empty file lists as equivalent', () => {
expect(areMessageFilesEqual(undefined, [])).toBe(true);
});
});