perf: route- and modal-level code splitting (React.lazy)
The whole app shipped in one bundle, so the logged-out landing and every page pulled all module code. Split into lazy chunks: - main.tsx: lazy App + the public leaves (WatchPage, Privacy, Terms), so a public /watch share link never downloads the authenticated app. - App.tsx: each module page (Feed, Channels, Playlists, Stats, Scheduler, Config, Users, Settings, Notifications, Messages, Downloads, ChannelPage) and the About/ReleaseNotes/Onboarding modals load on demand behind <Suspense>. - Heavy modals lazy in their parents: PlayerModal (Feed, Playlists), DownloadDialog (DownloadButton — kept out of the feed chunk), and the VideoEditor/ShareDialog/ProfileEditor (DownloadCenter). - Extracted focusAccessRequestsTab + the tab constants to lib/adminUsersTab so callers can pre-select the admin tab without statically importing the now lazy-loaded AdminUsers page (which would defeat the split). Build now emits ~25 chunks. Landing no longer downloads any module code (~350 KB deferred); dev landing Perf 93->95. Verified in a real browser: every page + the video editor load with no console errors.
This commit is contained in:
parent
18a33b96c8
commit
2344902a7f
10 changed files with 167 additions and 117 deletions
|
|
@ -4,7 +4,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|||
import { Ban, Check, RotateCcw, Shield, Trash2, UserPlus, X } from "lucide-react";
|
||||
import { api, type AdminUserRow, type Invite, type Me } from "../lib/api";
|
||||
import { notify } from "../lib/notifications";
|
||||
import { LS, setAccountRaw } from "../lib/storage";
|
||||
import { ADMIN_USERS_TAB_KEY } from "../lib/adminUsersTab";
|
||||
import Tooltip from "./Tooltip";
|
||||
import { Section } from "./ui/form";
|
||||
import { useConfirm } from "./ConfirmProvider";
|
||||
|
|
@ -13,15 +13,8 @@ import Tabs, { usePersistedTab } from "./Tabs";
|
|||
// Admin-only user & access management: roles, access requests (the Invite whitelist), and the
|
||||
// demo whitelist + reset. Each section is its own tab (persisted across reloads); the Access tab
|
||||
// carries a badge with the count of pending requests so it's visible without switching to it.
|
||||
// The persisted-tab storage key + the access-requests tab id, exported so a notification's
|
||||
// "Review" link can pre-select that tab before navigating here (usePersistedTab reads the key
|
||||
// on mount, and this page mounts fresh on navigation).
|
||||
export const ADMIN_USERS_TAB_KEY = LS.adminUsersTab;
|
||||
export const ADMIN_USERS_ACCESS_TAB = "access";
|
||||
|
||||
export function focusAccessRequestsTab(): void {
|
||||
setAccountRaw(ADMIN_USERS_TAB_KEY, ADMIN_USERS_ACCESS_TAB);
|
||||
}
|
||||
// (The tab key + a "focus the Access tab" helper live in lib/adminUsersTab so callers can
|
||||
// pre-select the tab without importing this lazy-loaded page — see that file.)
|
||||
|
||||
export default function AdminUsers({ me }: { me: Me }) {
|
||||
const { t } = useTranslation();
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { useState } from "react";
|
||||
import { lazy, Suspense, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Download } from "lucide-react";
|
||||
import clsx from "clsx";
|
||||
import DownloadDialog from "./DownloadDialog";
|
||||
// Lazy: the download dialog (+ its profile editor) loads only when a card's download button is
|
||||
// clicked, so it never ships in the feed chunk for the majority who just browse.
|
||||
const DownloadDialog = lazy(() => import("./DownloadDialog"));
|
||||
import { api, type Me } from "../lib/api";
|
||||
|
||||
// Self-contained download affordance for a video card / the player. Hidden for the demo account
|
||||
|
|
@ -59,7 +61,9 @@ export default function DownloadButton({
|
|||
<Download className={clsx("w-4 h-4", inQueue && "animate-pulse")} />
|
||||
</button>
|
||||
{open && (
|
||||
<DownloadDialog source={videoId} title={title} onClose={() => setOpen(false)} />
|
||||
<Suspense fallback={null}>
|
||||
<DownloadDialog source={videoId} title={title} onClose={() => setOpen(false)} />
|
||||
</Suspense>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useMemo, useState } from "react";
|
||||
import { lazy, Suspense, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
|
|
@ -16,10 +16,12 @@ import {
|
|||
import clsx from "clsx";
|
||||
import Tabs, { usePersistedTab } from "./Tabs";
|
||||
import Modal from "./Modal";
|
||||
import DownloadDialog from "./DownloadDialog";
|
||||
import ProfileEditor from "./ProfileEditor";
|
||||
import VideoEditor from "./VideoEditor";
|
||||
import ShareDialog from "./ShareDialog";
|
||||
// Lazy: these dialogs/editors open on demand, so they stay out of the Downloads page chunk until
|
||||
// used (the video editor in particular is heavy — filmstrip + scrubber).
|
||||
const DownloadDialog = lazy(() => import("./DownloadDialog"));
|
||||
const ProfileEditor = lazy(() => import("./ProfileEditor"));
|
||||
const VideoEditor = lazy(() => import("./VideoEditor"));
|
||||
const ShareDialog = lazy(() => import("./ShareDialog"));
|
||||
import { useConfirm } from "./ConfirmProvider";
|
||||
import { useLiveQuery } from "../lib/useLiveQuery";
|
||||
import { api, type DownloadJob, type Me } from "../lib/api";
|
||||
|
|
@ -582,19 +584,21 @@ export default function DownloadCenter({ me }: { me: Me }) {
|
|||
|
||||
{tab === "system" && me.role === "admin" && <AdminSystem />}
|
||||
|
||||
{addSource && (
|
||||
<DownloadDialog
|
||||
source={addSource}
|
||||
onClose={() => {
|
||||
setAddSource(null);
|
||||
setAddUrl("");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{manage && <ProfileEditor onClose={() => setManage(false)} />}
|
||||
{renameJob && <RenameModal job={renameJob} onClose={() => setRenameJob(null)} />}
|
||||
{shareJob && <ShareDialog job={shareJob} onClose={() => setShareJob(null)} />}
|
||||
{editJob && <VideoEditor job={editJob} onClose={() => setEditJob(null)} />}
|
||||
<Suspense fallback={null}>
|
||||
{addSource && (
|
||||
<DownloadDialog
|
||||
source={addSource}
|
||||
onClose={() => {
|
||||
setAddSource(null);
|
||||
setAddUrl("");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{manage && <ProfileEditor onClose={() => setManage(false)} />}
|
||||
{renameJob && <RenameModal job={renameJob} onClose={() => setRenameJob(null)} />}
|
||||
{shareJob && <ShareDialog job={shareJob} onClose={() => setShareJob(null)} />}
|
||||
{editJob && <VideoEditor job={editJob} onClose={() => setEditJob(null)} />}
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { useState } from "react";
|
||||
import { lazy, Suspense, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import Modal from "./Modal";
|
||||
import ProfileEditor from "./ProfileEditor";
|
||||
import { api } from "../lib/api";
|
||||
|
||||
// Lazy: the full profile editor only opens from the dialog's "manage presets" affordance.
|
||||
const ProfileEditor = lazy(() => import("./ProfileEditor"));
|
||||
import { notify } from "../lib/notifications";
|
||||
import { navigateTo } from "../lib/nav";
|
||||
|
||||
|
|
@ -105,12 +107,14 @@ export default function DownloadDialog({
|
|||
</div>
|
||||
|
||||
{manage && (
|
||||
<ProfileEditor
|
||||
onClose={() => {
|
||||
setManage(false);
|
||||
profilesQ.refetch();
|
||||
}}
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
<ProfileEditor
|
||||
onClose={() => {
|
||||
setManage(false);
|
||||
profilesQ.refetch();
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { lazy, Suspense, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { keepPreviousData, useInfiniteQuery, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { ArrowDown, ArrowUp, ArrowLeft, Info, RefreshCw, Trash2, Youtube } from "lucide-react";
|
||||
|
|
@ -7,7 +7,8 @@ import i18n from "../i18n";
|
|||
import { notify, resolveVideo } from "../lib/notifications";
|
||||
import { useDebounced } from "../lib/useDebounced";
|
||||
import VirtualFeed from "./VirtualFeed";
|
||||
import PlayerModal from "./PlayerModal";
|
||||
// Lazy: the in-app YouTube player (IFrame API) loads only when a video is first opened.
|
||||
const PlayerModal = lazy(() => import("./PlayerModal"));
|
||||
|
||||
const PAGE = 60;
|
||||
|
||||
|
|
@ -441,13 +442,15 @@ export default function Feed({
|
|||
)}
|
||||
|
||||
{activeVideo && (
|
||||
<PlayerModal
|
||||
video={activeVideo.video}
|
||||
startAt={activeVideo.startAt}
|
||||
onClose={() => setActiveVideo(null)}
|
||||
onState={onState}
|
||||
onOpenChannel={onOpenChannel}
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
<PlayerModal
|
||||
video={activeVideo.video}
|
||||
startAt={activeVideo.startAt}
|
||||
onClose={() => setActiveVideo(null)}
|
||||
onState={onState}
|
||||
onOpenChannel={onOpenChannel}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -647,13 +650,15 @@ export default function Feed({
|
|||
/>
|
||||
)}
|
||||
{activeVideo && (
|
||||
<PlayerModal
|
||||
video={activeVideo.video}
|
||||
startAt={activeVideo.startAt}
|
||||
onClose={() => setActiveVideo(null)}
|
||||
onState={onState}
|
||||
onOpenChannel={onOpenChannel}
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
<PlayerModal
|
||||
video={activeVideo.video}
|
||||
startAt={activeVideo.startAt}
|
||||
onClose={() => setActiveVideo(null)}
|
||||
onState={onState}
|
||||
onOpenChannel={onOpenChannel}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
{isFetchingNextPage && (
|
||||
<div className="text-center text-muted py-4">{t("feed.loadingMore")}</div>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|||
import { Activity, Bell, Check, CheckCheck, ExternalLink, Eye, RotateCcw, Search, Trash2, Tv, UserPlus, X } from "lucide-react";
|
||||
import { api, type AppNotification, type FeedFilters } from "../lib/api";
|
||||
import { useLiveQuery } from "../lib/useLiveQuery";
|
||||
import { focusAccessRequestsTab } from "./AdminUsers";
|
||||
import { focusAccessRequestsTab } from "../lib/adminUsersTab";
|
||||
import {
|
||||
clearAll as clearClient,
|
||||
dismiss as dismissClient,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { lazy, Suspense, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
|
|
@ -39,7 +39,7 @@ import { formatDuration } from "../lib/format";
|
|||
import { notify } from "../lib/notifications";
|
||||
import { getAccountRaw, LS, readAccountMerged, setAccountRaw, writeAccount } from "../lib/storage";
|
||||
import { useUndoable } from "../lib/useUndoable";
|
||||
import PlayerModal from "./PlayerModal";
|
||||
const PlayerModal = lazy(() => import("./PlayerModal"));
|
||||
import UndoToolbar from "./UndoToolbar";
|
||||
import { useConfirm } from "./ConfirmProvider";
|
||||
|
||||
|
|
@ -850,14 +850,16 @@ export default function Playlists({ canWrite }: { canWrite: boolean }) {
|
|||
</main>
|
||||
|
||||
{playingIndex != null && items[playingIndex] && (
|
||||
<PlayerModal
|
||||
video={items[playingIndex]}
|
||||
queue={items}
|
||||
startIndex={playingIndex}
|
||||
startAt={null}
|
||||
onClose={() => setPlayingIndex(null)}
|
||||
onState={playerState}
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
<PlayerModal
|
||||
video={items[playingIndex]}
|
||||
queue={items}
|
||||
startIndex={playingIndex}
|
||||
startAt={null}
|
||||
onClose={() => setPlayingIndex(null)}
|
||||
onState={playerState}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue