⚙️ refactor: lazy-load React Query Devtools (#13639)

* perf(client): lazy-load query devtools

* fix: keep query devtools deps lazy

* fix: address query devtools review findings

* fix: exclude query devtools from pwa precache

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
Ravi Kumar L 2026-06-10 13:06:20 -04:00 committed by GitHub
parent d91cec2101
commit 865e1da857
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 221 additions and 30 deletions

View file

@ -16,9 +16,11 @@ const {
isEnabled,
apiNotFound,
ErrorController,
QUERY_DEVTOOLS_HEADER,
performStartupChecks,
handleJsonParseError,
initializeFileStorage,
maybeInjectQueryDevtoolsBootstrap,
preAuthTenantMiddleware,
} = require('@librechat/api');
const { connectDb, indexSync } = require('~/db');
@ -315,6 +317,23 @@ if (cluster.isMaster) {
}
}
const sendIndexHtml = (req, res) => {
res.set({
'Cache-Control': process.env.INDEX_CACHE_CONTROL || 'no-cache, no-store, must-revalidate',
Pragma: process.env.INDEX_PRAGMA || 'no-cache',
Expires: process.env.INDEX_EXPIRES || '0',
});
res.vary(QUERY_DEVTOOLS_HEADER);
const lang = req.cookies.lang || req.headers['accept-language']?.split(',')[0] || 'en-US';
const saneLang = lang.replace(/"/g, '&quot;');
let updatedIndexHtml = indexHTML.replace(/lang="en-US"/g, `lang="${saneLang}"`);
updatedIndexHtml = maybeInjectQueryDevtoolsBootstrap(updatedIndexHtml, req);
res.type('html');
res.send(updatedIndexHtml);
};
/** Health check endpoint */
app.get('/health', (_req, res) => res.status(200).send('OK'));
@ -348,6 +367,7 @@ if (cluster.isMaster) {
logger.warn('Response compression has been disabled via DISABLE_COMPRESSION.');
}
app.get('/index.html', sendIndexHtml);
app.use(staticCache(appConfig.paths.dist));
app.use(staticCache(appConfig.paths.fonts));
app.use(staticCache(appConfig.paths.assets));
@ -406,20 +426,7 @@ if (cluster.isMaster) {
app.use('/api', apiNotFound);
/** SPA fallback - serve index.html for all unmatched routes */
app.use((req, res) => {
res.set({
'Cache-Control': process.env.INDEX_CACHE_CONTROL || 'no-cache, no-store, must-revalidate',
Pragma: process.env.INDEX_PRAGMA || 'no-cache',
Expires: process.env.INDEX_EXPIRES || '0',
});
const lang = req.cookies.lang || req.headers['accept-language']?.split(',')[0] || 'en-US';
const saneLang = lang.replace(/"/g, '&quot;');
let updatedIndexHtml = indexHTML.replace(/lang="en-US"/g, `lang="${saneLang}"`);
res.type('html');
res.send(updatedIndexHtml);
});
app.use(sendIndexHtml);
/** Error handler (must be last - Express identifies error middleware by its 4-arg signature) */
app.use(ErrorController);

View file

@ -19,9 +19,11 @@ const {
performStartupChecks,
handleJsonParseError,
GenerationJobManager,
QUERY_DEVTOOLS_HEADER,
createStreamServices,
initializeFileStorage,
initializeDeploymentSkills,
maybeInjectQueryDevtoolsBootstrap,
preAuthTenantMiddleware,
setupGracefulShutdown,
updateInterfacePermissions,
@ -140,6 +142,23 @@ const startServer = async () => {
}
}
const sendIndexHtml = (req, res) => {
res.set({
'Cache-Control': process.env.INDEX_CACHE_CONTROL || 'no-cache, no-store, must-revalidate',
Pragma: process.env.INDEX_PRAGMA || 'no-cache',
Expires: process.env.INDEX_EXPIRES || '0',
});
res.vary(QUERY_DEVTOOLS_HEADER);
const lang = req.cookies.lang || req.headers['accept-language']?.split(',')[0] || 'en-US';
const saneLang = lang.replace(/"/g, '&quot;');
let updatedIndexHtml = indexHTML.replace(/lang="en-US"/g, `lang="${saneLang}"`);
updatedIndexHtml = maybeInjectQueryDevtoolsBootstrap(updatedIndexHtml, req);
res.type('html');
res.send(updatedIndexHtml);
};
app.get('/health', (_req, res) => res.status(200).send('OK'));
app.get('/livez', (_req, res) => res.status(200).send('OK'));
app.get('/readyz', (_req, res) => {
@ -179,6 +198,7 @@ const startServer = async () => {
console.warn('Response compression has been disabled via DISABLE_COMPRESSION.');
}
app.get('/index.html', sendIndexHtml);
app.use(staticCache(appConfig.paths.dist));
app.use(staticCache(appConfig.paths.fonts));
app.use(staticCache(appConfig.paths.assets));
@ -256,20 +276,7 @@ const startServer = async () => {
app.use('/api', apiNotFound);
/** SPA fallback - serve index.html for all unmatched routes */
app.use((req, res) => {
res.set({
'Cache-Control': process.env.INDEX_CACHE_CONTROL || 'no-cache, no-store, must-revalidate',
Pragma: process.env.INDEX_PRAGMA || 'no-cache',
Expires: process.env.INDEX_EXPIRES || '0',
});
const lang = req.cookies.lang || req.headers['accept-language']?.split(',')[0] || 'en-US';
const saneLang = lang.replace(/"/g, '&quot;');
let updatedIndexHtml = indexHTML.replace(/lang="en-US"/g, `lang="${saneLang}"`);
res.type('html');
res.send(updatedIndexHtml);
});
app.use(sendIndexHtml);
/** Record trace errors before the final error controller. */
if (telemetry.enabled) {

View file

@ -212,6 +212,32 @@ describe('Server Configuration', () => {
expect(response.headers['content-type']).toMatch(/html/);
});
it('should gate React Query Devtools config in SPA HTML by debug header', async () => {
const defaultResponse = await request(app).get('/this/does/not/exist');
const debugResponse = await request(app)
.get('/this/does/not/exist')
.set('x-librechat-enable-query-devtools', '1');
const directIndexResponse = await request(app)
.get('/index.html')
.set('x-librechat-enable-query-devtools', '1');
expect(defaultResponse.status).toBe(200);
expect(defaultResponse.headers.vary).toContain('x-librechat-enable-query-devtools');
expect(defaultResponse.text).not.toContain('enableQueryDevtools');
expect(debugResponse.status).toBe(200);
expect(debugResponse.headers.vary).toContain('x-librechat-enable-query-devtools');
expect(debugResponse.text).toContain('window.__LIBRECHAT_CONFIG__');
expect(debugResponse.text).toContain('data-librechat-query-devtools="true"');
expect(debugResponse.text).toContain('"enableQueryDevtools":true');
expect(directIndexResponse.status).toBe(200);
expect(directIndexResponse.headers.vary).toContain('x-librechat-enable-query-devtools');
expect(directIndexResponse.text).toContain('window.__LIBRECHAT_CONFIG__');
expect(directIndexResponse.text).toContain('data-librechat-query-devtools="true"');
expect(directIndexResponse.text).toContain('"enableQueryDevtools":true');
});
it('should return 500 for unknown errors via ErrorController', async () => {
// Testing the error handling here on top of unit tests to ensure the middleware is correctly integrated

View file

@ -4,11 +4,11 @@ import { DndProvider } from 'react-dnd';
import { RouterProvider } from 'react-router-dom';
import * as RadixToast from '@radix-ui/react-toast';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { Toast, ThemeProvider, ToastProvider } from '@librechat/client';
import { QueryClient, QueryClientProvider, QueryCache } from '@tanstack/react-query';
import { ScreenshotProvider, useApiErrorBoundary } from './hooks';
import WakeLockManager from '~/components/System/WakeLockManager';
import QueryDevtoolsGate from '~/components/QueryDevtoolsGate';
import LanguageSync from '~/components/System/LanguageSync';
import { getThemeFromEnv } from './utils/getThemeFromEnv';
import { initializeFontSize } from '~/store/fontSize';
@ -65,7 +65,7 @@ const App = () => {
<DndProvider backend={HTML5Backend}>
<RouterProvider router={router} />
<WakeLockManager />
<ReactQueryDevtools initialIsOpen={false} position="top-right" />
<QueryDevtoolsGate />
<Toast />
<RadixToast.Viewport className="pointer-events-none fixed inset-0 z-[1000] mx-auto my-2 flex max-w-[560px] flex-col items-stretch justify-start md:pb-5" />
</DndProvider>

View file

@ -658,5 +658,8 @@ export type TThread = { id: string; createdAt: string };
declare global {
interface Window {
google_tag_manager?: unknown;
__LIBRECHAT_CONFIG__?: {
enableQueryDevtools?: boolean;
};
}
}

View file

@ -0,0 +1,33 @@
import { lazy, Suspense } from 'react';
interface QueryDevtoolsConfig {
enableQueryDevtools?: boolean;
}
interface QueryDevtoolsGateProps {
config?: QueryDevtoolsConfig;
isDevelopment?: boolean;
}
const LazyReactQueryDevtools = lazy(() =>
import('@tanstack/react-query-devtools/production').then(({ ReactQueryDevtools }) => ({
default: ReactQueryDevtools,
})),
);
export const shouldEnableQueryDevtools = ({
isDevelopment = import.meta.env.DEV,
config = typeof window === 'undefined' ? undefined : window.__LIBRECHAT_CONFIG__,
}: QueryDevtoolsGateProps = {}) => isDevelopment || config?.enableQueryDevtools === true;
export default function QueryDevtoolsGate({ isDevelopment, config }: QueryDevtoolsGateProps = {}) {
if (!shouldEnableQueryDevtools({ isDevelopment, config })) {
return null;
}
return (
<Suspense fallback={null}>
<LazyReactQueryDevtools initialIsOpen={false} position="top-right" />
</Suspense>
);
}

View file

@ -0,0 +1,29 @@
import { render, screen, waitFor } from '@testing-library/react';
import QueryDevtoolsGate, { shouldEnableQueryDevtools } from '../QueryDevtoolsGate';
jest.mock('@tanstack/react-query-devtools/production', () => ({
ReactQueryDevtools: () => <div data-testid="query-devtools" />,
}));
describe('QueryDevtoolsGate', () => {
it('keeps query devtools disabled in production by default', () => {
expect(shouldEnableQueryDevtools({ isDevelopment: false, config: undefined })).toBe(false);
const { container } = render(<QueryDevtoolsGate isDevelopment={false} config={undefined} />);
expect(container).toBeEmptyDOMElement();
expect(screen.queryByTestId('query-devtools')).not.toBeInTheDocument();
});
it('enables query devtools in local development', async () => {
render(<QueryDevtoolsGate isDevelopment={true} config={undefined} />);
await waitFor(() => expect(screen.getByTestId('query-devtools')).toBeInTheDocument());
});
it('enables query devtools in production when the server-injected flag is true', async () => {
render(<QueryDevtoolsGate isDevelopment={false} config={{ enableQueryDevtools: true }} />);
await waitFor(() => expect(screen.getByTestId('query-devtools')).toBeInTheDocument());
});
});

View file

@ -34,6 +34,13 @@ const backendURL = process.env.HOST
? `http://${process.env.HOST}:${backendPort}`
: `http://localhost:${backendPort}`;
const buildSourceMap = process.env.NODE_ENV === 'development';
const QUERY_DEVTOOLS_CHUNK_MODULES = [
'@tanstack/react-query-devtools',
'@tanstack/match-sorter-utils',
'node_modules/superjson',
'node_modules/copy-anything',
'node_modules/is-what',
];
export default defineConfig(({ command }) => ({
base: '',
@ -89,6 +96,7 @@ export default defineConfig(({ command }) => ({
'index.html',
'assets/rum.*.js',
'assets/locale-*.js',
'assets/query-devtools*.js',
],
maximumFileSizeToCacheInBytes: 4 * 1024 * 1024,
/** LibreChat mutates index.html per request for subpath and language support. */
@ -311,6 +319,13 @@ export default defineConfig(({ command }) => ({
if (normalizedId.includes('node_modules/hast-util-raw')) {
return 'markdown_large';
}
if (
QUERY_DEVTOOLS_CHUNK_MODULES.some((moduleName) =>
normalizedId.includes(moduleName),
)
) {
return 'query-devtools';
}
if (normalizedId.includes('@tanstack')) {
return 'tanstack-vendor';
}

View file

@ -0,0 +1,34 @@
import type { QueryDevtoolsRequest } from './devtools';
import {
QUERY_DEVTOOLS_HEADER,
maybeInjectQueryDevtoolsBootstrap,
shouldEnableQueryDevtools,
} from './devtools';
const createReq = (value?: string): QueryDevtoolsRequest => ({
get: (header) => (header === QUERY_DEVTOOLS_HEADER ? value : undefined),
});
describe('query devtools HTML bootstrap', () => {
const html =
'<!DOCTYPE html><html lang="en-US"><head><title>LibreChat</title></head><body></body></html>';
it('uses the documented debug header value as the opt-in signal', () => {
expect(shouldEnableQueryDevtools(createReq('1'))).toBe(true);
expect(shouldEnableQueryDevtools(createReq('true'))).toBe(false);
expect(shouldEnableQueryDevtools(createReq())).toBe(false);
});
it('does not inject the production devtools flag by default', () => {
expect(maybeInjectQueryDevtoolsBootstrap(html, createReq())).toBe(html);
});
it('injects a server-to-client flag when the debug header is present', () => {
const updatedHtml = maybeInjectQueryDevtoolsBootstrap(html, createReq('1'));
expect(updatedHtml).toContain('window.__LIBRECHAT_CONFIG__');
expect(updatedHtml).toContain('data-librechat-query-devtools="true"');
expect(updatedHtml).toContain('"enableQueryDevtools":true');
expect(updatedHtml.indexOf('enableQueryDevtools')).toBeLessThan(updatedHtml.indexOf('</head>'));
});
});

View file

@ -0,0 +1,34 @@
export const QUERY_DEVTOOLS_HEADER = 'x-librechat-enable-query-devtools';
const QUERY_DEVTOOLS_SENTINEL = 'data-librechat-query-devtools="true"';
const QUERY_DEVTOOLS_BOOTSTRAP = `<script ${QUERY_DEVTOOLS_SENTINEL}>window.__LIBRECHAT_CONFIG__=Object.assign({},window.__LIBRECHAT_CONFIG__,{"enableQueryDevtools":true});</script>`;
export interface QueryDevtoolsRequest {
get(header: string): string | undefined;
}
export const shouldEnableQueryDevtools = (req: QueryDevtoolsRequest): boolean =>
req.get(QUERY_DEVTOOLS_HEADER) === '1';
const injectQueryDevtoolsBootstrap = (html: string): string => {
if (html.includes(QUERY_DEVTOOLS_SENTINEL)) {
return html;
}
if (html.includes('</head>')) {
return html.replace('</head>', `${QUERY_DEVTOOLS_BOOTSTRAP}</head>`);
}
return html.replace(/<body([^>]*)>/i, `<body$1>${QUERY_DEVTOOLS_BOOTSTRAP}`);
};
export const maybeInjectQueryDevtoolsBootstrap = (
html: string,
req: QueryDevtoolsRequest,
): string => {
if (!shouldEnableQueryDevtools(req)) {
return html;
}
return injectQueryDevtoolsBootstrap(html);
};

View file

@ -0,0 +1 @@
export * from './devtools';

View file

@ -25,6 +25,8 @@ export * from './utils';
export { default as Tokenizer, countTokens } from './utils/tokenizer';
export type { EncodingName } from './utils/tokenizer';
export * from './db/utils';
/* HTML */
export * from './html';
/* OAuth */
export * from './oauth';
export * from './mcp/oauth/OAuthReconnectionManager';