perf: improve home and canvas initial load

This commit is contained in:
134 2026-07-07 22:16:07 +08:00 committed by root
parent 6b25171ce1
commit 134411f7f8
5 changed files with 68 additions and 9 deletions

View File

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

View File

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

View File

@ -62,6 +62,8 @@ export function LovartFeaturedGallery({
<img
src={item.thumbnailUrl || item.originalUrl}
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]"
/>
<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 type { ChangeEvent } from "react";
import { HomeAnimationEmbed } from "@/components/home/home-animation-embed";
import { Button } from "@/components/ui/button";
import {
Select,
@ -12,6 +12,19 @@ import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
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 = {
composerPlaceholder: string;
displayLogo: string;

View File

@ -76,6 +76,8 @@ export function LovartProjectsSection({
<img
src={item.thumbnail}
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]"
/>
) : (