perf: improve home and canvas initial load

This commit is contained in:
134 2026-07-07 22:16:07 +08:00
parent 67b526408a
commit 7c1d985c09
5 changed files with 68 additions and 9 deletions

View File

@ -1976,7 +1976,7 @@ export default function CanvasEditorLegacyPage({
); );
const [activeWorkflowExecutionTaskKey, setActiveWorkflowExecutionTaskKey] = const [activeWorkflowExecutionTaskKey, setActiveWorkflowExecutionTaskKey] =
useState<string | null>(null); useState<string | null>(null);
const [isPanelOpen, setIsPanelOpen] = useState(() => !isVideoMode); const [isPanelOpen, setIsPanelOpen] = useState(() => requestedAgentMode);
const [floatingPanelWidth, setFloatingPanelWidth] = const [floatingPanelWidth, setFloatingPanelWidth] =
useState(FLOATING_PANEL_WIDTH); useState(FLOATING_PANEL_WIDTH);
const [ const [
@ -3280,11 +3280,15 @@ export default function CanvasEditorLegacyPage({
}, [checkCanvasAuthSession]); }, [checkCanvasAuthSession]);
useEffect(() => { useEffect(() => {
if (!isPanelOpen && !isVideoCanvasFileListOpen) {
return;
}
const timeoutId = window.setTimeout(() => { const timeoutId = window.setTimeout(() => {
setAreSecondaryPanelsReady(true); setAreSecondaryPanelsReady(true);
}, 800); }, 120);
return () => window.clearTimeout(timeoutId); return () => window.clearTimeout(timeoutId);
}, []); }, [isPanelOpen, isVideoCanvasFileListOpen]);
const hydrateCanvasProject = useCallback( const hydrateCanvasProject = useCallback(
(project: CanvasInitialProject) => { (project: CanvasInitialProject) => {
@ -10844,7 +10848,7 @@ export default function CanvasEditorLegacyPage({
</div> </div>
) : null} ) : null}
<div className="flex h-full w-full flex-col overflow-hidden border-l bg-white dark:bg-zinc-950"> <div className="flex h-full w-full flex-col overflow-hidden border-l bg-white dark:bg-zinc-950">
{!areSecondaryPanelsReady ? ( {!isPanelOpen ? null : !areSecondaryPanelsReady ? (
<CanvasLazyPanelLoader label="正在准备侧边面板..." /> <CanvasLazyPanelLoader label="正在准备侧边面板..." />
) : panelMode === "chat" ? ( ) : panelMode === "chat" ? (
// AI 对话内容 - 使用 key 强制在 id 变化时重新挂载 // AI 对话内容 - 使用 key 强制在 id 变化时重新挂载

View File

@ -2,7 +2,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { signIn, signOut } from "next-auth/react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import useSWR from "swr"; import useSWR from "swr";
import useSWRInfinite from "swr/infinite"; import useSWRInfinite from "swr/infinite";
@ -60,7 +59,6 @@ import {
getCanvasLaunchStorageKey, getCanvasLaunchStorageKey,
} from "@/lib/canvas-launch"; } from "@/lib/canvas-launch";
import type { MembershipConfig } from "@/lib/membership/config"; import type { MembershipConfig } from "@/lib/membership/config";
import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
import { import {
normalizeLovartHomeSidebarConfig, normalizeLovartHomeSidebarConfig,
type LovartHomeSidebarItemKey, type LovartHomeSidebarItemKey,
@ -465,6 +463,7 @@ export function LovartHome({
const [previewCursorVisible, setPreviewCursorVisible] = useState(true); const [previewCursorVisible, setPreviewCursorVisible] = useState(true);
const [isSecondaryDataReady, setIsSecondaryDataReady] = useState(false); const [isSecondaryDataReady, setIsSecondaryDataReady] = useState(false);
const [isFeaturedDataReady, setIsFeaturedDataReady] = useState(false); const [isFeaturedDataReady, setIsFeaturedDataReady] = useState(false);
const [isHomeAnimationReady, setIsHomeAnimationReady] = useState(false);
const projectSectionRef = useRef<HTMLDivElement | null>(null); const projectSectionRef = useRef<HTMLDivElement | null>(null);
const featuredSectionRef = useRef<HTMLElement | null>(null); const featuredSectionRef = useRef<HTMLElement | null>(null);
const featuredLoadMoreRef = useRef<HTMLDivElement | null>(null); const featuredLoadMoreRef = useRef<HTMLDivElement | null>(null);
@ -535,6 +534,31 @@ export function LovartHome({
}; };
}, [isFeaturedDataReady]); }, [isFeaturedDataReady]);
useEffect(() => {
if (typeof window === "undefined" || !isSecondaryDataReady) {
return;
}
const loadHomeAnimation = () => setIsHomeAnimationReady(true);
const idleWindow = window as Window & {
cancelIdleCallback?: (handle: number) => void;
requestIdleCallback?: (
callback: IdleRequestCallback,
options?: IdleRequestOptions
) => number;
};
if (typeof idleWindow.requestIdleCallback === "function") {
const idleId = idleWindow.requestIdleCallback(loadHomeAnimation, {
timeout: 3200,
});
return () => idleWindow.cancelIdleCallback?.(idleId);
}
const timer = window.setTimeout(loadHomeAnimation, 1800);
return () => window.clearTimeout(timer);
}, [isSecondaryDataReady]);
const { data: canvasData, mutate: mutateCanvas } = useSWR<{ const { data: canvasData, mutate: mutateCanvas } = useSWR<{
success: boolean; success: boolean;
data: CanvasProject[]; data: CanvasProject[];
@ -671,7 +695,10 @@ export function LovartHome({
enabled?: boolean; enabled?: boolean;
htmlContent?: string; htmlContent?: string;
fileName?: string; fileName?: string;
}>(isSecondaryDataReady ? "/api/home-animation" : null, fetcher); }>(
isSecondaryDataReady && isHomeAnimationReady ? "/api/home-animation" : null,
fetcher
);
const membershipConfig = membershipData?.config; const membershipConfig = membershipData?.config;
const membershipPlans = membershipConfig?.plans || []; const membershipPlans = membershipConfig?.plans || [];
@ -896,6 +923,7 @@ export function LovartHome({
} catch (_error) {} } catch (_error) {}
try { try {
const { signOut } = await import("next-auth/react");
await signOut({ await signOut({
callbackUrl: "/", callbackUrl: "/",
redirect: false, redirect: false,
@ -1109,6 +1137,9 @@ export function LovartHome({
}; };
const uploadSelectedFiles = async (): Promise<CanvasLaunchAttachment[]> => { const uploadSelectedFiles = async (): Promise<CanvasLaunchAttachment[]> => {
const { fetchUploadWithRetry } = await import(
"@/lib/client/upload-with-retry"
);
const result: CanvasLaunchAttachment[] = []; const result: CanvasLaunchAttachment[] = [];
for (const item of files) { for (const item of files) {
@ -1146,6 +1177,7 @@ export function LovartHome({
setIsLoggingIn(true); setIsLoggingIn(true);
try { try {
const { signIn } = await import("next-auth/react");
const result = await signIn("credentials", { const result = await signIn("credentials", {
email, email,
password: loginPassword, password: loginPassword,
@ -1171,6 +1203,7 @@ export function LovartHome({
toast.error("Google 登录未启用"); toast.error("Google 登录未启用");
return; return;
} }
const { signIn } = await import("next-auth/react");
await signIn("google", { callbackUrl: "/" }); await signIn("google", { callbackUrl: "/" });
}; };
@ -1226,6 +1259,9 @@ export function LovartHome({
const blob = await response.blob(); const blob = await response.blob();
const formData = new FormData(); const formData = new FormData();
formData.append("file", blob, `edited-${Date.now()}.png`); formData.append("file", blob, `edited-${Date.now()}.png`);
const { fetchUploadWithRetry } = await import(
"@/lib/client/upload-with-retry"
);
const uploadResponse = await fetchUploadWithRetry(formData); const uploadResponse = await fetchUploadWithRetry(formData);
if (!uploadResponse.ok) { if (!uploadResponse.ok) {
@ -1802,7 +1838,9 @@ export function LovartHome({
heroPromoText={heroPromoText} heroPromoText={heroPromoText}
heroSubtitleText={heroSubtitleText} heroSubtitleText={heroSubtitleText}
heroTitleText={heroTitleText} heroTitleText={heroTitleText}
homeAnimationData={homeAnimationData} homeAnimationData={
isHomeAnimationReady ? homeAnimationData : undefined
}
isAgentModeActive={isComposerAgentModeActive} isAgentModeActive={isComposerAgentModeActive}
isMembershipEnabled={isMembershipEnabled} isMembershipEnabled={isMembershipEnabled}
isSubmitting={isSubmitting} isSubmitting={isSubmitting}

View File

@ -62,6 +62,8 @@ export function LovartFeaturedGallery({
<img <img
src={item.thumbnailUrl || item.originalUrl} src={item.thumbnailUrl || item.originalUrl}
alt={item.prompt || "灵感图"} alt={item.prompt || "灵感图"}
loading="lazy"
decoding="async"
className="block h-auto w-full rounded-[6px] bg-[#e8e8e8] object-cover transition-transform duration-300 ease-out will-change-transform group-hover:scale-[1.01] dark:bg-[#1b1c20]" className="block h-auto w-full rounded-[6px] bg-[#e8e8e8] object-cover transition-transform duration-300 ease-out will-change-transform group-hover:scale-[1.01] dark:bg-[#1b1c20]"
/> />
<div className="mt-[8px] flex h-[22px] items-center justify-between text-[11px] font-normal leading-none text-[#b8b8b8] dark:text-white/45"> <div className="mt-[8px] flex h-[22px] items-center justify-between text-[11px] font-normal leading-none text-[#b8b8b8] dark:text-white/45">

View File

@ -1,6 +1,6 @@
import dynamic from "next/dynamic";
import { ArrowUp, Box, Lightbulb, Loader2, Plus, Sparkles } from "lucide-react"; import { ArrowUp, Box, Lightbulb, Loader2, Plus, Sparkles } from "lucide-react";
import type { ChangeEvent } from "react"; import type { ChangeEvent } from "react";
import { HomeAnimationEmbed } from "@/components/home/home-animation-embed";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Select, Select,
@ -12,6 +12,19 @@ import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { ModelOption, UploadFileItem } from "./types"; import type { ModelOption, UploadFileItem } from "./types";
const HomeAnimationEmbed = dynamic(
() =>
import("@/components/home/home-animation-embed").then(
(module) => module.HomeAnimationEmbed
),
{
loading: () => (
<div className="h-[360px] w-full rounded-[24px] border border-black/10 bg-black/[0.03] md:h-[420px] dark:border-white/10 dark:bg-white/[0.04]" />
),
ssr: false,
}
);
type LovartHeroComposerProps = { type LovartHeroComposerProps = {
composerPlaceholder: string; composerPlaceholder: string;
displayLogo: string; displayLogo: string;

View File

@ -76,6 +76,8 @@ export function LovartProjectsSection({
<img <img
src={item.thumbnail} src={item.thumbnail}
alt={item.title} alt={item.title}
loading="lazy"
decoding="async"
className="h-full w-full object-cover transition-transform duration-300 ease-out will-change-transform group-hover:scale-[1.025]" className="h-full w-full object-cover transition-transform duration-300 ease-out will-change-transform group-hover:scale-[1.025]"
/> />
) : ( ) : (