[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:
@@ -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 = () => {
|
||||
|
||||
+5
-1
@@ -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,
|
||||
|
||||
+2
-3
@@ -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',
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user