Switch key areas to lazily import to improve Vite chunk size (#6278)

# Description of Changes
Vite currently warns that when it's bundling our code that the chunk
size is way too high because most of the imports are static so it can't
split them into smaller chunks. This PR changes a few key areas to use
lazy imports to try and make the chunks as small as possible with
minimal code changes.

Vite's warnings kick in at minified chunks being >500kB, and we've got a
little way to go still to reach that, but we can keep chipping away at
this and I'd rather get the biggest wins done now. I've also included
Lighthouse scores because there's been discussion about improving ours
recently. It's not the aim of this PR to improve it, but it's nice that
it makes it a little better.

## Current main chunks

Build split into 12 chunks. Largest chunk in build is:

```
[frontend:build] dist/assets/index-B6JiWDxZ.js               5,175.51 kB │ gzip: 1,495.85 kB
```

<img width="1442" height="775" alt="image"
src="https://github.com/user-attachments/assets/b0e8a3fa-4ef3-4ccd-8c1d-bfed2d99bd27"
/>

Lighthouse score:

<img width="423" height="146" alt="before"
src="https://github.com/user-attachments/assets/c62056e8-2e77-49a6-a1ae-f08ec8021fb3"
/>

## This PR's chunks

Build split into 176 chunks. Largest chunk in build is:

```
[frontend:build] dist/assets/index-qCgeCY4B.js                              2,878.54 kB │ gzip:   861.03 kB
```

<img width="1447" height="776" alt="image"
src="https://github.com/user-attachments/assets/8d0c3cf0-cc25-41c3-b114-4940d3e99349"
/>

Lighthouse score:

<img width="402" height="145" alt="after"
src="https://github.com/user-attachments/assets/99a26eb3-bd15-4b92-bf22-82b58b458f52"
/>

---------

Co-authored-by: EthanHealy01 <[email protected]>
This commit is contained in:
James Brunton
2026-05-01 15:21:06 +00:00
committed by GitHub
co-authored by EthanHealy01
parent 51f5345151
commit 3fe8adc5cb
11 changed files with 620 additions and 189 deletions
@@ -1,5 +1,5 @@
import { useCallback } from "react";
import { Box } from "@mantine/core";
import { Suspense, lazy, useCallback } from "react";
import { Box, Loader, Center } from "@mantine/core";
import { useRainbowThemeContext } from "@app/components/shared/RainbowThemeProvider";
import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext";
import { useFileHandler } from "@app/hooks/useFileHandler";
@@ -16,14 +16,20 @@ import { FileId } from "@app/types/file";
import styles from "@app/components/layout/Workbench.module.css";
import TopControls from "@app/components/shared/TopControls";
import FileEditor from "@app/components/fileEditor/FileEditor";
import PageEditor from "@app/components/pageEditor/PageEditor";
import PageEditorControls from "@app/components/pageEditor/PageEditorControls";
import Viewer from "@app/components/viewer/Viewer";
import LandingPage from "@app/components/shared/LandingPage";
import Footer from "@app/components/shared/Footer";
import DismissAllErrorsButton from "@app/components/shared/DismissAllErrorsButton";
// Workbench panels are loaded on demand. Viewer pulls in pdfjs-dist and the
// full @embedpdf plugin set; FileEditor/PageEditor are only needed once a file
// is open. Lazy-loading keeps all of that out of the initial bundle.
const FileEditor = lazy(() => import("@app/components/fileEditor/FileEditor"));
const PageEditor = lazy(() => import("@app/components/pageEditor/PageEditor"));
const PageEditorControls = lazy(
() => import("@app/components/pageEditor/PageEditorControls"),
);
const Viewer = lazy(() => import("@app/components/viewer/Viewer"));
// No props needed - component uses contexts directly
export default function Workbench() {
const { isRainbowMode } = useRainbowThemeContext();
@@ -236,7 +242,15 @@ export default function Workbench() {
...(currentView === "pageEditor" && { height: 0 }),
}}
>
{renderMainContent()}
<Suspense
fallback={
<Center style={{ height: "100%" }}>
<Loader />
</Center>
}
>
{renderMainContent()}
</Suspense>
</Box>
<Footer
@@ -0,0 +1,32 @@
import { Suspense, lazy, useEffect, useState } from "react";
// AppConfigModal pulls in the entire settings UI tree (admin sections,
// account, supabase auth flows, etc.). We defer loading until the user first
// opens the modal, then keep it mounted so the close animation runs.
const AppConfigModal = lazy(
() => import("@app/components/shared/AppConfigModal"),
);
interface AppConfigModalLazyProps {
opened: boolean;
onClose: () => void;
}
export default function AppConfigModalLazy({
opened,
onClose,
}: AppConfigModalLazyProps) {
const [shouldMount, setShouldMount] = useState(false);
useEffect(() => {
if (opened) setShouldMount(true);
}, [opened]);
if (!shouldMount) return null;
return (
<Suspense fallback={null}>
<AppConfigModal opened={opened} onClose={onClose} />
</Suspense>
);
}
@@ -30,7 +30,7 @@ import "@app/components/shared/quickAccessBar/QuickAccessBar.css";
import { Tooltip } from "@app/components/shared/Tooltip";
import AllToolsNavButton from "@app/components/shared/AllToolsNavButton";
import ActiveToolButton from "@app/components/shared/quickAccessBar/ActiveToolButton";
import AppConfigModal from "@app/components/shared/AppConfigModal";
import AppConfigModal from "@app/components/shared/AppConfigModalLazy";
import { useAppConfig } from "@app/contexts/AppConfigContext";
import { useGroupSigningEnabled } from "@app/hooks/useGroupSigningEnabled";
import { useSharingEnabled } from "@app/hooks/useSharingEnabled";
@@ -1,4 +1,4 @@
import { useState, useEffect, useCallback, useRef } from "react";
import { lazy, useState, useEffect, useCallback, useRef } from "react";
import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next";
import { Drawer } from "@mantine/core";
@@ -23,8 +23,15 @@ import {
import { useFileSelection } from "@app/contexts/file/fileHooks";
import { fileStorage } from "@app/services/fileStorage";
import { useFileActions } from "@app/contexts/FileContext";
import SignRequestWorkbenchView from "@app/components/tools/certSign/SignRequestWorkbenchView";
import SessionDetailWorkbenchView from "@app/components/tools/certSign/SessionDetailWorkbenchView";
// These workbench views pull in the PDF viewer / pdfium / @embedpdf chain, so
// they are loaded on demand when the certSign collab feature actually opens
// one of them. Workbench wraps custom views in <Suspense>.
const SignRequestWorkbenchView = lazy(
() => import("@app/components/tools/certSign/SignRequestWorkbenchView"),
);
const SessionDetailWorkbenchView = lazy(
() => import("@app/components/tools/certSign/SessionDetailWorkbenchView"),
);
import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from "@app/styles/zIndex";
export const SIGN_REQUEST_WORKBENCH_TYPE =
@@ -1,6 +1,15 @@
import { useState, useEffect } from "react";
import { Suspense, useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Modal, Title, Button, Group, Stack, Text, Alert } from "@mantine/core";
import {
Modal,
Title,
Button,
Group,
Stack,
Text,
Alert,
Loader,
} from "@mantine/core";
import { Z_INDEX_AUTOMATE_MODAL } from "@app/styles/zIndex";
import SettingsIcon from "@mui/icons-material/Settings";
import CheckIcon from "@mui/icons-material/Check";
@@ -124,7 +133,9 @@ export default function ToolConfigurationModal({
<div
style={{ maxHeight: "60vh", overflowY: "auto", overflowX: "hidden" }}
>
{renderToolSettings()}
<Suspense fallback={<Loader size="sm" />}>
{renderToolSettings()}
</Suspense>
</div>
<Group justify="flex-end" gap="sm">