perf(ai): lazy-load agent chat runtime so it doesn't fetch/diff threads until opened (#21331)

## Problem

On workspaces with a sizeable AI chat history, the whole app was
freezing during navigation, including Settings (one navigation click
measured ~6.5s).

## Root cause

`AgentChatProvider` is mounted app-wide in `AppRouterProviders`, so its
effects run on every page.
On every render it would:
1. auto-select the most recently active thread
(`AgentChatThreadInitializationEffect`),
2. fetch that thread's **full message history**
(`AgentChatMessagesFetchEffect`),
3. run `AgentChatStreamingPartsDiffSyncEffect` →
`updateStreamingPartsWithDiff`, which loops over every message doing
`isDeeplyEqual(existing, incoming)` + `structuredClone`.

A large thread would produce multi-second freeze on every interaction,
app-wide. (Confirmed via a Chrome CPU profile)

 ## Fix

Don't run the agent-chat **message runtime** until the chat is actually
opened.

## Note
There is still room for improvement, opening AI chats would still be
very slow.
This commit is contained in:
Weiko
2026-06-09 11:10:43 +02:00
committed by GitHub
parent 4305a7dc84
commit 55cbd3bfbf
3 changed files with 56 additions and 11 deletions

View File

@@ -1,9 +1,4 @@
import { AgentChatStreamSubscriptionEffect } from '@/ai/components/AgentChatStreamSubscriptionEffect';
import { AgentChatMessagesFetchEffect } from '@/ai/components/AgentChatMessagesFetchEffect';
import { AgentChatSessionStartTimeEffect } from '@/ai/components/AgentChatSessionStartTimeEffect';
import { AgentChatStreamingAutoScrollEffect } from '@/ai/components/AgentChatStreamingAutoScrollEffect';
import { AgentChatStreamingPartsDiffSyncEffect } from '@/ai/components/AgentChatStreamingPartsDiffSyncEffect';
import { AgentChatRuntimeEffects } from '@/ai/components/AgentChatRuntimeEffects';
import { AgentChatThreadInitializationEffect } from '@/ai/components/AgentChatThreadInitializationEffect';
import { AgentChatComponentInstanceContext } from '@/ai/contexts/AgentChatComponentInstanceContext';
import { Suspense } from 'react';
@@ -19,11 +14,7 @@ export const AgentChatProviderContent = ({
value={{ instanceId: 'agentChatComponentInstance' }}
>
<AgentChatThreadInitializationEffect />
<AgentChatMessagesFetchEffect />
<AgentChatStreamSubscriptionEffect />
<AgentChatStreamingPartsDiffSyncEffect />
<AgentChatSessionStartTimeEffect />
<AgentChatStreamingAutoScrollEffect />
<AgentChatRuntimeEffects />
{children}
</AgentChatComponentInstanceContext.Provider>
</Suspense>

View File

@@ -0,0 +1,48 @@
import { AgentChatMessagesFetchEffect } from '@/ai/components/AgentChatMessagesFetchEffect';
import { AgentChatSessionStartTimeEffect } from '@/ai/components/AgentChatSessionStartTimeEffect';
import { AgentChatStreamSubscriptionEffect } from '@/ai/components/AgentChatStreamSubscriptionEffect';
import { AgentChatStreamingAutoScrollEffect } from '@/ai/components/AgentChatStreamingAutoScrollEffect';
import { AgentChatStreamingPartsDiffSyncEffect } from '@/ai/components/AgentChatStreamingPartsDiffSyncEffect';
import { hasAgentChatBeenOpenedState } from '@/ai/states/hasAgentChatBeenOpenedState';
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useEffect } from 'react';
import { SidePanelPages } from 'twenty-shared/types';
export const AgentChatRuntimeEffects = () => {
const isSidePanelOpened = useAtomStateValue(isSidePanelOpenedState);
const sidePanelPage = useAtomStateValue(sidePanelPageState);
const [hasAgentChatBeenOpened, setHasAgentChatBeenOpened] = useAtomState(
hasAgentChatBeenOpenedState,
);
const isAgentChatOpen =
isSidePanelOpened && sidePanelPage === SidePanelPages.AskAI;
useEffect(() => {
if (isAgentChatOpen && !hasAgentChatBeenOpened) {
setHasAgentChatBeenOpened(true);
}
}, [isAgentChatOpen, hasAgentChatBeenOpened, setHasAgentChatBeenOpened]);
if (!hasAgentChatBeenOpened) {
return null;
}
return (
<>
<AgentChatMessagesFetchEffect />
<AgentChatStreamSubscriptionEffect />
<AgentChatSessionStartTimeEffect />
{isAgentChatOpen && (
<>
<AgentChatStreamingPartsDiffSyncEffect />
<AgentChatStreamingAutoScrollEffect />
</>
)}
</>
);
};

View File

@@ -0,0 +1,6 @@
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
export const hasAgentChatBeenOpenedState = createAtomState<boolean>({
key: 'ai/hasAgentChatBeenOpenedState',
defaultValue: false,
});