perf: defer heavy modules and add performance baselines

Lazy-load Stripe and the chat reply animation, remove the unused Lottie runtime, and add repeatable bundle and Web Vitals baseline tooling.
This commit is contained in:
2026-07-14 10:55:08 +08:00
parent 0fe74b5371
commit b4904e738b
12 changed files with 846 additions and 31 deletions
+18 -3
View File
@@ -12,7 +12,14 @@
* - 新消息到达时自动滚到底部
* - 用户向上滚动时不强制拉回(保持阅读上下文)
*/
import { useEffect, useLayoutEffect, useMemo, useRef } from "react";
import {
lazy,
Suspense,
useEffect,
useLayoutEffect,
useMemo,
useRef,
} from "react";
import type { UiMessage } from "@/data/dto/chat";
@@ -23,11 +30,15 @@ import {
} from "../chat-render-items";
import { AiDisclosureBanner } from "./ai-disclosure-banner";
import { DateHeader } from "./date-header";
import { LottieMessageBubble } from "./lottie-message-bubble";
import { MessageBubble } from "./message-bubble";
import styles from "./chat-area.module.css";
const CHAT_AUTO_SCROLL_BOTTOM_THRESHOLD = 96;
const ReplyingAnimation = lazy(() =>
import("./lottie-message-bubble").then((module) => ({
default: module.LottieMessageBubble,
})),
);
export interface ChatAreaProps {
messages: readonly UiMessage[];
@@ -104,7 +115,11 @@ export function ChatArea({
onOpenImage,
)}
{isReplyingAI && <LottieMessageBubble />}
{isReplyingAI ? (
<Suspense fallback={null}>
<ReplyingAnimation />
</Suspense>
) : null}
</main>
);
}
-1
View File
@@ -17,7 +17,6 @@ export * from "./fullscreen-image-viewer";
export * from "./history-unlock-dialog";
export * from "./image-bubble";
export * from "./insufficient-credits-dialog";
export * from "./lottie-message-bubble";
export * from "./message-avatar";
export * from "./message-bubble";
export * from "./message-content";
-1
View File
@@ -8,4 +8,3 @@ export * from "./subscription-cta-button";
export * from "./subscription-payment-method";
export * from "./subscription-payment-success-dialog";
export * from "./subscription-vip-offer-section";
export * from "./stripe-payment-dialog";
@@ -0,0 +1,41 @@
"use client";
import dynamic from "next/dynamic";
import type { StripePaymentDialogProps } from "./stripe-payment-dialog";
import { stripePaymentDialogStyles as styles } from "./stripe-payment-dialog.styles";
const StripePaymentDialog = dynamic(
() =>
import("./stripe-payment-dialog").then(
(module) => module.StripePaymentDialog,
),
{
ssr: false,
loading: StripePaymentDialogLoading,
},
);
export function LazyStripePaymentDialog(props: StripePaymentDialogProps) {
return <StripePaymentDialog {...props} />;
}
function StripePaymentDialogLoading() {
return (
<div
className={styles.overlay}
role="dialog"
aria-modal="true"
aria-labelledby="stripe-payment-loading-title"
>
<div className={styles.dialog} aria-busy="true">
<div className={styles.header}>
<h2 id="stripe-payment-loading-title" className={styles.title}>
Preparing secure payment
</h2>
<p className={styles.content}>Loading payment methods...</p>
</div>
</div>
</div>
);
}
@@ -11,7 +11,7 @@ import {
} from "@/stores/payment/payment-context";
import { Logger } from "@/utils";
import { StripePaymentDialog } from "./stripe-payment-dialog";
import { LazyStripePaymentDialog } from "./lazy-stripe-payment-dialog";
import { stripePaymentDialogStyles as dialogStyles } from "./stripe-payment-dialog.styles";
import { SubscriptionCtaButton } from "./subscription-cta-button";
@@ -95,7 +95,7 @@ export function SubscriptionCheckoutButton({
/>
) : null}
{paymentLaunch.shouldShowStripeDialog && paymentLaunch.stripeClientSecret ? (
<StripePaymentDialog
<LazyStripePaymentDialog
clientSecret={paymentLaunch.stripeClientSecret}
onClose={paymentLaunch.handleStripeClose}
onConfirmed={paymentLaunch.handleStripeConfirmed}
+2 -2
View File
@@ -8,7 +8,7 @@ import {
} from "@/stores/payment/payment-context";
import { Logger } from "@/utils";
import { StripePaymentDialog } from "../subscription/components/stripe-payment-dialog";
import { LazyStripePaymentDialog } from "../subscription/components/lazy-stripe-payment-dialog";
import { stripePaymentDialogStyles as dialogStyles } from "../subscription/components/stripe-payment-dialog.styles";
import styles from "./tip-screen.module.css";
@@ -74,7 +74,7 @@ export function TipCheckoutButton({
/>
) : null}
{paymentLaunch.shouldShowStripeDialog && paymentLaunch.stripeClientSecret ? (
<StripePaymentDialog
<LazyStripePaymentDialog
clientSecret={paymentLaunch.stripeClientSecret}
returnPath={returnPath}
onClose={paymentLaunch.handleStripeClose}