diff --git a/src/app/components/mobile-tabs-pager/MobileTabsLayout.tsx b/src/app/components/mobile-tabs-pager/MobileTabsLayout.tsx index d7ed9ccc..093a3e8a 100644 --- a/src/app/components/mobile-tabs-pager/MobileTabsLayout.tsx +++ b/src/app/components/mobile-tabs-pager/MobileTabsLayout.tsx @@ -4,11 +4,14 @@ import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize'; import { isNativePlatform } from '../../utils/capacitor'; import { BOTS_PATH, CHANNELS_PATH, CHANNELS_SPACE_PATH, DIRECT_PATH } from '../../pages/paths'; -// 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. +// MobileTabsPager statically imports the Channels and Bots feature modules and +// renders only on `mobile && native` (below), so lazy-loading it here keeps the +// pager glue (gesture/geometry/header) off non-native targets. NOTE: this no +// longer keeps Channels/Bots off the boot bundle — those listing tabs are now +// imported eagerly in `Router.tsx` (their lazy-split caused a web tab-switch +// flicker for ~5.6 KB gz; see `docs/ai/bugs.md`), so they ship in the entry on +// every target regardless. On native the pager chunk streams from the local APK +// filesystem (no network), so its null Suspense fallback is imperceptible. const MobileTabsPager = React.lazy(() => import('./MobileTabsPager').then((m) => ({ default: m.MobileTabsPager })) ); diff --git a/src/app/pages/Router.tsx b/src/app/pages/Router.tsx index 60c2394d..2b45fe53 100644 --- a/src/app/pages/Router.tsx +++ b/src/app/pages/Router.tsx @@ -55,6 +55,11 @@ import { ClientBindAtoms, ClientLayout, ClientRoot } from './client'; import { HomeRouteRoomProvider } from './client/home'; import { Direct, DirectCreate, DirectRouteRoomProvider } from './client/direct'; import { ChannelPickPlaceholder } from './client/channels/ChannelPickPlaceholder'; +// Bots/Channels LISTING tabs are eager (not React.lazy) — see the code-split +// comment below. Imported from the concrete source files (not the barrels) so +// the eager edge doesn't drag the still-lazy BotExperienceHost chunk into boot. +import { Bots } from './client/bots/Bots'; +import { Channels, ChannelsRootNav } from './client/channels/Channels'; import { RouteSpaceProvider, Space, SpaceRouteRoomProvider, SpaceSearch } from './client/space'; import { setAfterLoginRedirectPath } from './afterLoginRedirectPath'; import { WelcomePage } from './client/WelcomePage'; @@ -127,11 +132,18 @@ function UserLinkRedirect() { // 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. +// explore, create and the bot-experience host 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. // +// The Channels/Bots LISTING tabs are deliberately NOT here — they are imported +// eagerly (top of file). Code-splitting those two tiny chunks (~5.6 KB gz) won +// almost nothing on first load, but made every first switch to the tab suspend +// and paint a fallback in the nav column that reflowed the layout — a visible +// web-only "tab-switch flicker". Eager import restores the pre-split behavior. +// See `docs/ai/bugs.md`. +// // 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 @@ -152,16 +164,9 @@ const FeaturedRooms = React.lazy(() => 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 })) ); @@ -313,7 +318,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) - {routeSuspense()} + } > @@ -343,7 +348,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) - {routeSuspense()} + } > @@ -358,7 +363,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) - {routeSuspense()} + } >