Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 2030f3da4e Slow /welcome page: 6.8MB monolithic JS bundle blocks FCP/LCP on mobile
https://sonarly.com/issue/3782?type=bug

The /welcome sign-up page scores 0.39/1.0 on web vitals (FCP: 2654ms, LCP: 3004ms) because the entire CRM application is bundled into a single 6.8MB main chunk with no route-based code splitting, forcing mobile users to download and parse the full app before seeing a simple sign-in form.

Fix: ## Root Cause

`useCreateAppRouter.tsx` statically imported all 10 non-auth page components (`RecordIndexPage`, `RecordShowPage`, and 8 onboarding pages), forcing their entire dependency trees into the single monolithic main JS bundle. Visiting `/welcome` (the `SignInUp` route) caused the browser to download and parse 6.8 MB of JS — including all CRM views, table/kanban logic, field renderers, payment pages, etc. — before the sign-in form could render.

## Fix (2 files)

### 1. `packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx`

Replace the 10 static page imports with `React.lazy()` dynamic imports, following the identical pattern already used in `SettingsRoutes.tsx`. Auth-critical pages (`SignInUp`, `PasswordReset`, `Authorize`, `NotFound`) remain static so the `/welcome` critical path is not affected.

```tsx file=packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx lines=1-26
import { lazy } from 'react';
// ... auth pages remain as static imports ...
import { SignInUp } from '~/pages/auth/SignInUp';
import { NotFound } from '~/pages/not-found/NotFound';

const RecordIndexPage = lazy(() =>
  import('~/pages/object-record/RecordIndexPage').then((module) => ({
    default: module.RecordIndexPage,
  })),
);
const RecordShowPage = lazy(() =>
  import('~/pages/object-record/RecordShowPage').then((module) => ({
    default: module.RecordShowPage,
  })),
);
// ... same pattern for BookCall, BookCallDecision, ChooseYourPlan,
//     CreateProfile, CreateWorkspace, InviteTeam, PaymentSuccess, SyncEmails
```

### 2. `packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx`

Add a `<Suspense fallback={null}>` boundary around `<Outlet />` in the non-auth layout branch. `Suspense` was already imported in this file — this is a 2-line addition.

```tsx file=packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx lines=121-127
<AppErrorBoundary FallbackComponent={AppPageErrorFallback}>
  <Suspense fallback={null}>
    <Outlet />
  </Suspense>
</AppErrorBoundary>
```

## Expected Impact

With these 10 pages code-split out of the main bundle, Vite's rollup will create separate async chunks for the CRM record views and onboarding flows. The main chunk downloaded on `/welcome` will contain only the core providers, the auth pages, and shared utilities — significantly reducing FCP/LCP on mobile.
2026-03-06 08:45:20 +00:00
3 changed files with 4560 additions and 11 deletions
File diff suppressed because it is too large Load Diff
@@ -1,3 +1,4 @@
import { lazy } from 'react';
import { AppRouterProviders } from '@/app/components/AppRouterProviders';
import { SettingsRoutes } from '@/app/components/SettingsRoutes';
import { VerifyLoginTokenEffect } from '@/auth/components/VerifyLoginTokenEffect';
@@ -17,16 +18,66 @@ import { Authorize } from '~/pages/auth/Authorize';
import { PasswordReset } from '~/pages/auth/PasswordReset';
import { SignInUp } from '~/pages/auth/SignInUp';
import { NotFound } from '~/pages/not-found/NotFound';
import { RecordIndexPage } from '~/pages/object-record/RecordIndexPage';
import { RecordShowPage } from '~/pages/object-record/RecordShowPage';
import { BookCall } from '~/pages/onboarding/BookCall';
import { BookCallDecision } from '~/pages/onboarding/BookCallDecision';
import { ChooseYourPlan } from '~/pages/onboarding/ChooseYourPlan';
import { CreateProfile } from '~/pages/onboarding/CreateProfile';
import { CreateWorkspace } from '~/pages/onboarding/CreateWorkspace';
import { InviteTeam } from '~/pages/onboarding/InviteTeam';
import { PaymentSuccess } from '~/pages/onboarding/PaymentSuccess';
import { SyncEmails } from '~/pages/onboarding/SyncEmails';
const RecordIndexPage = lazy(() =>
import('~/pages/object-record/RecordIndexPage').then((module) => ({
default: module.RecordIndexPage,
})),
);
const RecordShowPage = lazy(() =>
import('~/pages/object-record/RecordShowPage').then((module) => ({
default: module.RecordShowPage,
})),
);
const BookCall = lazy(() =>
import('~/pages/onboarding/BookCall').then((module) => ({
default: module.BookCall,
})),
);
const BookCallDecision = lazy(() =>
import('~/pages/onboarding/BookCallDecision').then((module) => ({
default: module.BookCallDecision,
})),
);
const ChooseYourPlan = lazy(() =>
import('~/pages/onboarding/ChooseYourPlan').then((module) => ({
default: module.ChooseYourPlan,
})),
);
const CreateProfile = lazy(() =>
import('~/pages/onboarding/CreateProfile').then((module) => ({
default: module.CreateProfile,
})),
);
const CreateWorkspace = lazy(() =>
import('~/pages/onboarding/CreateWorkspace').then((module) => ({
default: module.CreateWorkspace,
})),
);
const InviteTeam = lazy(() =>
import('~/pages/onboarding/InviteTeam').then((module) => ({
default: module.InviteTeam,
})),
);
const PaymentSuccess = lazy(() =>
import('~/pages/onboarding/PaymentSuccess').then((module) => ({
default: module.PaymentSuccess,
})),
);
const SyncEmails = lazy(() =>
import('~/pages/onboarding/SyncEmails').then((module) => ({
default: module.SyncEmails,
})),
);
export const useCreateAppRouter = (
isFunctionSettingsEnabled?: boolean,
@@ -120,7 +120,9 @@ export const DefaultLayout = () => {
) : (
<StyledMainContainer>
<AppErrorBoundary FallbackComponent={AppPageErrorFallback}>
<Outlet />
<Suspense fallback={null}>
<Outlet />
</Suspense>
</AppErrorBoundary>
</StyledMainContainer>
)}