From 7bbe73e903e7475108ecf9067d8312bc9f92a073 Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Wed, 28 Oct 2026 12:58:14 +0700 Subject: [PATCH] perf: code-split and defer the notification bell --- src/components/Layout.tsx | 6 +-- src/components/NotificationBell.tsx | 10 +++- src/components/NotificationBellLazy.tsx | 72 +++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 src/components/NotificationBellLazy.tsx diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 7c3d89b..b169c87 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -59,7 +59,7 @@ import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; import { useAuthor } from '@/hooks/useAuthor'; import { genUserName } from '@/lib/genUserName'; import { NewsletterOverlay } from '@/components/NewsletterOverlay'; -import { NotificationBell } from '@/components/NotificationBell'; +import { NotificationBellLazy } from '@/components/NotificationBellLazy'; import { OsNotificationBridge } from '@/components/OsNotificationBridge'; // Cashu balance widget is lazy — see components/wallet/CashuBalance.tsx for why @@ -781,7 +781,7 @@ export function Layout({ children, noPadding }: LayoutProps) { )} {/* Notifications (crew invites & membership requests) */} - {user && } + {user && } {/* Offline Indicator */} {/* Language Toggle */} @@ -825,7 +825,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
{/* Notifications (crew invites & membership requests) */} - {user && } + {user && } {/* User menu when logged in / Login button when not. IMPORTANT: LoginArea hosts the SignupDialog. The signup flow logs the user in at the *first* step (to publish their profile), then diff --git a/src/components/NotificationBell.tsx b/src/components/NotificationBell.tsx index 9966838..2be5a04 100644 --- a/src/components/NotificationBell.tsx +++ b/src/components/NotificationBell.tsx @@ -209,9 +209,15 @@ function EmptyState({ label }: { label: string }) { * Unread state is tracked by a per-account "last read" timestamp; opening the * bell (or hitting "mark all read") clears the badge. Only renders when logged in. */ -export function NotificationBell({ className }: { className?: string }) { +export function NotificationBell({ + className, + defaultOpen = false, +}: { + className?: string; + defaultOpen?: boolean; +}) { const { t } = useTranslation(); - const [open, setOpen] = useState(false); + const [open, setOpen] = useState(defaultOpen); const { user, invites, listedCrews, joinRequests, collectiveRequests, isLoading: crewLoading } = useCrewNotifications(); const { notifications: activity, isLoading: activityLoading } = useActivityNotifications(); diff --git a/src/components/NotificationBellLazy.tsx b/src/components/NotificationBellLazy.tsx new file mode 100644 index 0000000..c530de9 --- /dev/null +++ b/src/components/NotificationBellLazy.tsx @@ -0,0 +1,72 @@ +import { Suspense, useEffect, useState } from 'react'; +import { Bell } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { useTranslation } from '@/contexts/language'; +import { lazyWithRetry } from '@/lib/lazyWithRetry'; +import { cn } from '@/lib/utils'; + +type BellProps = { className?: string; defaultOpen?: boolean }; + +// Keep the whole notification center (and its five data sources) out of the +// entry bundle. It loads after first paint on idle, or immediately if the user +// taps the bell. The placeholder is the same footprint as the real button. +const LazyBell = lazyWithRetry(() => + import('@/components/NotificationBell').then((m) => ({ + default: m.NotificationBell as unknown as React.ComponentType, + })), +) as React.LazyExoticComponent>; + +function scheduleIdle(callback: () => void): () => void { + const win = window as Window & { + requestIdleCallback?: (cb: () => void, opts?: { timeout: number }) => number; + cancelIdleCallback?: (id: number) => void; + }; + if (win.requestIdleCallback) { + const id = win.requestIdleCallback(callback, { timeout: 1800 }); + return () => win.cancelIdleCallback?.(id); + } + const id = window.setTimeout(callback, 1200); + return () => window.clearTimeout(id); +} + +/** + * Deferred notification bell. The bell icon is present immediately; the data + * hooks and lazy chunk arrive once the map/app has had its first paint. + */ +export function NotificationBellLazy({ className }: { className?: string }) { + const { t } = useTranslation(); + const [armed, setArmed] = useState(false); + const [openOnMount, setOpenOnMount] = useState(false); + + useEffect(() => { + if (armed) return; + return scheduleIdle(() => setArmed(true)); + }, [armed]); + + const placeholder = ( + + ); + + if (!armed) return placeholder; + + return ( + + + + ); +}