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 { nip19 } from 'nostr-tools';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
|
@ -20,7 +20,6 @@ import { SasquatchGuide } from '@/components/SasquatchGuide';
|
|||
import { useSasquatchGuide } from '@/hooks/useSasquatchGuide';
|
||||
import { BackupReminder } from '@/components/BackupReminder';
|
||||
import { LanguageToggle } from '@/components/LanguageToggle';
|
||||
import { useCashu } from '@/hooks/wallet/useCashu';
|
||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
import { useTranslation } from '@/contexts/LanguageContext';
|
||||
import { useLoggedInAccounts } from '@/hooks/useLoggedInAccounts';
|
||||
|
|
@ -63,6 +62,18 @@ import { genUserName } from '@/lib/genUserName';
|
|||
import { NewsletterOverlay } from '@/components/NewsletterOverlay';
|
||||
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 {
|
||||
children: React.ReactNode;
|
||||
/** If true, removes the bottom padding that prevents bottom nav overlap.
|
||||
|
|
@ -79,17 +90,6 @@ const navItemKeys = [
|
|||
{ 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({
|
||||
href,
|
||||
label,
|
||||
|
|
@ -288,7 +288,6 @@ function MobileHeaderUserMenu({
|
|||
|
||||
function MobileNav({
|
||||
isActive,
|
||||
cashuBalance,
|
||||
isLoggedIn,
|
||||
userPubkey,
|
||||
userAvatar,
|
||||
|
|
@ -299,7 +298,6 @@ function MobileNav({
|
|||
middleTabs,
|
||||
}: {
|
||||
isActive: (path: string) => boolean;
|
||||
cashuBalance: number;
|
||||
isLoggedIn: boolean;
|
||||
userPubkey?: string;
|
||||
userAvatar?: string;
|
||||
|
|
@ -370,11 +368,9 @@ function MobileNav({
|
|||
{isWallet ? (
|
||||
<>
|
||||
<Wallet className={cn('h-[22px] w-[22px] transition-transform duration-300', active ? 'scale-110' : 'group-active:scale-95')} />
|
||||
{cashuBalance > 0 && (
|
||||
<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(cashuBalance)}
|
||||
</span>
|
||||
)}
|
||||
<Suspense fallback={null}>
|
||||
<LazyCashuBalanceBadge />
|
||||
</Suspense>
|
||||
</>
|
||||
) : (
|
||||
<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 { currentMessage, dismissCurrentMessage } = useSasquatchGuide();
|
||||
const { user } = useCurrentUser();
|
||||
const { totalBalance: cashuBalance, isInitialized: hasCashu } = useCashu();
|
||||
const { t } = useTranslation();
|
||||
const { currentUser: currentAccount, removeLogin } = useLoggedInAccounts();
|
||||
const author = useAuthor(user?.pubkey || '');
|
||||
|
|
@ -659,11 +654,9 @@ export function Layout({ children, noPadding }: LayoutProps) {
|
|||
)}
|
||||
>
|
||||
<Wallet className="h-5 w-5" />
|
||||
{hasCashu && cashuBalance > 0 && (
|
||||
<span className="text-sm font-bold">
|
||||
{formatSatsCompact(cashuBalance)}
|
||||
</span>
|
||||
)}
|
||||
<Suspense fallback={null}>
|
||||
<LazyCashuBalanceText />
|
||||
</Suspense>
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
|
|
@ -782,14 +775,9 @@ export function Layout({ children, noPadding }: LayoutProps) {
|
|||
<p className="text-xs text-muted-foreground">Ecash</p>
|
||||
</div>
|
||||
</div>
|
||||
{hasCashu && (
|
||||
<div className="text-right">
|
||||
<p className="font-bold text-orange-500">
|
||||
{cashuBalance.toLocaleString()}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">sats</p>
|
||||
</div>
|
||||
)}
|
||||
<Suspense fallback={null}>
|
||||
<LazyCashuMenuBalance />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
|
@ -1047,7 +1035,6 @@ export function Layout({ children, noPadding }: LayoutProps) {
|
|||
{/* Mobile Bottom Navigation */}
|
||||
<MobileNav
|
||||
isActive={isActive}
|
||||
cashuBalance={cashuBalance}
|
||||
isLoggedIn={!!user}
|
||||
userPubkey={user?.pubkey}
|
||||
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 { useLocalStorage } from '@/hooks/useLocalStorage';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { LN } from '@getalby/sdk';
|
||||
import type { LN } from '@getalby/sdk';
|
||||
|
||||
export interface NWCConnection {
|
||||
connectionString: string;
|
||||
|
|
@ -61,6 +61,8 @@ export function useNWCInternal() {
|
|||
}
|
||||
|
||||
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;
|
||||
const testPromise = new Promise((resolve, reject) => {
|
||||
try {
|
||||
|
|
@ -165,6 +167,8 @@ export function useNWCInternal() {
|
|||
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;
|
||||
try {
|
||||
client = new LN(connection.connectionString);
|
||||
|
|
|
|||
Loading…
Reference in a new issue