Skip to content

Commit d89b9f2

Browse files
thephezclaude
andcommitted
perf(dashmint-lab): defer SDK + WASM off the critical path
Lighthouse FCP/LCP were 5.1s/5.8s because the ~8MB evo-sdk WASM bundle loaded synchronously at app boot. Switch SessionContext to a dynamic import for createClient + IdentityKeyManager so Vite splits the SDK into its own chunk that fetches after the shell paints. Auto-browse is also deferred via requestAnimationFrame. Split contract.ts: storage helpers (loadStoredContractId, fetchContractOwnerId, ...) move to contractStorage.ts so SessionContext can import them without dragging @dashevo/evo-sdk into the entry chunk. Entry chunk drops from a single ~6MB-gzip bundle to 89KB gzip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ee75705 commit d89b9f2

5 files changed

Lines changed: 86 additions & 53 deletions

File tree

example-apps/dashmint-lab/src/App.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,12 @@ function App() {
6767
}, [refreshBalance]);
6868

6969
// Auto-connect in browse-only mode so read tabs work without login.
70+
// Defer to the next frame so the shell paints before the SDK chunk
71+
// (~8MB WASM) starts downloading.
7072
useEffect(() => {
71-
if (status === "idle") void browseOnly();
73+
if (status !== "idle") return;
74+
const raf = requestAnimationFrame(() => void browseOnly());
75+
return () => cancelAnimationFrame(raf);
7276
}, [status, browseOnly]);
7377

7478
// Default sub-tab: "My" when logged in, otherwise "All".

example-apps/dashmint-lab/src/dash/contract.ts

Lines changed: 14 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* NFT card data contract schema + ensureContract().
2+
* NFT card data contract schema + registerContract / ensureContract.
33
*
44
* WHAT: A Dash Platform "data contract" defines the schema for documents.
55
* This one describes a single document type (`card`) with four fields
@@ -11,13 +11,26 @@
1111
* tradeMode: 1 — documents can be priced and purchased (0 to disable)
1212
* creationRestrictionMode: 1 — (1 - only the contract owner can mint; 0 - anyone can mint)
1313
*
14+
* Storage helpers (loadStoredContractId, saveContractId, …) and the owner
15+
* lookup live in contractStorage.ts so they can be imported without
16+
* pulling the @dashevo/evo-sdk runtime into the entry bundle.
17+
*
1418
* SDK methods: new DataContract({ ... }), sdk.contracts.publish(...)
1519
*/
1620
import { DataContract } from "@dashevo/evo-sdk";
1721

22+
import { loadStoredContractId, saveContractId } from "./contractStorage";
1823
import type { Logger } from "./logger";
1924
import type { DashKeyManager, DashSdk } from "./types";
2025

26+
export {
27+
DEFAULT_CONTRACT_ID,
28+
clearStoredContractId,
29+
fetchContractOwnerId,
30+
loadStoredContractId,
31+
saveContractId,
32+
} from "./contractStorage";
33+
2134
export const CARD_SCHEMAS = {
2235
card: {
2336
type: "object",
@@ -62,49 +75,6 @@ export const CARD_SCHEMAS = {
6275
},
6376
} as const;
6477

65-
/**
66-
* Fetch the owner identity ID for a given data contract.
67-
*
68-
* SDK method: sdk.contracts.fetch(...)
69-
*/
70-
export async function fetchContractOwnerId({
71-
sdk,
72-
contractId,
73-
}: {
74-
sdk: DashSdk;
75-
contractId: string;
76-
}): Promise<string | null> {
77-
const contract = await sdk.contracts.fetch(contractId);
78-
if (!contract) return null;
79-
const json =
80-
typeof contract.toJSON === "function" ? contract.toJSON() : contract;
81-
const ownerId = json.$ownerId ?? json.ownerId ?? null;
82-
return ownerId ? String(ownerId) : null;
83-
}
84-
85-
const STORAGE_KEY = "dashmint-lab.contractId";
86-
87-
/**
88-
* Default contract ID baked into the tutorial so browse-only mode works
89-
* on a fresh machine without any setup. Comes from the original
90-
* HTML tutorial's pre-deployed testnet contract. Users can override it
91-
* in the Settings modal or register their own.
92-
*/
93-
export const DEFAULT_CONTRACT_ID =
94-
"4eJR4pgV9mQdyoodfTTwFUp3SYBRJbUrJ5X1ViN2zBhY";
95-
96-
export function loadStoredContractId(): string | null {
97-
return localStorage.getItem(STORAGE_KEY) ?? DEFAULT_CONTRACT_ID;
98-
}
99-
100-
export function saveContractId(id: string): void {
101-
localStorage.setItem(STORAGE_KEY, id);
102-
}
103-
104-
export function clearStoredContractId(): void {
105-
localStorage.removeItem(STORAGE_KEY);
106-
}
107-
10878
/**
10979
* Register a fresh NFT card data contract on Platform and persist its ID.
11080
*
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* Contract ID persistence + owner lookup. Split from contract.ts so the
3+
* session bootstrap can import these helpers without dragging the
4+
* @dashevo/evo-sdk module (and its WASM bundle) into the entry chunk.
5+
*
6+
* SDK method (fetchContractOwnerId): sdk.contracts.fetch(...)
7+
*/
8+
import type { DashSdk } from "./types";
9+
10+
const STORAGE_KEY = "dashmint-lab.contractId";
11+
12+
/**
13+
* Default contract ID baked into the tutorial so browse-only mode works
14+
* on a fresh machine without any setup. Comes from the original
15+
* HTML tutorial's pre-deployed testnet contract. Users can override it
16+
* in the Settings modal or register their own.
17+
*/
18+
export const DEFAULT_CONTRACT_ID =
19+
"4eJR4pgV9mQdyoodfTTwFUp3SYBRJbUrJ5X1ViN2zBhY";
20+
21+
export function loadStoredContractId(): string | null {
22+
return localStorage.getItem(STORAGE_KEY) ?? DEFAULT_CONTRACT_ID;
23+
}
24+
25+
export function saveContractId(id: string): void {
26+
localStorage.setItem(STORAGE_KEY, id);
27+
}
28+
29+
export function clearStoredContractId(): void {
30+
localStorage.removeItem(STORAGE_KEY);
31+
}
32+
33+
export async function fetchContractOwnerId({
34+
sdk,
35+
contractId,
36+
}: {
37+
sdk: DashSdk;
38+
contractId: string;
39+
}): Promise<string | null> {
40+
const contract = await sdk.contracts.fetch(contractId);
41+
if (!contract) return null;
42+
const json =
43+
typeof contract.toJSON === "function" ? contract.toJSON() : contract;
44+
const ownerId = json.$ownerId ?? json.ownerId ?? null;
45+
return ownerId ? String(ownerId) : null;
46+
}

example-apps/dashmint-lab/src/session/SessionContext.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,29 @@ import {
2424
type ReactNode,
2525
} from "react";
2626

27-
import { createClient } from "../dash/client";
28-
import { IdentityKeyManager } from "../dash/keyManager";
2927
import {
3028
clearStoredContractId,
3129
fetchContractOwnerId,
3230
loadStoredContractId,
3331
saveContractId,
34-
} from "../dash/contract";
32+
} from "../dash/contractStorage";
3533
import { errorMessage, type Logger } from "../dash/logger";
3634
import type { DashKeyManager, DashSdk } from "../dash/types";
3735

36+
// The SDK + IdentityKeyManager pull in @dashevo/evo-sdk (and its ~8MB WASM
37+
// bundle), so we load them lazily on first use to keep the app shell off
38+
// the critical path. Cached after first call.
39+
let sdkModulePromise: Promise<{
40+
createClient: (network: string) => Promise<DashSdk>;
41+
IdentityKeyManager: typeof import("../../../../setupDashClient-core.mjs").IdentityKeyManager;
42+
}> | null = null;
43+
function loadSdkModule() {
44+
if (!sdkModulePromise) {
45+
sdkModulePromise = import("../../../../setupDashClient-core.mjs");
46+
}
47+
return sdkModulePromise;
48+
}
49+
3850
export type SessionStatus =
3951
| "idle"
4052
| "connecting"
@@ -162,6 +174,7 @@ export function SessionProvider({ children }: { children: ReactNode }) {
162174
setStatus("connecting");
163175
setError(null);
164176
log("Connecting to Dash Platform testnet…");
177+
const { createClient } = await loadSdkModule();
165178
const connected = await createClient("testnet");
166179
log("Connected to testnet.", "info");
167180
setSdk(connected);
@@ -176,6 +189,7 @@ export function SessionProvider({ children }: { children: ReactNode }) {
176189
try {
177190
const connected = sdk ?? (await connect());
178191
log("Deriving identity keys from mnemonic…");
192+
const { IdentityKeyManager } = await loadSdkModule();
179193
const km = await IdentityKeyManager.create({
180194
sdk: connected,
181195
mnemonic: trimmed,

example-apps/dashmint-lab/test/SessionContext.test.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,16 @@ const {
3434
mockToastError: vi.fn(),
3535
}));
3636

37-
vi.mock("../src/dash/client", () => ({
37+
// SessionContext dynamic-imports the SDK core module directly (not via the
38+
// app's client.ts/keyManager.ts wrappers), so mock that module instead.
39+
vi.mock("../../../setupDashClient-core.mjs", () => ({
3840
createClient: mockCreateClient,
39-
}));
40-
41-
vi.mock("../src/dash/keyManager", () => ({
4241
IdentityKeyManager: {
4342
create: mockIdentityKeyManagerCreate,
4443
},
4544
}));
4645

47-
vi.mock("../src/dash/contract", () => ({
46+
vi.mock("../src/dash/contractStorage", () => ({
4847
DEFAULT_CONTRACT_ID: "default-contract-id",
4948
fetchContractOwnerId: mockFetchContractOwnerId,
5049
loadStoredContractId: mockLoadStoredContractId,

0 commit comments

Comments
 (0)