[Experiment] perf(front): cache-first currentUser bootstrap (#21532)

## Experiment — not for merge as-is

A perf experiment for discussion. Opening as a draft to gather feedback
and let CI run.

## Problem

On a warm (returning) load, the app gate
([`MinimalMetadataGater`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-front/src/modules/metadata-store/components/MinimalMetadataGater.tsx))
blocks first paint until **both** object/view metadata **and**
`currentUser` are ready.

The metadata store is already cache-first: it persists each entity
(including `status: 'up-to-date'`) to `localStorage` with `getOnInit`,
opens from cache, and revalidates in the background via collection
hashes. 👏

`currentUser` (and `currentWorkspace` / `currentWorkspaceMember` /
`currentUserWorkspace`) is **not** — it lives in an in-memory atom, so
every load fires a blocking `GetCurrentUser` round-trip before the gate
opens. That round-trip is the one remaining network hop on the warm-load
critical path; everything else the first screen needs is already in
`localStorage`.

## Approach

Generalize the pattern the metadata store already proves out, to the
user bootstrap — **without adding any new `useEffect`**:

- Persist the four bootstrap atoms (`currentUser`, `currentWorkspace`,
`currentWorkspaceMember`, `currentUserWorkspace`) via the existing
`createAtomState({ useLocalStorage, localStorageOptions: { getOnInit:
true } })`.
- The gate opens from cache on its own: the existing
`IsMinimalMetadataReadyEffect` already derives readiness from the
`currentUser` atom alongside metadata status, so persisting the atoms is
enough — no new effect.
- Keep firing `GetCurrentUser` (now `network-only`, no longer skipped
when a user is present) so it **revalidates in the background** and the
existing write-through effect updates the atoms with the fresh result.
- Clear the cached identity on sign-out by adding the four keys to
`clearSessionLocalStorageKeys` (already invoked by `clearSession`, which
then hard-reloads).

Net effect: warm loads no longer wait on `GetCurrentUser`; the shell
paints from cache and corrects within one round-trip. Cold loads (no
cache) are unchanged.

## Risks to validate

- **Permission staleness** — `currentUserWorkspace` carries
`objectsPermissions` / `permissionFlags`. Cache-first means a brief
stale-permission window before revalidation. Not a security boundary
(the server authorizes every request), but it can momentarily show a
menu item the user no longer has; worst case it 401s and corrects on the
next paint.
- **Feature-flag / workspace staleness** —
`currentWorkspace.featureFlags` may be one round-trip stale on warm
load.
- **`X-Schema-Version` header** — sourced from
`currentWorkspace.metadataVersion`; caching it actually makes the header
*consistent* with the already-cached metadata rather than absent, but
worth confirming against the server's mismatch handling.
- **Test isolation** — these atoms now persist; tests relying on the
default `null` could see cross-test leakage if `localStorage` isn't
reset. The directly-affected suites pass locally (`useAuth`,
`useDefaultHomePagePath`, `useSetNextOnboardingStatus`); CI's full run
is the real check.

## Validation

- [ ] Full CI (types/lint/unit) green
- [ ] Manual: throttle network, hard-reload a logged-in workspace,
confirm the shell paints before `GetCurrentUser` resolves and that fresh
data writes through
- [ ] Sign out → sign in as a different user on the same browser;
confirm no stale identity flashes
This commit is contained in:
Félix Malfait
2026-06-13 18:38:45 +02:00
committed by GitHub
parent 5d892bdfd0
commit f869ce87b1
7 changed files with 19 additions and 4 deletions
@@ -18,4 +18,6 @@ export type CurrentUser = Pick<
export const currentUserState = createAtomState<CurrentUser | null>({
key: 'currentUserState',
defaultValue: null,
useLocalStorage: true,
localStorageOptions: { getOnInit: true },
});
@@ -13,4 +13,6 @@ export const currentUserWorkspaceState =
createAtomState<CurrentUserWorkspace | null>({
key: 'currentUserWorkspaceState',
defaultValue: null,
useLocalStorage: true,
localStorageOptions: { getOnInit: true },
});
@@ -10,4 +10,6 @@ export const currentWorkspaceMemberState =
createAtomState<CurrentWorkspaceMember | null>({
key: 'currentWorkspaceMemberState',
defaultValue: null,
useLocalStorage: true,
localStorageOptions: { getOnInit: true },
});
@@ -54,4 +54,6 @@ export type CurrentWorkspace = Pick<
export const currentWorkspaceState = createAtomState<CurrentWorkspace | null>({
key: 'currentWorkspaceState',
defaultValue: null,
useLocalStorage: true,
localStorageOptions: { getOnInit: true },
});
@@ -5,6 +5,10 @@ const SESSION_KEYS_TO_CLEAR = [
'lastVisitedViewPerObjectMetadataItemState',
'ai/agentChatDraftsByThreadIdState',
'locale',
'currentUserState',
'currentWorkspaceState',
'currentWorkspaceMemberState',
'currentUserWorkspaceState',
];
export const clearSessionLocalStorageKeys = () => {
@@ -1,4 +1,5 @@
import { useHasAccessTokenPair } from '@/auth/hooks/useHasAccessTokenPair';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
import { useLoadMinimalMetadata } from '@/metadata-store/hooks/useLoadMinimalMetadata';
@@ -6,11 +7,13 @@ import { useLoadStaleMetadataEntities } from '@/metadata-store/hooks/useLoadStal
import { metadataLoadedVersionState } from '@/metadata-store/states/metadataLoadedVersionState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useEffect, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
export const MinimalMetadataLoadEffect = () => {
const hasAccessTokenPair = useHasAccessTokenPair();
const isCurrentUserLoaded = useAtomStateValue(isCurrentUserLoadedState);
const currentUser = useAtomStateValue(currentUserState);
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const metadataLoadedVersion = useAtomStateValue(metadataLoadedVersionState);
const [lastLoadedVersion, setLastLoadedVersion] = useState<number>(-1);
@@ -22,7 +25,7 @@ export const MinimalMetadataLoadEffect = () => {
const shouldLoadRealMetadata = hasAccessTokenPair && isActiveWorkspace;
useEffect(() => {
if (!isCurrentUserLoaded) {
if (!isCurrentUserLoaded && !isDefined(currentUser)) {
return;
}
@@ -47,6 +50,7 @@ export const MinimalMetadataLoadEffect = () => {
performLoad();
}, [
isCurrentUserLoaded,
currentUser,
shouldLoadRealMetadata,
lastLoadedVersion,
metadataLoadedVersion,
@@ -9,7 +9,6 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
import { useInitializeFormatPreferences } from '@/localization/hooks/useInitializeFormatPreferences';
import { getDateFnsLocale } from '@/ui/field/display/utils/getDateFnsLocale';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { type ColorScheme } from '@/workspace-member/types/WorkspaceMember';
import { enUS } from 'date-fns/locale';
@@ -28,7 +27,6 @@ import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
export const UserMetadataProviderInitialEffect = () => {
const hasAccessTokenPair = useHasAccessTokenPair();
const currentUser = useAtomStateValue(currentUserState);
const store = useStore();
const [isInitialized, setIsInitialized] = useState(false);
@@ -65,12 +63,13 @@ export const UserMetadataProviderInitialEffect = () => {
[store],
);
const shouldSkipUserQuery = !hasAccessTokenPair || isDefined(currentUser);
const shouldSkipUserQuery = !hasAccessTokenPair;
const { data: userQueryData, loading: userQueryLoading } = useQuery(
GetCurrentUserDocument,
{
skip: shouldSkipUserQuery,
fetchPolicy: 'network-only',
},
);