perf(bundle): code-split heavy routes and the emoji picker and add cacheable vendor chunks to shrink first-load
This commit is contained in:
parent
297b55f693
commit
067417050c
13 changed files with 305 additions and 158 deletions
11
package-lock.json
generated
11
package-lock.json
generated
|
|
@ -34,7 +34,6 @@
|
|||
"browser-encrypt-attachment": "0.3.0",
|
||||
"chroma-js": "3.1.2",
|
||||
"classnames": "2.3.2",
|
||||
"dateformat": "5.0.3",
|
||||
"dayjs": "1.11.10",
|
||||
"domhandler": "5.0.3",
|
||||
"emojibase": "15.3.1",
|
||||
|
|
@ -116,7 +115,7 @@
|
|||
"wait-on": "9.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
"node": ">=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
|
|
@ -8266,14 +8265,6 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/dateformat": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-5.0.3.tgz",
|
||||
"integrity": "sha512-Kvr6HmPXUMerlLcLF+Pwq3K7apHpYmGDVqrxcDasBg86UcKeTSNWbEzU8bwdXnxnR44FtMhJAxI4Bov6Y/KUfA==",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.10",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
|
||||
|
|
|
|||
|
|
@ -76,7 +76,6 @@
|
|||
"browser-encrypt-attachment": "0.3.0",
|
||||
"chroma-js": "3.1.2",
|
||||
"classnames": "2.3.2",
|
||||
"dateformat": "5.0.3",
|
||||
"dayjs": "1.11.10",
|
||||
"domhandler": "5.0.3",
|
||||
"emojibase": "15.3.1",
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ import { onTabPress } from '../../../utils/keyboard';
|
|||
import { createEmoticonElement, moveCursor, replaceWithElement } from '../utils';
|
||||
import { useRecentEmoji } from '../../../hooks/useRecentEmoji';
|
||||
import { useRelevantImagePacks } from '../../../hooks/useImagePacks';
|
||||
import { IEmoji, emojis } from '../../../plugins/emoji';
|
||||
import { IEmoji } from '../../../plugins/emoji';
|
||||
import { emojis } from '../../../plugins/emoji-data';
|
||||
import { useKeyDown } from '../../../hooks/useKeyDown';
|
||||
import { mxcUrlToHttp } from '../../../utils/matrix';
|
||||
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ import { isKeyHotkey } from 'is-hotkey';
|
|||
import { Room } from 'matrix-js-sdk';
|
||||
import { atom, PrimitiveAtom, useAtom, useSetAtom } from 'jotai';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { IEmoji, emojiGroups, emojis } from '../../plugins/emoji';
|
||||
import { IEmoji } from '../../plugins/emoji';
|
||||
import { emojiGroups, emojis } from '../../plugins/emoji-data';
|
||||
import { useEmojiGroupLabels } from './useEmojiGroupLabels';
|
||||
import { useEmojiGroupIcons } from './useEmojiGroupIcons';
|
||||
import { preventScrollWithArrowKey, stopPropagation } from '../../utils/keyboard';
|
||||
|
|
|
|||
|
|
@ -1,9 +1,17 @@
|
|||
import React from 'react';
|
||||
import React, { Suspense } from 'react';
|
||||
import { Outlet, useMatch } from 'react-router-dom';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||
import { isNativePlatform } from '../../utils/capacitor';
|
||||
import { BOTS_PATH, CHANNELS_PATH, CHANNELS_SPACE_PATH, DIRECT_PATH } from '../../pages/paths';
|
||||
import { MobileTabsPager } from './MobileTabsPager';
|
||||
|
||||
// MobileTabsPager is the ONLY static importer of the Channels and Bots feature
|
||||
// modules. It renders only on `mobile && native` (below), so lazy-loading it
|
||||
// here removes the static edge that otherwise pinned Channels + Bots into the
|
||||
// boot bundle for every target. On native the chunk streams from the local APK
|
||||
// filesystem (no network), so a null Suspense fallback is imperceptible.
|
||||
const MobileTabsPager = React.lazy(() =>
|
||||
import('./MobileTabsPager').then((m) => ({ default: m.MobileTabsPager }))
|
||||
);
|
||||
|
||||
// Router-level wrapper around the three listing tabs (/direct/,
|
||||
// /channels/, /bots/). When all of (mobile breakpoint, Capacitor
|
||||
|
|
@ -44,5 +52,9 @@ export function MobileTabsLayout() {
|
|||
if (!(mobile && native) || !onListingRoot) {
|
||||
return <Outlet />;
|
||||
}
|
||||
return <MobileTabsPager />;
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<MobileTabsPager />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
import React from 'react';
|
||||
import React, { Suspense } from 'react';
|
||||
import { Dialog, Text, Box, Button, config } from 'folds';
|
||||
import { AuthType } from 'matrix-js-sdk';
|
||||
import ReCAPTCHA from 'react-google-recaptcha';
|
||||
import { StageComponentProps } from './types';
|
||||
|
||||
// react-google-recaptcha (+ its grecaptcha loader) is only ever rendered in
|
||||
// the registration UIA captcha stage — a cold, rare path. Lazy-loading it
|
||||
// keeps it out of the boot bundle.
|
||||
const ReCAPTCHA = React.lazy(() => import('react-google-recaptcha'));
|
||||
|
||||
function ReCaptchaErrorDialog({
|
||||
title,
|
||||
message,
|
||||
|
|
@ -57,7 +61,9 @@ export function ReCaptchaStageDialog({ stageData, submitAuthDict, onCancel }: St
|
|||
<Dialog>
|
||||
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
|
||||
<Text>Please check the box below to proceed.</Text>
|
||||
<ReCAPTCHA sitekey={publicKey} onChange={handleChange} />
|
||||
<Suspense fallback={<Text size="T200">Loading…</Text>}>
|
||||
<ReCAPTCHA sitekey={publicKey} onChange={handleChange} />
|
||||
</Suspense>
|
||||
</Box>
|
||||
</Dialog>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,11 @@
|
|||
import React, { FormEventHandler, MouseEventHandler, useCallback, useMemo, useState } from 'react';
|
||||
import React, {
|
||||
FormEventHandler,
|
||||
MouseEventHandler,
|
||||
Suspense,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
|
|
@ -37,7 +44,6 @@ import { useRoom } from '../../../hooks/useRoom';
|
|||
import { HexColorPickerPopOut } from '../../../components/HexColorPickerPopOut';
|
||||
import { PowerColorBadge, PowerIcon } from '../../../components/power';
|
||||
import { UseStateProvider } from '../../../components/UseStateProvider';
|
||||
import { EmojiBoard } from '../../../components/emoji-board';
|
||||
import { useImagePackRooms } from '../../../hooks/useImagePackRooms';
|
||||
import { roomToParentsAtom } from '../../../state/room/roomToParents';
|
||||
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||
|
|
@ -52,6 +58,17 @@ import { BetaNoticeBadge } from '../../../components/BetaNoticeBadge';
|
|||
import { getPowerTagIconSrc } from '../../../hooks/useMemberPowerTag';
|
||||
import { creatorsSupported } from '../../../utils/matrix';
|
||||
|
||||
// Lazy-loaded: EmojiBoard pulls the heavy emoji picker dataset (`emoji-data`,
|
||||
// ~506 KB compact.json). PowersEditor is reachable from the boot graph
|
||||
// (RoomSettingsRenderer / SpaceSettingsRenderer are mounted at the app root),
|
||||
// so a static import here would drag `emoji-data` into the initial bundle. The
|
||||
// board only mounts inside the PopOut below when the user opens the icon
|
||||
// picker, so deferring it keeps `emoji-data` off boot (the chat composer's own
|
||||
// static EmojiBoard import keeps the chunk in the lazy Room bundle).
|
||||
const EmojiBoard = React.lazy(() =>
|
||||
import('../../../components/emoji-board').then((m) => ({ default: m.EmojiBoard }))
|
||||
);
|
||||
|
||||
type EditPowerProps = {
|
||||
maxPower: number;
|
||||
power?: number;
|
||||
|
|
@ -208,23 +225,25 @@ function EditPower({ maxPower, power, tag, onSave, onClose }: EditPowerProps) {
|
|||
position="Bottom"
|
||||
anchor={cords}
|
||||
content={
|
||||
<EmojiBoard
|
||||
imagePackRooms={imagePackRooms}
|
||||
returnFocusOnDeactivate={false}
|
||||
allowTextCustomEmoji={false}
|
||||
addToRecentEmoji={false}
|
||||
onEmojiSelect={(key) => {
|
||||
setTagIcon({ key });
|
||||
setCords(undefined);
|
||||
}}
|
||||
onCustomEmojiSelect={(mxc) => {
|
||||
setTagIcon({ key: mxc });
|
||||
setCords(undefined);
|
||||
}}
|
||||
requestClose={() => {
|
||||
setCords(undefined);
|
||||
}}
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
<EmojiBoard
|
||||
imagePackRooms={imagePackRooms}
|
||||
returnFocusOnDeactivate={false}
|
||||
allowTextCustomEmoji={false}
|
||||
addToRecentEmoji={false}
|
||||
onEmojiSelect={(key) => {
|
||||
setTagIcon({ key });
|
||||
setCords(undefined);
|
||||
}}
|
||||
onCustomEmojiSelect={(mxc) => {
|
||||
setTagIcon({ key: mxc });
|
||||
setCords(undefined);
|
||||
}}
|
||||
requestClose={() => {
|
||||
setCords(undefined);
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { Provider as JotaiProvider } from 'jotai';
|
|||
import { OverlayContainerProvider, PopOutContainerProvider, TooltipContainerProvider } from 'folds';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
|
||||
import { ClientConfigLoader } from '../components/ClientConfigLoader';
|
||||
import { ClientConfigProvider } from '../hooks/useClientConfig';
|
||||
|
|
@ -16,6 +15,15 @@ import { installPushLanguageBridge } from '../utils/pushLanguageBridge';
|
|||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
// Dev-only: react-query-devtools must never ship to production. The dynamic
|
||||
// import lives in a branch Vite statically eliminates (import.meta.env.DEV →
|
||||
// false in prod), so Rollup drops the chunk entirely from the prod bundle.
|
||||
const ReactQueryDevtools = import.meta.env.DEV
|
||||
? React.lazy(() =>
|
||||
import('@tanstack/react-query-devtools').then((m) => ({ default: m.ReactQueryDevtools }))
|
||||
)
|
||||
: null;
|
||||
|
||||
function App() {
|
||||
const screenSize = useScreenSize();
|
||||
useCompositionEndTracking();
|
||||
|
|
@ -48,7 +56,11 @@ function App() {
|
|||
<JotaiProvider>
|
||||
<RouterProvider router={createRouter(clientConfig, screenSize)} />
|
||||
</JotaiProvider>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
{ReactQueryDevtools && (
|
||||
<React.Suspense fallback={null}>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</React.Suspense>
|
||||
)}
|
||||
</QueryClientProvider>
|
||||
</ClientConfigProvider>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import React, { Suspense } from 'react';
|
||||
import { Box, Spinner } from 'folds';
|
||||
import {
|
||||
Navigate,
|
||||
Outlet,
|
||||
|
|
@ -53,13 +54,9 @@ import { getMxIdServer, isUserId } from '../utils/matrix';
|
|||
import { ClientBindAtoms, ClientLayout, ClientRoot } from './client';
|
||||
import { HomeRouteRoomProvider } from './client/home';
|
||||
import { Direct, DirectCreate, DirectRouteRoomProvider } from './client/direct';
|
||||
import { BotExperienceHost, Bots } from './client/bots';
|
||||
import { Channels, ChannelsRootNav, ChannelPickPlaceholder } from './client/channels';
|
||||
import { ChannelPickPlaceholder } from './client/channels/ChannelPickPlaceholder';
|
||||
import { RouteSpaceProvider, Space, SpaceRouteRoomProvider, SpaceSearch } from './client/space';
|
||||
import { Explore, FeaturedRooms, PublicRooms } from './client/explore';
|
||||
import { setAfterLoginRedirectPath } from './afterLoginRedirectPath';
|
||||
import { Room } from '../features/room';
|
||||
import { Lobby } from '../features/lobby';
|
||||
import { WelcomePage } from './client/WelcomePage';
|
||||
import { PageRoot } from '../components/page';
|
||||
import { ScreenSize } from '../hooks/useScreenSize';
|
||||
|
|
@ -73,11 +70,10 @@ import { RoomSettingsRenderer } from '../features/room-settings';
|
|||
import { ClientRoomsNotificationPreferences } from './client/ClientRoomsNotificationPreferences';
|
||||
import { SpaceSettingsRenderer } from '../features/space-settings';
|
||||
import { CreateRoomModalRenderer } from '../features/create-room';
|
||||
import { Create } from './client/create';
|
||||
import { CreateSpaceModalRenderer } from '../features/create-space';
|
||||
import { SearchModalRenderer } from '../features/search';
|
||||
import { useShareTargetReceiver } from '../hooks/useShareTargetReceiver';
|
||||
import { SettingsScreen } from '../features/settings';
|
||||
import { SettingsScreen } from '../features/settings/SettingsScreen';
|
||||
import { getFallbackSession } from '../state/sessions';
|
||||
import { CallEmbedProvider } from '../components/CallEmbedProvider';
|
||||
import { useIncomingRtcNotifications } from '../hooks/useIncomingRtcNotifications';
|
||||
|
|
@ -129,6 +125,59 @@ function UserLinkRedirect() {
|
|||
return <Navigate to={withSearchParam(getDirectCreatePath(), params)} replace />;
|
||||
}
|
||||
|
||||
// Route-level code-splitting. Each heavy page is loaded on first navigation
|
||||
// instead of in the boot bundle — this pulls the timeline (Room), lobby,
|
||||
// explore, bots and channels subsystems off first load. Imports point at the
|
||||
// concrete source file (not the barrel) so each chunk stays tight.
|
||||
// `routeSuspense` wraps every usage so only the content area shows the fallback
|
||||
// while the chunk streams in — surrounding nav/chrome stays mounted.
|
||||
//
|
||||
// NOTE: SettingsScreen is intentionally NOT lazy here. The settings UI
|
||||
// (`features/settings/Settings`) is already pulled into the boot graph by
|
||||
// `Direct` → `MobileSettingsHorseshoe` (the mobile settings sheet, mounted in
|
||||
// the boot listing view), so a Router-level lazy split produces a Rollup
|
||||
// "dynamically + statically imported" warning and zero size win. Splitting
|
||||
// settings off boot requires lazy-loading `<Settings>` inside its two hosts
|
||||
// (MobileSettingsHorseshoe + SettingsScreen) — deferred as a separate change.
|
||||
const Room = React.lazy(() => import('../features/room/Room').then((m) => ({ default: m.Room })));
|
||||
const Lobby = React.lazy(() =>
|
||||
import('../features/lobby/Lobby').then((m) => ({ default: m.Lobby }))
|
||||
);
|
||||
const Explore = React.lazy(() =>
|
||||
import('./client/explore/Explore').then((m) => ({ default: m.Explore }))
|
||||
);
|
||||
const FeaturedRooms = React.lazy(() =>
|
||||
import('./client/explore/Featured').then((m) => ({ default: m.FeaturedRooms }))
|
||||
);
|
||||
const PublicRooms = React.lazy(() =>
|
||||
import('./client/explore/Server').then((m) => ({ default: m.PublicRooms }))
|
||||
);
|
||||
const Bots = React.lazy(() => import('./client/bots/Bots').then((m) => ({ default: m.Bots })));
|
||||
const BotExperienceHost = React.lazy(() =>
|
||||
import('./client/bots/BotExperienceHost').then((m) => ({ default: m.BotExperienceHost }))
|
||||
);
|
||||
const Channels = React.lazy(() =>
|
||||
import('./client/channels/Channels').then((m) => ({ default: m.Channels }))
|
||||
);
|
||||
const ChannelsRootNav = React.lazy(() =>
|
||||
import('./client/channels/Channels').then((m) => ({ default: m.ChannelsRootNav }))
|
||||
);
|
||||
const Create = React.lazy(() =>
|
||||
import('./client/create/Create').then((m) => ({ default: m.Create }))
|
||||
);
|
||||
|
||||
function RouteSuspenseFallback() {
|
||||
return (
|
||||
<Box grow="Yes" style={{ height: '100%' }} alignItems="Center" justifyContent="Center">
|
||||
<Spinner variant="Secondary" size="600" />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const routeSuspense = (node: React.ReactNode) => (
|
||||
<Suspense fallback={<RouteSuspenseFallback />}>{node}</Suspense>
|
||||
);
|
||||
|
||||
export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) => {
|
||||
const { hashRouter } = clientConfig;
|
||||
const mobile = screenSize === ScreenSize.Mobile;
|
||||
|
|
@ -225,11 +274,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
|||
<Route path={_SEARCH_PATH} element={<Navigate to={DIRECT_PATH} replace />} />
|
||||
<Route
|
||||
path={_ROOM_PATH}
|
||||
element={
|
||||
<HomeRouteRoomProvider>
|
||||
<Room />
|
||||
</HomeRouteRoomProvider>
|
||||
}
|
||||
element={<HomeRouteRoomProvider>{routeSuspense(<Room />)}</HomeRouteRoomProvider>}
|
||||
/>
|
||||
</Route>
|
||||
{/* Mobile + Capacitor horizontal swipe pager. The layout-route
|
||||
|
|
@ -258,11 +303,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
|||
<Route path={_CREATE_PATH} element={<DirectCreate />} />
|
||||
<Route
|
||||
path={_ROOM_PATH}
|
||||
element={
|
||||
<DirectRouteRoomProvider>
|
||||
<Room />
|
||||
</DirectRouteRoomProvider>
|
||||
}
|
||||
element={<DirectRouteRoomProvider>{routeSuspense(<Room />)}</DirectRouteRoomProvider>}
|
||||
/>
|
||||
</Route>
|
||||
{/* Bots reuses StreamHeader segments. /bots/* is reserved before SPACE_PATH so deep URLs don't fall to /:spaceIdOrAlias/. */}
|
||||
|
|
@ -272,7 +313,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
|||
<PageRoot
|
||||
nav={
|
||||
<MobileFriendlyPageNav path={BOTS_PATH}>
|
||||
<Bots />
|
||||
{routeSuspense(<Bots />)}
|
||||
</MobileFriendlyPageNav>
|
||||
}
|
||||
>
|
||||
|
|
@ -281,7 +322,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
|||
}
|
||||
>
|
||||
{mobile ? null : <Route index element={<WelcomePage />} />}
|
||||
<Route path=":botId" element={<BotExperienceHost />} />
|
||||
<Route path=":botId" element={routeSuspense(<BotExperienceHost />)} />
|
||||
</Route>
|
||||
<Route path="/bots/*" element={<Navigate to={BOTS_PATH} replace />} />
|
||||
{/* Channels segment. /channels/* is reserved before SPACE_PATH so the
|
||||
|
|
@ -302,7 +343,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
|||
<PageRoot
|
||||
nav={
|
||||
<MobileFriendlyPageNav path={CHANNELS_PATH}>
|
||||
<ChannelsRootNav />
|
||||
{routeSuspense(<ChannelsRootNav />)}
|
||||
</MobileFriendlyPageNav>
|
||||
}
|
||||
>
|
||||
|
|
@ -317,7 +358,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
|||
<PageRoot
|
||||
nav={
|
||||
<MobileFriendlyPageNav path={CHANNELS_SPACE_PATH}>
|
||||
<Channels />
|
||||
{routeSuspense(<Channels />)}
|
||||
</MobileFriendlyPageNav>
|
||||
}
|
||||
>
|
||||
|
|
@ -329,11 +370,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
|||
{mobile ? null : <Route index element={<ChannelPickPlaceholder />} />}
|
||||
<Route
|
||||
path={CHANNELS_ROOM_PATH.slice(CHANNELS_SPACE_PATH.length)}
|
||||
element={
|
||||
<SpaceRouteRoomProvider>
|
||||
<Room />
|
||||
</SpaceRouteRoomProvider>
|
||||
}
|
||||
element={<SpaceRouteRoomProvider>{routeSuspense(<Room />)}</SpaceRouteRoomProvider>}
|
||||
>
|
||||
{/* Thread drawer URL — same Room element renders, drawer
|
||||
opens by reading `:rootId` via useParams. The
|
||||
|
|
@ -383,15 +420,11 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
|||
element={<WelcomePage />}
|
||||
/>
|
||||
)}
|
||||
<Route path={_LOBBY_PATH} element={<Lobby />} />
|
||||
<Route path={_LOBBY_PATH} element={routeSuspense(<Lobby />)} />
|
||||
<Route path={_SEARCH_PATH} element={<SpaceSearch />} />
|
||||
<Route
|
||||
path={_ROOM_PATH}
|
||||
element={
|
||||
<SpaceRouteRoomProvider>
|
||||
<Room />
|
||||
</SpaceRouteRoomProvider>
|
||||
}
|
||||
element={<SpaceRouteRoomProvider>{routeSuspense(<Room />)}</SpaceRouteRoomProvider>}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
|
|
@ -400,7 +433,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
|||
<PageRoot
|
||||
nav={
|
||||
<MobileFriendlyPageNav path={EXPLORE_PATH}>
|
||||
<Explore />
|
||||
{routeSuspense(<Explore />)}
|
||||
</MobileFriendlyPageNav>
|
||||
}
|
||||
>
|
||||
|
|
@ -415,10 +448,10 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
|
|||
element={<WelcomePage />}
|
||||
/>
|
||||
)}
|
||||
<Route path={_FEATURED_PATH} element={<FeaturedRooms />} />
|
||||
<Route path={_SERVER_PATH} element={<PublicRooms />} />
|
||||
<Route path={_FEATURED_PATH} element={routeSuspense(<FeaturedRooms />)} />
|
||||
<Route path={_SERVER_PATH} element={routeSuspense(<PublicRooms />)} />
|
||||
</Route>
|
||||
<Route path={CREATE_PATH} element={<Create />} />
|
||||
<Route path={CREATE_PATH} element={routeSuspense(<Create />)} />
|
||||
{/* /settings shares the DIRECT_PATH shell — left page-nav stays
|
||||
the DM list, the right pane swaps the chat outlet for the
|
||||
Settings UI. The horseshoe rounded TL/BL on the right pane
|
||||
|
|
|
|||
86
src/app/plugins/emoji-data.ts
Normal file
86
src/app/plugins/emoji-data.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import emojisData from 'emojibase-data/en/compact.json';
|
||||
import { EmojiGroupId, IEmoji, IEmojiGroup, getShortcodesFor } from './emoji';
|
||||
|
||||
// Heavy emoji dataset (~506 KB raw `compact.json`). Split out of `./emoji` so
|
||||
// it is bundled only into the lazy chunks that render the emoji picker /
|
||||
// autocomplete (EmojiBoard, EmoticonAutocomplete) and never into the initial
|
||||
// app bundle. The module-level processing below runs once on first import of
|
||||
// this file.
|
||||
export const emojiGroups: IEmojiGroup[] = [
|
||||
{
|
||||
id: EmojiGroupId.People,
|
||||
order: 0,
|
||||
emojis: [],
|
||||
},
|
||||
{
|
||||
id: EmojiGroupId.Nature,
|
||||
order: 1,
|
||||
emojis: [],
|
||||
},
|
||||
{
|
||||
id: EmojiGroupId.Food,
|
||||
order: 2,
|
||||
emojis: [],
|
||||
},
|
||||
{
|
||||
id: EmojiGroupId.Activity,
|
||||
order: 3,
|
||||
emojis: [],
|
||||
},
|
||||
{
|
||||
id: EmojiGroupId.Travel,
|
||||
order: 4,
|
||||
emojis: [],
|
||||
},
|
||||
{
|
||||
id: EmojiGroupId.Object,
|
||||
order: 5,
|
||||
emojis: [],
|
||||
},
|
||||
{
|
||||
id: EmojiGroupId.Symbol,
|
||||
order: 6,
|
||||
emojis: [],
|
||||
},
|
||||
{
|
||||
id: EmojiGroupId.Flag,
|
||||
order: 7,
|
||||
emojis: [],
|
||||
},
|
||||
];
|
||||
|
||||
export const emojis: IEmoji[] = [];
|
||||
|
||||
function addEmojiToGroup(groupIndex: number, emoji: IEmoji) {
|
||||
emojiGroups[groupIndex].emojis.push(emoji);
|
||||
}
|
||||
|
||||
function getGroupIndex(emoji: IEmoji): number | undefined {
|
||||
if (emoji.group === 0 || emoji.group === 1) return 0;
|
||||
if (emoji.group === 3) return 1;
|
||||
if (emoji.group === 4) return 2;
|
||||
if (emoji.group === 6) return 3;
|
||||
if (emoji.group === 5) return 4;
|
||||
if (emoji.group === 7) return 5;
|
||||
if (emoji.group === 8 || typeof emoji.group === 'undefined') return 6;
|
||||
if (emoji.group === 9) return 7;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
emojisData.forEach((emoji) => {
|
||||
const myShortCodes = getShortcodesFor(emoji.hexcode);
|
||||
if (!myShortCodes) return;
|
||||
if (Array.isArray(myShortCodes) && myShortCodes.length === 0) return;
|
||||
|
||||
const em: IEmoji = {
|
||||
...emoji,
|
||||
shortcode: Array.isArray(myShortCodes) ? myShortCodes[0] : myShortCodes,
|
||||
shortcodes: Array.isArray(myShortCodes) ? myShortCodes : emoji.shortcodes,
|
||||
};
|
||||
|
||||
const groupIndex = getGroupIndex(em);
|
||||
if (groupIndex !== undefined) {
|
||||
addEmojiToGroup(groupIndex, em);
|
||||
emojis.push(em);
|
||||
}
|
||||
});
|
||||
|
|
@ -1,8 +1,15 @@
|
|||
import { CompactEmoji, fromUnicodeToHexcode } from 'emojibase';
|
||||
import emojisData from 'emojibase-data/en/compact.json';
|
||||
import joypixels from 'emojibase-data/en/shortcodes/joypixels.json';
|
||||
import emojibase from 'emojibase-data/en/shortcodes/emojibase.json';
|
||||
|
||||
// Light emoji module. Holds the shortcode lookups + types + group enum — the
|
||||
// only emoji surface the broadly-imported HTML parser (and thus the boot
|
||||
// graph, via RoomNavItem/SpaceTabs → InviteUserPrompt → react-custom-html-parser)
|
||||
// actually needs. The heavy `compact.json` dataset (~506 KB) that builds the
|
||||
// `emojis` / `emojiGroups` arrays for the picker + autocomplete lives in
|
||||
// `./emoji-data` so it only loads inside the lazy Room/Settings chunks instead
|
||||
// of the initial bundle. Don't re-import `compact.json` here or the split
|
||||
// regresses.
|
||||
export type IEmoji = CompactEmoji & {
|
||||
shortcode: string;
|
||||
};
|
||||
|
|
@ -33,82 +40,3 @@ export const getShortcodeFor = (hexcode: string): string | undefined => {
|
|||
};
|
||||
|
||||
export const getHexcodeForEmoji = fromUnicodeToHexcode;
|
||||
|
||||
export const emojiGroups: IEmojiGroup[] = [
|
||||
{
|
||||
id: EmojiGroupId.People,
|
||||
order: 0,
|
||||
emojis: [],
|
||||
},
|
||||
{
|
||||
id: EmojiGroupId.Nature,
|
||||
order: 1,
|
||||
emojis: [],
|
||||
},
|
||||
{
|
||||
id: EmojiGroupId.Food,
|
||||
order: 2,
|
||||
emojis: [],
|
||||
},
|
||||
{
|
||||
id: EmojiGroupId.Activity,
|
||||
order: 3,
|
||||
emojis: [],
|
||||
},
|
||||
{
|
||||
id: EmojiGroupId.Travel,
|
||||
order: 4,
|
||||
emojis: [],
|
||||
},
|
||||
{
|
||||
id: EmojiGroupId.Object,
|
||||
order: 5,
|
||||
emojis: [],
|
||||
},
|
||||
{
|
||||
id: EmojiGroupId.Symbol,
|
||||
order: 6,
|
||||
emojis: [],
|
||||
},
|
||||
{
|
||||
id: EmojiGroupId.Flag,
|
||||
order: 7,
|
||||
emojis: [],
|
||||
},
|
||||
];
|
||||
|
||||
export const emojis: IEmoji[] = [];
|
||||
|
||||
function addEmojiToGroup(groupIndex: number, emoji: IEmoji) {
|
||||
emojiGroups[groupIndex].emojis.push(emoji);
|
||||
}
|
||||
|
||||
function getGroupIndex(emoji: IEmoji): number | undefined {
|
||||
if (emoji.group === 0 || emoji.group === 1) return 0;
|
||||
if (emoji.group === 3) return 1;
|
||||
if (emoji.group === 4) return 2;
|
||||
if (emoji.group === 6) return 3;
|
||||
if (emoji.group === 5) return 4;
|
||||
if (emoji.group === 7) return 5;
|
||||
if (emoji.group === 8 || typeof emoji.group === 'undefined') return 6;
|
||||
if (emoji.group === 9) return 7;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
emojisData.forEach((emoji) => {
|
||||
const myShortCodes = getShortcodesFor(emoji.hexcode);
|
||||
if (!myShortCodes) return;
|
||||
if (Array.isArray(myShortCodes) && myShortCodes.length === 0) return;
|
||||
|
||||
const em: IEmoji = {
|
||||
...emoji,
|
||||
shortcode: Array.isArray(myShortCodes) ? myShortCodes[0] : myShortCodes,
|
||||
shortcodes: Array.isArray(myShortCodes) ? myShortCodes : emoji.shortcodes,
|
||||
};
|
||||
|
||||
const groupIndex = getGroupIndex(em);
|
||||
if (groupIndex !== undefined) {
|
||||
addEmojiToGroup(groupIndex, em);
|
||||
emojis.push(em);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import { getAccountData } from '../utils/room';
|
||||
import { IEmoji, emojis } from './emoji';
|
||||
import { IEmoji } from './emoji';
|
||||
import { emojis } from './emoji-data';
|
||||
import { AccountDataEvent } from '../../types/matrix/accountData';
|
||||
|
||||
type EmojiUnicode = string;
|
||||
|
|
|
|||
|
|
@ -306,6 +306,64 @@ export default defineConfig({
|
|||
copyPublicDir: false,
|
||||
rollupOptions: {
|
||||
plugins: [inject({ Buffer: ['buffer', 'Buffer'] })],
|
||||
output: {
|
||||
// Conservative vendor splitting: carve only the large, stable,
|
||||
// leaf-ish dependency families out of the single ~1 MB-gzip app
|
||||
// chunk into their own cacheable chunks. The win is cache stability
|
||||
// — an app-code redeploy no longer busts the matrix-js-sdk / slate
|
||||
// bytes for returning users, and the two load in parallel with the
|
||||
// entry. We deliberately do NOT split react / folds / react-aria:
|
||||
// they are boot-critical and tightly coupled, and aggressive vendor
|
||||
// splitting under topLevelAwait + wasm can reorder init and break
|
||||
// shared singletons (element-web likewise avoids hand-rolled vendor
|
||||
// cacheGroups). Heavy *feature* code is code-split via React.lazy in
|
||||
// Router.tsx instead — that's what actually shrinks first load.
|
||||
manualChunks(id) {
|
||||
// Pin the heavy emoji PICKER dataset (~506 KB `compact.json` + the
|
||||
// `emoji-data.ts` module that processes it) to its own chunk so it
|
||||
// stays OFF the initial bundle — only the lazy Room / Settings chunks
|
||||
// import it (on first room open / picker mount). Runs BEFORE the
|
||||
// node_modules guard so the src module is pinned too.
|
||||
if (id.includes('/plugins/emoji-data') || id.includes('emojibase-data/en/compact')) {
|
||||
return 'emoji-data';
|
||||
}
|
||||
// Pin the LIGHT emoji module (shortcode lookups + `getHexcodeForEmoji`)
|
||||
// and its shortcode maps to a SEPARATE chunk. These are genuinely on the
|
||||
// boot path — the HTML parser (react-custom-html-parser, reachable from
|
||||
// RoomNavItem → InviteUserPrompt) statically imports them. Pinning them
|
||||
// here is what KEEPS `emoji-data` off boot: without it Rollup folds the
|
||||
// light module into the `emoji-data` chunk (it's a dependency of
|
||||
// `emoji-data.ts`), and the entry's boot import of the light functions
|
||||
// then drags the 506 KB `compact.json` into the initial load with it.
|
||||
// `.ts` is matched specifically so this never catches `emoji-data.ts`
|
||||
// (already returned above).
|
||||
if (id.includes('/plugins/emoji.ts') || id.includes('emojibase-data/en/shortcodes')) {
|
||||
return 'emoji-shortcodes';
|
||||
}
|
||||
if (!id.includes('node_modules')) return undefined;
|
||||
// Conservative vendor splitting: carve only the large, stable,
|
||||
// leaf-ish dependency families out of the single app chunk into their
|
||||
// own cacheable chunks. The win is cache stability — an app-code
|
||||
// redeploy no longer busts the matrix-js-sdk / slate bytes for
|
||||
// returning users. We deliberately do NOT split react / folds /
|
||||
// react-aria: they are boot-critical and tightly coupled, and
|
||||
// aggressive vendor splitting under topLevelAwait + wasm can reorder
|
||||
// init and break shared singletons (element-web likewise avoids
|
||||
// hand-rolled vendor cacheGroups).
|
||||
if (
|
||||
id.includes('matrix-js-sdk') ||
|
||||
id.includes('@matrix-org/') ||
|
||||
id.includes('matrix-events-sdk') ||
|
||||
id.includes('matrix-widget-api')
|
||||
) {
|
||||
return 'matrix-sdk';
|
||||
}
|
||||
if (/[\\/]node_modules[\\/](slate|slate-react|slate-dom|slate-history)[\\/]/.test(id)) {
|
||||
return 'editor';
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue