From 640ecca9ca34105e29662dc59e5f018ebd175c3c Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Thu, 2 Oct 2025 12:22:19 +0200 Subject: [PATCH] perf: Reduce upfront component loading (#10285) * Reducing loading on first open, closes #10263 * perf: Prosemirror deps loaded with Document model * More initial component reduction * more * refactor --- app/actions/definitions/collections.tsx | 5 +- app/actions/definitions/documents.tsx | 24 ++++++--- app/components/AuthenticatedLayout.tsx | 11 ++--- app/components/Branding.tsx | 3 +- app/components/Dialogs.tsx | 11 +++-- app/components/DocumentCard.tsx | 39 ++++++--------- app/components/DocumentTasks.tsx | 1 + .../Notifications/NotificationListItem.tsx | 6 ++- .../Notifications/NotificationsPopover.tsx | 26 +++++----- app/components/ReadingTime.tsx | 26 ++++++++++ app/components/Theme.tsx | 2 - app/components/Tooltip.tsx | 6 +-- app/hooks/useSettingsConfig.ts | 5 +- app/index.tsx | 20 ++++---- app/models/Document.ts | 45 ----------------- app/models/helpers/ProsemirrorHelper.ts | 49 +++++++++++++++++++ .../Collection/components/ShareButton.tsx | 20 +++++--- app/scenes/Collection/index.tsx | 8 +-- .../Document/components/CommentForm.tsx | 4 +- app/scenes/Document/components/Contents.tsx | 4 +- app/scenes/Document/components/Document.tsx | 29 +---------- .../Document/components/DocumentTitle.tsx | 3 +- app/scenes/Document/components/Footer.tsx | 27 ++++++++++ app/scenes/Document/components/Insights.tsx | 3 +- .../Document/components/ShareButton.tsx | 20 +++++--- .../Document/components/SizeWarning.tsx | 3 +- app/scenes/Document/index.tsx | 7 ++- app/scenes/Login/Login.tsx | 12 ++++- app/scenes/Settings/Integrations.tsx | 5 +- app/scenes/Shared/Document.tsx | 30 +++++++++--- app/scenes/Shared/index.tsx | 38 +++++++------- server/models/helpers/ProsemirrorHelper.tsx | 1 - shared/i18n/locales/en_US/translation.json | 2 +- 33 files changed, 298 insertions(+), 197 deletions(-) create mode 100644 app/components/ReadingTime.tsx create mode 100644 app/models/helpers/ProsemirrorHelper.ts create mode 100644 app/scenes/Document/components/Footer.tsx diff --git a/app/actions/definitions/collections.tsx b/app/actions/definitions/collections.tsx index 06039e0c34..e2a850823d 100644 --- a/app/actions/definitions/collections.tsx +++ b/app/actions/definitions/collections.tsx @@ -22,7 +22,6 @@ import { CollectionNew } from "~/components/Collection/CollectionNew"; import CollectionDeleteDialog from "~/components/CollectionDeleteDialog"; import ConfirmationDialog from "~/components/ConfirmationDialog"; import DynamicCollectionIcon from "~/components/Icons/CollectionIcon"; -import SharePopover from "~/components/Sharing/Collection/SharePopover"; import { getHeaderExpandedKey } from "~/components/Sidebar/components/Header"; import { createAction, @@ -37,10 +36,14 @@ import { searchPath, } from "~/utils/routeHelpers"; import ExportDialog from "~/components/ExportDialog"; +import lazyWithRetry from "~/utils/lazyWithRetry"; const ColorCollectionIcon = ({ collection }: { collection: Collection }) => ( ); +const SharePopover = lazyWithRetry( + () => import("~/components/Sharing/Collection/SharePopover") +); export const openCollection = createAction({ name: ({ t }) => t("Open collection"), diff --git a/app/actions/definitions/documents.tsx b/app/actions/definitions/documents.tsx index 9bdb5dae8d..eb4b5e08dd 100644 --- a/app/actions/definitions/documents.tsx +++ b/app/actions/definitions/documents.tsx @@ -50,7 +50,6 @@ import DeleteDocumentsInTrash from "~/scenes/Trash/components/DeleteDocumentsInT import ConfirmationDialog from "~/components/ConfirmationDialog"; import DocumentCopy from "~/components/DocumentCopy"; import MarkdownIcon from "~/components/Icons/MarkdownIcon"; -import SharePopover from "~/components/Sharing/Document"; import { getHeaderExpandedKey } from "~/components/Sidebar/components/Header"; import DocumentTemplatizeDialog from "~/components/TemplatizeDialog"; import { @@ -82,7 +81,14 @@ import { import capitalize from "lodash/capitalize"; import CollectionIcon from "~/components/Icons/CollectionIcon"; import { ActionV2, ActionV2Group, ActionV2Separator } from "~/types"; -import Insights from "~/scenes/Document/components/Insights"; +import lazyWithRetry from "~/utils/lazyWithRetry"; + +const Insights = lazyWithRetry( + () => import("~/scenes/Document/components/Insights") +); +const SharePopover = lazyWithRetry( + () => import("~/components/Sharing/Document/SharePopover") +); export const openDocument = createAction({ name: ({ t }) => t("Open document"), @@ -593,12 +599,15 @@ export const copyDocumentAsMarkdown = createActionV2({ iconInContextMenu: false, visible: ({ activeDocumentId, stores }) => !!activeDocumentId && stores.policies.abilities(activeDocumentId).download, - perform: ({ stores, activeDocumentId, t }) => { + perform: async ({ stores, activeDocumentId, t }) => { const document = activeDocumentId ? stores.documents.get(activeDocumentId) : undefined; if (document) { - copy(document.toMarkdown()); + const { ProsemirrorHelper } = await import( + "~/models/helpers/ProsemirrorHelper" + ); + copy(ProsemirrorHelper.toMarkdown(document)); toast.success(t("Markdown copied to clipboard")); } }, @@ -612,12 +621,15 @@ export const copyDocumentAsPlainText = createActionV2({ iconInContextMenu: false, visible: ({ activeDocumentId, stores }) => !!activeDocumentId && stores.policies.abilities(activeDocumentId).download, - perform: ({ stores, activeDocumentId, t }) => { + perform: async ({ stores, activeDocumentId, t }) => { const document = activeDocumentId ? stores.documents.get(activeDocumentId) : undefined; if (document) { - copy(document.toPlainText()); + const { ProsemirrorHelper } = await import( + "~/models/helpers/ProsemirrorHelper" + ); + copy(ProsemirrorHelper.toPlainText(document)); toast.success(t("Text copied to clipboard")); } }, diff --git a/app/components/AuthenticatedLayout.tsx b/app/components/AuthenticatedLayout.tsx index 2289b74242..5cd459dcac 100644 --- a/app/components/AuthenticatedLayout.tsx +++ b/app/components/AuthenticatedLayout.tsx @@ -13,7 +13,6 @@ import ErrorSuspended from "~/scenes/Errors/ErrorSuspended"; import Layout from "~/components/Layout"; import RegisterKeyDown from "~/components/RegisterKeyDown"; import Sidebar from "~/components/Sidebar"; -import SettingsSidebar from "~/components/Sidebar/Settings"; import useCurrentTeam from "~/hooks/useCurrentTeam"; import { usePostLoginPath } from "~/hooks/useLastVisitedPath"; import usePolicy from "~/hooks/usePolicy"; @@ -30,6 +29,7 @@ import { import { DocumentContextProvider } from "./DocumentContext"; import Fade from "./Fade"; import { PortalContext } from "./Portal"; +import CommandBar from "./CommandBar"; const DocumentComments = lazyWithRetry( () => import("~/scenes/Document/components/Comments") @@ -37,8 +37,9 @@ const DocumentComments = lazyWithRetry( const DocumentHistory = lazyWithRetry( () => import("~/scenes/Document/components/History") ); - -const CommandBar = lazyWithRetry(() => import("~/components/CommandBar")); +const SettingsSidebar = lazyWithRetry( + () => import("~/components/Sidebar/Settings") +); type Props = { children?: React.ReactNode; @@ -130,9 +131,7 @@ const AuthenticatedLayout: React.FC = ({ children }: Props) => { {children} - - - + diff --git a/app/components/Branding.tsx b/app/components/Branding.tsx index b7f70dca06..96f03fa74a 100644 --- a/app/components/Branding.tsx +++ b/app/components/Branding.tsx @@ -1,3 +1,4 @@ +import * as React from "react"; import styled from "styled-components"; import { depths, s } from "@shared/styles"; import env from "~/env"; @@ -44,4 +45,4 @@ const Link = styled.a` } `; -export default Branding; +export default React.memo(Branding); diff --git a/app/components/Dialogs.tsx b/app/components/Dialogs.tsx index b136566ca5..8306906312 100644 --- a/app/components/Dialogs.tsx +++ b/app/components/Dialogs.tsx @@ -1,7 +1,10 @@ import { observer } from "mobx-react"; -import Guide from "~/components/Guide"; -import Modal from "~/components/Modal"; +import { Suspense } from "react"; import useStores from "~/hooks/useStores"; +import lazyWithRetry from "~/utils/lazyWithRetry"; + +const Guide = lazyWithRetry(() => import("~/components/Guide")); +const Modal = lazyWithRetry(() => import("~/components/Modal")); function Dialogs() { const { dialogs } = useStores(); @@ -9,7 +12,7 @@ function Dialogs() { const modals = [...modalStack]; return ( - <> + {guide ? ( ))} - + ); } diff --git a/app/components/DocumentCard.tsx b/app/components/DocumentCard.tsx index ac470336ea..ec904c4dd5 100644 --- a/app/components/DocumentCard.tsx +++ b/app/components/DocumentCard.tsx @@ -3,8 +3,8 @@ import { CSS } from "@dnd-kit/utilities"; import { subDays } from "date-fns"; import { m } from "framer-motion"; import { observer } from "mobx-react"; -import { CloseIcon, DocumentIcon, ClockIcon, EyeIcon } from "outline-icons"; -import { useRef, useCallback, useMemo } from "react"; +import { CloseIcon, DocumentIcon, ClockIcon } from "outline-icons"; +import { useRef, useCallback, Suspense } from "react"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import styled, { useTheme } from "styled-components"; @@ -19,10 +19,12 @@ import Flex from "~/components/Flex"; import NudeButton from "~/components/NudeButton"; import Time from "~/components/Time"; import useStores from "~/hooks/useStores"; -import { useTextStats } from "~/hooks/useTextStats"; import CollectionIcon from "./Icons/CollectionIcon"; import Text from "./Text"; import Tooltip from "./Tooltip"; +import lazyWithRetry from "~/utils/lazyWithRetry"; + +const ReadingTime = lazyWithRetry(() => import("./ReadingTime")); type Props = { /** The pin record */ @@ -76,6 +78,13 @@ function DocumentCard(props: Props) { const isRecentlyUpdated = new Date(document.updatedAt) > subDays(new Date(), 7); + const updatedAt = ( + <> + +