perf(bundle): lazy-load the Cashu balance; load @getalby/sdk on demand
- Layout (app shell) imported useCashu, pulling the Cashu stack into the entry chunk every visitor downloads. The three balance widgets now live in components/wallet/CashuBalance.tsx and load via React.lazy. - useNWC statically imported @getalby/sdk; it is now a type-only import plus a dynamic import at call sites, so the SDK only loads when NWC is used. Entry chunk: 1879 KB -> 1835 KB.
This commit is contained in:
parent
fb5ebb5cd4
commit
e6f1fe2673
3 changed files with 73 additions and 36 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useRef } from 'react';
|
import { useState, useRef, lazy, Suspense } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
@ -20,7 +20,6 @@ import { SasquatchGuide } from '@/components/SasquatchGuide';
|
||||||
import { useSasquatchGuide } from '@/hooks/useSasquatchGuide';
|
import { useSasquatchGuide } from '@/hooks/useSasquatchGuide';
|
||||||
import { BackupReminder } from '@/components/BackupReminder';
|
import { BackupReminder } from '@/components/BackupReminder';
|
||||||
import { LanguageToggle } from '@/components/LanguageToggle';
|
import { LanguageToggle } from '@/components/LanguageToggle';
|
||||||
import { useCashu } from '@/hooks/wallet/useCashu';
|
|
||||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||||
import { useTranslation } from '@/contexts/LanguageContext';
|
import { useTranslation } from '@/contexts/LanguageContext';
|
||||||
import { useLoggedInAccounts } from '@/hooks/useLoggedInAccounts';
|
import { useLoggedInAccounts } from '@/hooks/useLoggedInAccounts';
|
||||||
|
|
@ -63,6 +62,18 @@ import { genUserName } from '@/lib/genUserName';
|
||||||
import { NewsletterOverlay } from '@/components/NewsletterOverlay';
|
import { NewsletterOverlay } from '@/components/NewsletterOverlay';
|
||||||
import { NotificationBell } from '@/components/NotificationBell';
|
import { NotificationBell } from '@/components/NotificationBell';
|
||||||
|
|
||||||
|
// Cashu balance widget is lazy — see components/wallet/CashuBalance.tsx for why
|
||||||
|
// (keeps the Cashu stack out of the initial bundle).
|
||||||
|
const LazyCashuBalanceText = lazy(() =>
|
||||||
|
import('@/components/wallet/CashuBalance').then((m) => ({ default: m.CashuBalanceText })),
|
||||||
|
);
|
||||||
|
const LazyCashuBalanceBadge = lazy(() =>
|
||||||
|
import('@/components/wallet/CashuBalance').then((m) => ({ default: m.CashuBalanceBadge })),
|
||||||
|
);
|
||||||
|
const LazyCashuMenuBalance = lazy(() =>
|
||||||
|
import('@/components/wallet/CashuBalance').then((m) => ({ default: m.CashuMenuBalance })),
|
||||||
|
);
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
/** If true, removes the bottom padding that prevents bottom nav overlap.
|
/** If true, removes the bottom padding that prevents bottom nav overlap.
|
||||||
|
|
@ -79,17 +90,6 @@ const navItemKeys = [
|
||||||
{ href: '/crews', labelKey: 'nav.crews', icon: Users },
|
{ href: '/crews', labelKey: 'nav.crews', icon: Users },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Format sats for display (e.g., 1234 -> "1.2k", 12345 -> "12k")
|
|
||||||
function formatSatsCompact(sats: number): string {
|
|
||||||
if (sats >= 1000000) {
|
|
||||||
return `${(sats / 1000000).toFixed(1)}M`;
|
|
||||||
}
|
|
||||||
if (sats >= 1000) {
|
|
||||||
return `${(sats / 1000).toFixed(sats >= 10000 ? 0 : 1)}k`;
|
|
||||||
}
|
|
||||||
return sats.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function NavLink({
|
function NavLink({
|
||||||
href,
|
href,
|
||||||
label,
|
label,
|
||||||
|
|
@ -288,7 +288,6 @@ function MobileHeaderUserMenu({
|
||||||
|
|
||||||
function MobileNav({
|
function MobileNav({
|
||||||
isActive,
|
isActive,
|
||||||
cashuBalance,
|
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
userPubkey,
|
userPubkey,
|
||||||
userAvatar,
|
userAvatar,
|
||||||
|
|
@ -299,7 +298,6 @@ function MobileNav({
|
||||||
middleTabs,
|
middleTabs,
|
||||||
}: {
|
}: {
|
||||||
isActive: (path: string) => boolean;
|
isActive: (path: string) => boolean;
|
||||||
cashuBalance: number;
|
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
userPubkey?: string;
|
userPubkey?: string;
|
||||||
userAvatar?: string;
|
userAvatar?: string;
|
||||||
|
|
@ -370,11 +368,9 @@ function MobileNav({
|
||||||
{isWallet ? (
|
{isWallet ? (
|
||||||
<>
|
<>
|
||||||
<Wallet className={cn('h-[22px] w-[22px] transition-transform duration-300', active ? 'scale-110' : 'group-active:scale-95')} />
|
<Wallet className={cn('h-[22px] w-[22px] transition-transform duration-300', active ? 'scale-110' : 'group-active:scale-95')} />
|
||||||
{cashuBalance > 0 && (
|
<Suspense fallback={null}>
|
||||||
<span className="absolute -top-1 -right-1.5 min-w-[16px] h-4 px-1 text-[8px] font-bold bg-orange-500 text-white rounded-full flex items-center justify-center leading-none ring-2 ring-black">
|
<LazyCashuBalanceBadge />
|
||||||
{formatSatsCompact(cashuBalance)}
|
</Suspense>
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<tab.icon className={cn('h-[22px] w-[22px] transition-transform duration-300', active ? 'scale-110' : 'group-active:scale-95')} />
|
<tab.icon className={cn('h-[22px] w-[22px] transition-transform duration-300', active ? 'scale-110' : 'group-active:scale-95')} />
|
||||||
|
|
@ -474,7 +470,6 @@ export function Layout({ children, noPadding }: LayoutProps) {
|
||||||
const [showAddAccount, setShowAddAccount] = useState(false);
|
const [showAddAccount, setShowAddAccount] = useState(false);
|
||||||
const { currentMessage, dismissCurrentMessage } = useSasquatchGuide();
|
const { currentMessage, dismissCurrentMessage } = useSasquatchGuide();
|
||||||
const { user } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
const { totalBalance: cashuBalance, isInitialized: hasCashu } = useCashu();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { currentUser: currentAccount, removeLogin } = useLoggedInAccounts();
|
const { currentUser: currentAccount, removeLogin } = useLoggedInAccounts();
|
||||||
const author = useAuthor(user?.pubkey || '');
|
const author = useAuthor(user?.pubkey || '');
|
||||||
|
|
@ -659,11 +654,9 @@ export function Layout({ children, noPadding }: LayoutProps) {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Wallet className="h-5 w-5" />
|
<Wallet className="h-5 w-5" />
|
||||||
{hasCashu && cashuBalance > 0 && (
|
<Suspense fallback={null}>
|
||||||
<span className="text-sm font-bold">
|
<LazyCashuBalanceText />
|
||||||
{formatSatsCompact(cashuBalance)}
|
</Suspense>
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
@ -782,14 +775,9 @@ export function Layout({ children, noPadding }: LayoutProps) {
|
||||||
<p className="text-xs text-muted-foreground">Ecash</p>
|
<p className="text-xs text-muted-foreground">Ecash</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{hasCashu && (
|
<Suspense fallback={null}>
|
||||||
<div className="text-right">
|
<LazyCashuMenuBalance />
|
||||||
<p className="font-bold text-orange-500">
|
</Suspense>
|
||||||
{cashuBalance.toLocaleString()}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-muted-foreground">sats</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -1047,7 +1035,6 @@ export function Layout({ children, noPadding }: LayoutProps) {
|
||||||
{/* Mobile Bottom Navigation */}
|
{/* Mobile Bottom Navigation */}
|
||||||
<MobileNav
|
<MobileNav
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
cashuBalance={cashuBalance}
|
|
||||||
isLoggedIn={!!user}
|
isLoggedIn={!!user}
|
||||||
userPubkey={user?.pubkey}
|
userPubkey={user?.pubkey}
|
||||||
userAvatar={userAvatar}
|
userAvatar={userAvatar}
|
||||||
|
|
|
||||||
46
src/components/wallet/CashuBalance.tsx
Normal file
46
src/components/wallet/CashuBalance.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { useCashu } from '@/hooks/wallet/useCashu';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cashu balance displays, split out of `Layout`.
|
||||||
|
*
|
||||||
|
* WHY: `Layout` is in the initial app shell, and importing `useCashu` there
|
||||||
|
* pulled the entire Cashu stack (`coco-cashu-core` + `@cashu/cashu-ts` + seed
|
||||||
|
* crypto) into the entry chunk that every visitor downloads. These three tiny
|
||||||
|
* components are loaded lazily by `Layout`, so the wallet code only ships when
|
||||||
|
* a balance is actually displayed.
|
||||||
|
*/
|
||||||
|
function formatSatsCompact(sats: number): string {
|
||||||
|
if (sats >= 1_000_000) return `${(sats / 1_000_000).toFixed(1)}M`;
|
||||||
|
if (sats >= 1000) return `${(sats / 1000).toFixed(sats >= 10000 ? 0 : 1)}k`;
|
||||||
|
return sats.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Desktop header: sats next to the wallet nav button. */
|
||||||
|
export function CashuBalanceText() {
|
||||||
|
const { totalBalance, isInitialized } = useCashu();
|
||||||
|
if (!isInitialized || totalBalance <= 0) return null;
|
||||||
|
return <span className="text-sm font-bold">{formatSatsCompact(totalBalance)}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mobile bottom nav: small badge on the wallet tab. */
|
||||||
|
export function CashuBalanceBadge() {
|
||||||
|
const { totalBalance, isInitialized } = useCashu();
|
||||||
|
if (!isInitialized || totalBalance <= 0) return null;
|
||||||
|
return (
|
||||||
|
<span className="absolute -top-1 -right-1.5 min-w-[16px] h-4 px-1 text-[8px] font-bold bg-orange-500 text-white rounded-full flex items-center justify-center leading-none ring-2 ring-black">
|
||||||
|
{formatSatsCompact(totalBalance)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mobile menu: the "Cashu / Ecash" row's balance. */
|
||||||
|
export function CashuMenuBalance() {
|
||||||
|
const { totalBalance, isInitialized } = useCashu();
|
||||||
|
if (!isInitialized) return null;
|
||||||
|
return (
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="font-bold text-orange-500">{totalBalance.toLocaleString()}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">sats</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { useLocalStorage } from '@/hooks/useLocalStorage';
|
import { useLocalStorage } from '@/hooks/useLocalStorage';
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
import { LN } from '@getalby/sdk';
|
import type { LN } from '@getalby/sdk';
|
||||||
|
|
||||||
export interface NWCConnection {
|
export interface NWCConnection {
|
||||||
connectionString: string;
|
connectionString: string;
|
||||||
|
|
@ -61,6 +61,8 @@ export function useNWCInternal() {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Load the Getalby SDK on demand so it stays out of the initial bundle.
|
||||||
|
const { LN } = await import('@getalby/sdk');
|
||||||
let timeoutId: NodeJS.Timeout | undefined;
|
let timeoutId: NodeJS.Timeout | undefined;
|
||||||
const testPromise = new Promise((resolve, reject) => {
|
const testPromise = new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -165,6 +167,8 @@ export function useNWCInternal() {
|
||||||
throw new Error('Invalid connection: missing connection string');
|
throw new Error('Invalid connection: missing connection string');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load the Getalby SDK on demand so it stays out of the initial bundle.
|
||||||
|
const { LN } = await import('@getalby/sdk');
|
||||||
let client: LN;
|
let client: LN;
|
||||||
try {
|
try {
|
||||||
client = new LN(connection.connectionString);
|
client = new LN(connection.connectionString);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue