Improve lazy loading (#12393)

Creating manual chunk was a bad idea, we should always solve lazy
loading problem at the source instance.

Setting a 4.5MB for the index bundle size, CI will fail if we go above.

There is still a lot of room for optimizations!
- More agressive lazy loading (e.g. xyflow and tiptap are still loaded
in index!)
- Add a  prefetch mechanism
- Add stronger CI checks to make sure libraries we've set asides are not
added back
- Fix AllIcons component with does not work as intended (loaded on
initial load)
This commit is contained in:
Félix Malfait
2025-06-01 09:33:16 +02:00
committed by GitHub
parent c74d7fe986
commit f6bfec882a
37 changed files with 577 additions and 277 deletions

View File

@ -3,7 +3,7 @@ import { useMemo } from 'react';
import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
import { CountryCode } from 'libphonenumber-js';
import type { CountryCode } from 'libphonenumber-js';
import { IconCircleOff, IconComponentProps } from 'twenty-ui/display';
import { SelectOption } from 'twenty-ui/input';

View File

@ -1,6 +1,6 @@
import { useRichTextV2FieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRichTextV2FieldDisplay';
import { getFirstNonEmptyLineOfRichText } from '@/ui/input/editor/utils/getFirstNonEmptyLineOfRichText';
import { PartialBlock } from '@blocknote/core';
import type { PartialBlock } from '@blocknote/core';
import { isDefined, parseJson } from 'twenty-shared/utils';
export const RichTextV2FieldDisplay = () => {

View File

@ -9,7 +9,7 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText';
import { isFieldRichTextValue } from '@/object-record/record-field/types/guards/isFieldRichTextValue';
import { PartialBlock } from '@blocknote/core';
import type { PartialBlock } from '@blocknote/core';
import { isNonEmptyString } from '@sniptt/guards';
import { FieldContext } from '../../contexts/FieldContext';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';

View File

@ -5,7 +5,7 @@ import { useRecordFieldValue } from '@/object-record/record-store/contexts/Recor
import { FieldRichTextValue } from '@/object-record/record-field/types/FieldMetadata';
import { assertFieldMetadata } from '@/object-record/record-field/types/guards/assertFieldMetadata';
import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText';
import { PartialBlock } from '@blocknote/core';
import type { PartialBlock } from '@blocknote/core';
import { isDefined, parseJson } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldContext } from '../../contexts/FieldContext';

View File

@ -12,7 +12,7 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isFieldRichTextV2';
import { isFieldRichTextV2Value } from '@/object-record/record-field/types/guards/isFieldRichTextValueV2';
import { PartialBlock } from '@blocknote/core';
import type { PartialBlock } from '@blocknote/core';
import { isNonEmptyString } from '@sniptt/guards';
import { FieldContext } from '../../contexts/FieldContext';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';

View File

@ -1,4 +1,4 @@
import { ActivityRichTextEditor } from '@/activities/components/ActivityRichTextEditor';
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { useRichTextCommandMenu } from '@/command-menu/hooks/useRichTextCommandMenu';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
@ -8,11 +8,19 @@ import {
FieldInputEvent,
} from '@/object-record/record-field/types/FieldInputEvent';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRef } from 'react';
import { lazy, Suspense, useRef } from 'react';
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
import { IconLayoutSidebarLeftCollapse } from 'twenty-ui/display';
import { FloatingIconButton } from 'twenty-ui/input';
const ActivityRichTextEditor = lazy(() =>
import('@/activities/components/ActivityRichTextEditor').then((module) => ({
default: module.ActivityRichTextEditor,
})),
);
export type RichTextFieldInputProps = {
onClickOutside?: FieldInputClickOutsideEvent;
onCancel?: () => void;
@ -38,6 +46,19 @@ const StyledCollapseButton = styled.div`
display: flex;
`;
const LoadingSkeleton = () => {
const theme = useTheme();
return (
<SkeletonTheme
baseColor={theme.background.tertiary}
highlightColor={theme.background.transparent.lighter}
borderRadius={theme.border.radius.sm}
>
<Skeleton height={SKELETON_LOADER_HEIGHT_SIZES.standard.s} />
</SkeletonTheme>
);
};
export const RichTextFieldInput = ({
targetableObject,
onClickOutside,
@ -70,10 +91,12 @@ export const RichTextFieldInput = ({
return (
<StyledContainer ref={containerRef}>
<ActivityRichTextEditor
activityId={targetableObject.id}
activityObjectNameSingular={targetableObject.targetObjectNameSingular}
/>
<Suspense fallback={<LoadingSkeleton />}>
<ActivityRichTextEditor
activityId={targetableObject.id}
activityObjectNameSingular={targetableObject.targetObjectNameSingular}
/>
</Suspense>
<StyledCollapseButton>
<FloatingIconButton
Icon={IconLayoutSidebarLeftCollapse}

View File

@ -1,4 +1,5 @@
import { Calendar } from '@/activities/calendar/components/Calendar';
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
import { EmailThreads } from '@/activities/emails/components/EmailThreads';
import { Attachments } from '@/activities/files/components/Attachments';
import { Notes } from '@/activities/notes/components/Notes';
@ -10,16 +11,15 @@ import { CardType } from '@/object-record/record-show/types/CardType';
import { ListenRecordUpdatesEffect } from '@/subscription/components/ListenUpdatesEffect';
import { ShowPageActivityContainer } from '@/ui/layout/show-page/components/ShowPageActivityContainer';
import { getWorkflowVisualizerComponentInstanceId } from '@/workflow/utils/getWorkflowVisualizerComponentInstanceId';
import { WorkflowRunVisualizer } from '@/workflow/workflow-diagram/components/WorkflowRunVisualizer';
import { WorkflowRunVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect';
import { WorkflowVersionVisualizer } from '@/workflow/workflow-diagram/components/WorkflowVersionVisualizer';
import { WorkflowVersionVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect';
import { WorkflowVisualizer } from '@/workflow/workflow-diagram/components/WorkflowVisualizer';
import { WorkflowVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowVisualizerEffect';
import { WorkflowRunVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowRunVisualizerComponentInstanceContext';
import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useId } from 'react';
import { lazy, Suspense, useId } from 'react';
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
const StyledGreyBox = styled.div<{ isInRightDrawer?: boolean }>`
background: ${({ theme, isInRightDrawer }) =>
@ -34,6 +34,15 @@ const StyledGreyBox = styled.div<{ isInRightDrawer?: boolean }>`
isInRightDrawer ? theme.spacing(4) : ''};
`;
const StyledLoadingSkeletonContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
height: 100%;
padding: ${({ theme }) => theme.spacing(4)};
width: 100%;
`;
type CardComponentProps = {
targetableObject: Pick<
ActivityTargetableObject,
@ -44,6 +53,48 @@ type CardComponentProps = {
type CardComponentType = (props: CardComponentProps) => JSX.Element | null;
const LoadingSkeleton = () => {
const theme = useTheme();
return (
<StyledLoadingSkeletonContainer>
<SkeletonTheme
baseColor={theme.background.tertiary}
highlightColor={theme.background.transparent.lighter}
borderRadius={theme.border.radius.sm}
>
<Skeleton height={SKELETON_LOADER_HEIGHT_SIZES.standard.m} />
<Skeleton height={SKELETON_LOADER_HEIGHT_SIZES.standard.m} />
<Skeleton height={SKELETON_LOADER_HEIGHT_SIZES.standard.m} />
</SkeletonTheme>
</StyledLoadingSkeletonContainer>
);
};
const WorkflowVisualizer = lazy(() =>
import('@/workflow/workflow-diagram/components/WorkflowVisualizer').then(
(module) => ({
default: module.WorkflowVisualizer,
}),
),
);
const WorkflowVersionVisualizer = lazy(() =>
import(
'@/workflow/workflow-diagram/components/WorkflowVersionVisualizer'
).then((module) => ({
default: module.WorkflowVersionVisualizer,
})),
);
const WorkflowRunVisualizer = lazy(() =>
import('@/workflow/workflow-diagram/components/WorkflowRunVisualizer').then(
(module) => ({
default: module.WorkflowRunVisualizer,
}),
),
);
export const CardComponents: Record<CardType, CardComponentType> = {
[CardType.TimelineCard]: ({ targetableObject, isInRightDrawer }) => (
<TimelineActivities
@ -95,7 +146,9 @@ export const CardComponents: Record<CardType, CardComponentType> = {
}}
>
<WorkflowVisualizerEffect workflowId={targetableObject.id} />
<WorkflowVisualizer workflowId={targetableObject.id} />
<Suspense fallback={<LoadingSkeleton />}>
<WorkflowVisualizer workflowId={targetableObject.id} />
</Suspense>
</WorkflowVisualizerComponentInstanceContext.Provider>
);
},
@ -112,7 +165,9 @@ export const CardComponents: Record<CardType, CardComponentType> = {
<WorkflowVersionVisualizerEffect
workflowVersionId={targetableObject.id}
/>
<WorkflowVersionVisualizer workflowVersionId={targetableObject.id} />
<Suspense fallback={<LoadingSkeleton />}>
<WorkflowVersionVisualizer workflowVersionId={targetableObject.id} />
</Suspense>
</WorkflowVisualizerComponentInstanceContext.Provider>
);
},
@ -139,7 +194,9 @@ export const CardComponents: Record<CardType, CardComponentType> = {
recordId={targetableObject.id}
listenedFields={['status', 'output']}
/>
<WorkflowRunVisualizer workflowRunId={targetableObject.id} />
<Suspense fallback={<LoadingSkeleton />}>
<WorkflowRunVisualizer workflowRunId={targetableObject.id} />
</Suspense>
</WorkflowRunVisualizerComponentInstanceContext.Provider>
</WorkflowVisualizerComponentInstanceContext.Provider>
);