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