Skip to content

Commit c0a575c

Browse files
committed
feat: dynamically load Firebase and lazy-load ChatMessage component
- Convert Firebase app and auth initialization to async with dynamic imports. The vendor-firebase chunk (~155KB) is now only fetched when a user interacts with auth (Faucet page), not eagerly on every page. - Lazy-load ChatMessage via React.lazy() so react-markdown, react-syntax-highlighter, remark-gfm, and Prism language grammars are deferred until chat messages are actually rendered. - Both changes reduce the initial page load for the vast majority of users who never use the Faucet or the chat widget.
1 parent 8c55133 commit c0a575c

File tree

6 files changed

+100
-63
lines changed

6 files changed

+100
-63
lines changed

src/components/chat-widget/chat-dialog.tsx

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@ import { useChatbot } from "@aptos-labs/ai-chatbot-client";
22
import * as Dialog from "@radix-ui/react-dialog";
33
import * as ScrollArea from "@radix-ui/react-scroll-area";
44
import { ChevronLeft, ChevronRight, Pencil, Share2, Trash2, X } from "lucide-react";
5-
import { useCallback, useEffect, useRef, useState } from "react";
5+
import { lazy, Suspense, useCallback, useEffect, useRef, useState } from "react";
66
import type { ChatInputRef } from "./chat-input";
77
import { ChatInput } from "./chat-input";
8-
import { ChatMessage } from "./chat-message";
98
import { ChatSidebar } from "./chat-sidebar";
109
import { ShareModal } from "./share-modal";
1110
import type { Chat, ChatWidgetProps, Message } from "./types";
1211

12+
// Lazy-load ChatMessage to defer react-markdown, react-syntax-highlighter,
13+
// and all Prism language grammars until a message actually needs rendering.
14+
const ChatMessage = lazy(() => import("./chat-message").then((m) => ({ default: m.ChatMessage })));
15+
1316
export interface ChatDialogProps extends Omit<ChatWidgetProps, "chats"> {
1417
open: boolean;
1518
onOpenChange: (open: boolean) => void;
@@ -233,17 +236,19 @@ export function ChatDialog({
233236
</div>
234237
</div>
235238
)}
236-
{convertedMessages.map((message: Message) => (
237-
<ChatMessage
238-
key={message.id}
239-
message={message}
240-
onCopy={() => onCopyMessage?.(message.id)}
241-
onFeedback={(messageId, feedback) =>
242-
onMessageFeedback?.(messageId, feedback)
243-
}
244-
className={messageClassName}
245-
/>
246-
))}
239+
<Suspense fallback={null}>
240+
{convertedMessages.map((message: Message) => (
241+
<ChatMessage
242+
key={message.id}
243+
message={message}
244+
onCopy={() => onCopyMessage?.(message.id)}
245+
onFeedback={(messageId, feedback) =>
246+
onMessageFeedback?.(messageId, feedback)
247+
}
248+
className={messageClassName}
249+
/>
250+
))}
251+
</Suspense>
247252
{isGenerating && !isTyping && (
248253
<div className="chat-message">
249254
<div className="chat-message-content">

src/components/chat-widget/chat-message.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import type React from "react";
33
import { type ComponentProps, useEffect, useRef, useState } from "react";
44
import ReactMarkdown from "react-markdown";
55
import { PrismLight as SyntaxHighlighter } from "react-syntax-highlighter";
6-
import oneDark from "react-syntax-highlighter/dist/cjs/styles/prism/one-dark";
76
import bashLang from "react-syntax-highlighter/dist/cjs/languages/prism/bash";
87
import goLang from "react-syntax-highlighter/dist/cjs/languages/prism/go";
98
import javascriptLang from "react-syntax-highlighter/dist/cjs/languages/prism/javascript";
@@ -15,8 +14,9 @@ import tomlLang from "react-syntax-highlighter/dist/cjs/languages/prism/toml";
1514
import tsxLang from "react-syntax-highlighter/dist/cjs/languages/prism/tsx";
1615
import typescriptLang from "react-syntax-highlighter/dist/cjs/languages/prism/typescript";
1716
import yamlLang from "react-syntax-highlighter/dist/cjs/languages/prism/yaml";
18-
import moveLang from "./prism-move";
17+
import oneDark from "react-syntax-highlighter/dist/cjs/styles/prism/one-dark";
1918
import remarkGfm from "remark-gfm";
19+
import moveLang from "./prism-move";
2020
import type { Message } from "./types";
2121

2222
// CJS modules export as { default: fn }, extract the actual language function

src/components/chat-widget/prism-move.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@
88
* Compatible with react-syntax-highlighter's PrismLight (refractor-based).
99
*/
1010

11-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
11+
// biome-ignore lint/suspicious/noExplicitAny: Prism's refractor registration API is untyped
1212
function move(Prism: any) {
13-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
1413
Prism.languages.move = {
1514
comment: [
1615
{ pattern: /\/\/\/.*/, greedy: true, alias: "doc-comment" },

src/lib/firebase/app.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,29 @@ import {
44
PUBLIC_FIREBASE_AUTH_DOMAIN,
55
PUBLIC_FIREBASE_PROJECT_ID,
66
} from "astro:env/client";
7-
import { initializeApp } from "@firebase/app";
8-
import { singletonGetter } from "~/lib/singletonGetter";
7+
import type { FirebaseApp } from "@firebase/app";
98
import { FirebaseError } from "./error";
109

11-
export const getFirebaseApp = singletonGetter(() => {
10+
let appInstance: FirebaseApp | null = null;
11+
12+
/**
13+
* Lazily initializes and returns the Firebase app instance.
14+
* The @firebase/app module (~100KB gzipped) is dynamically imported
15+
* so it is only downloaded when Firebase is actually needed.
16+
*/
17+
export async function getFirebaseApp(): Promise<FirebaseApp> {
18+
if (appInstance) return appInstance;
19+
1220
try {
13-
return initializeApp({
21+
const { initializeApp } = await import("@firebase/app");
22+
appInstance = initializeApp({
1423
apiKey: PUBLIC_FIREBASE_API_KEY,
1524
authDomain: PUBLIC_FIREBASE_AUTH_DOMAIN,
1625
projectId: PUBLIC_FIREBASE_PROJECT_ID,
1726
appId: PUBLIC_FIREBASE_APP_ID,
1827
});
28+
return appInstance;
1929
} catch (e) {
2030
throw new FirebaseError("Could not instantiate firebase", { cause: e });
2131
}
22-
});
32+
}

src/lib/firebase/auth.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
1-
import { type Auth, getAuth } from "@firebase/auth";
1+
import type { Auth } from "@firebase/auth";
22
import { getFirebaseApp } from "./app";
33

4-
export function getFirebaseAuth(): Auth {
5-
return getAuth(getFirebaseApp());
4+
let authInstance: Auth | null = null;
5+
6+
/**
7+
* Lazily initializes and returns the Firebase Auth instance.
8+
* The @firebase/auth module is dynamically imported so it is only
9+
* downloaded when authentication is actually needed.
10+
*/
11+
export async function getFirebaseAuth(): Promise<Auth> {
12+
if (authInstance) return authInstance;
13+
14+
const [{ getAuth }, app] = await Promise.all([import("@firebase/auth"), getFirebaseApp()]);
15+
authInstance = getAuth(app);
16+
return authInstance;
617
}

src/stores/auth.ts

Lines changed: 50 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,88 @@
1-
import {
2-
type AuthProvider,
3-
GithubAuthProvider,
4-
GoogleAuthProvider,
5-
onAuthStateChanged,
6-
signInWithPopup,
7-
signOut,
8-
type User,
9-
} from "@firebase/auth";
1+
import type { User } from "@firebase/auth";
102
import { atom, onMount } from "nanostores";
113

124
import { getFirebaseAuth } from "~/lib/firebase/auth";
135
import { singletonGetter } from "~/lib/singletonGetter";
146

157
export type { User } from "@firebase/auth";
168

9+
/**
10+
* Lazily loads @firebase/auth providers and methods.
11+
* This avoids pulling the full Firebase Auth SDK into the initial bundle --
12+
* it is only fetched when the user actually interacts with auth.
13+
*/
14+
async function loadFirebaseAuth() {
15+
return import("@firebase/auth");
16+
}
17+
1718
export class AuthStore {
1819
$user = atom<User | null>(null);
1920
$isLoading = atom<boolean>(false);
2021
$error = atom<string | null>(null);
2122

2223
constructor() {
2324
onMount(this.$user, () => {
24-
try {
25-
return onAuthStateChanged(getFirebaseAuth(), (currentUser) => {
26-
this.$isLoading.set(false);
27-
this.$user.set(currentUser);
28-
});
29-
} catch {
30-
this.$error.set("Could not instantiate a connection with firebase");
31-
}
32-
25+
void this.initAuthListener();
3326
return;
3427
});
3528
}
3629

30+
private async initAuthListener(): Promise<void> {
31+
try {
32+
const [{ onAuthStateChanged }, auth] = await Promise.all([
33+
loadFirebaseAuth(),
34+
getFirebaseAuth(),
35+
]);
36+
onAuthStateChanged(auth, (currentUser) => {
37+
this.$isLoading.set(false);
38+
this.$user.set(currentUser);
39+
});
40+
} catch {
41+
this.$error.set("Could not instantiate a connection with firebase");
42+
}
43+
}
44+
3745
// Auth methods
3846
loginByGithub = (): void => {
39-
this.loginByProvider(new GithubAuthProvider());
47+
void this.loginByProvider("github");
4048
};
4149

4250
loginByGoogle = (): void => {
43-
this.loginByProvider(new GoogleAuthProvider());
51+
void this.loginByProvider("google");
4452
};
4553

4654
logout = (): void => {
4755
this.$error.set(null);
4856
this.$isLoading.set(true);
49-
signOut(getFirebaseAuth())
50-
.then(() => {
57+
void (async () => {
58+
try {
59+
const [{ signOut }, auth] = await Promise.all([loadFirebaseAuth(), getFirebaseAuth()]);
60+
await signOut(auth);
5161
this.$user.set(null);
52-
})
53-
.catch((e: unknown) => {
62+
} catch (e: unknown) {
5463
this.$error.set(String(e));
55-
})
56-
.finally(() => {
64+
} finally {
5765
this.$isLoading.set(false);
58-
});
66+
}
67+
})();
5968
};
6069

61-
private loginByProvider(provider: AuthProvider): void {
70+
private async loginByProvider(provider: "github" | "google"): Promise<void> {
6271
this.$error.set(null);
6372
this.$isLoading.set(true);
64-
signInWithPopup(getFirebaseAuth(), provider)
65-
.then((creds) => {
66-
this.$user.set(creds.user);
67-
})
68-
.catch((e: unknown) => {
69-
this.$error.set(String(e));
70-
})
71-
.finally(() => {
72-
this.$isLoading.set(false);
73-
});
73+
try {
74+
const [firebaseAuth, auth] = await Promise.all([loadFirebaseAuth(), getFirebaseAuth()]);
75+
const authProvider =
76+
provider === "github"
77+
? new firebaseAuth.GithubAuthProvider()
78+
: new firebaseAuth.GoogleAuthProvider();
79+
const creds = await firebaseAuth.signInWithPopup(auth, authProvider);
80+
this.$user.set(creds.user);
81+
} catch (e: unknown) {
82+
this.$error.set(String(e));
83+
} finally {
84+
this.$isLoading.set(false);
85+
}
7486
}
7587
}
7688

0 commit comments

Comments
 (0)