perf: lazy load canvas 3d renderers
This commit is contained in:
parent
b50d3eb295
commit
78f14edbfc
@ -1,18 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import React, { useRef, useState, useCallback, useMemo } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { Rnd } from "react-rnd";
|
||||
import { motion } from "framer-motion";
|
||||
import { useCanvasStore, canvasSelectors } from "@/store/canvas";
|
||||
import type { AgentSlotNodeData, CanvasEdge, CanvasNode, Position } from "@/types/canvas";
|
||||
import { ImageNode } from "./nodes/ImageNode";
|
||||
import { AgentSlotNode } from "./nodes/AgentSlotNode";
|
||||
import { Model3DNode } from "./nodes/Model3DNode";
|
||||
import { TextNode } from "./nodes/TextNode";
|
||||
import { UiScreenNode } from "./nodes/UiScreenNode";
|
||||
import { VideoNode } from "./nodes/VideoNode";
|
||||
import { WorkflowNode } from "./nodes/WorkflowNode";
|
||||
import { World3DNode } from "./nodes/World3DNode";
|
||||
import { CanvasToolbarIcon } from "./CanvasToolbarIcon";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@ -68,6 +67,50 @@ import {
|
||||
} from "@/lib/canvas/agent-pending-requests";
|
||||
import { inferOriginalImageUrl } from "@/lib/image-url";
|
||||
|
||||
type Model3DNodeModule = typeof import("./nodes/Model3DNode");
|
||||
type World3DNodeModule = typeof import("./nodes/World3DNode");
|
||||
|
||||
let model3DNodeModulePromise: Promise<Model3DNodeModule> | null = null;
|
||||
let world3DNodeModulePromise: Promise<World3DNodeModule> | null = null;
|
||||
|
||||
function loadModel3DNodeModule() {
|
||||
model3DNodeModulePromise ??= import("./nodes/Model3DNode");
|
||||
return model3DNodeModulePromise;
|
||||
}
|
||||
|
||||
function loadWorld3DNodeModule() {
|
||||
world3DNodeModulePromise ??= import("./nodes/World3DNode");
|
||||
return world3DNodeModulePromise;
|
||||
}
|
||||
|
||||
function preloadCanvas3DNodeRenderers() {
|
||||
void loadModel3DNodeModule();
|
||||
void loadWorld3DNodeModule();
|
||||
}
|
||||
|
||||
function Canvas3DNodeLoading() {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center rounded-[12px] border border-dashed border-[#d8dee8] bg-[#f6f7f9] text-[12px] text-[#7a8494] dark:border-white/12 dark:bg-white/5 dark:text-white/45">
|
||||
正在加载 3D 预览
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Model3DNode = dynamic(
|
||||
() => loadModel3DNodeModule().then((module) => module.Model3DNode),
|
||||
{
|
||||
loading: () => <Canvas3DNodeLoading />,
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
const World3DNode = dynamic(
|
||||
() => loadWorld3DNodeModule().then((module) => module.World3DNode),
|
||||
{
|
||||
loading: () => <Canvas3DNodeLoading />,
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
const CANVAS_NO_DRAG_SELECTOR =
|
||||
'[data-canvas-no-drag="true"], [data-canvas-no-drag="true"] *';
|
||||
const CANVAS_DRAG_CANCEL_SELECTOR = [
|
||||
@ -352,6 +395,10 @@ export function CanvasWorkspace({
|
||||
const tool = useCanvasStore(canvasSelectors.tool);
|
||||
const lastAddedNodeId = useCanvasStore((state) => state.lastAddedNodeId);
|
||||
const setViewport = useCanvasStore((state) => state.setViewport);
|
||||
const has3DNodes = useMemo(
|
||||
() => nodes.some((node) => node.type === "model3d" || node.type === "world3d"),
|
||||
[nodes]
|
||||
);
|
||||
const viewportRef = useRef(viewport);
|
||||
const pendingViewportRef = useRef<typeof viewport | null>(null);
|
||||
const pendingViewportRafRef = useRef<number | null>(null);
|
||||
@ -370,6 +417,49 @@ export function CanvasWorkspace({
|
||||
viewportRef.current = viewport;
|
||||
}, [viewport]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (has3DNodes) {
|
||||
preloadCanvas3DNodeRenderers();
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof window === "undefined") return;
|
||||
const connection = (
|
||||
navigator as Navigator & {
|
||||
connection?: { effectiveType?: string; saveData?: boolean };
|
||||
}
|
||||
).connection;
|
||||
if (connection?.saveData || /(^|-)2g$/i.test(connection?.effectiveType || "")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const idleWindow = window as typeof window & {
|
||||
cancelIdleCallback?: (handle: number) => void;
|
||||
requestIdleCallback?: (
|
||||
callback: IdleRequestCallback,
|
||||
options?: IdleRequestOptions
|
||||
) => number;
|
||||
};
|
||||
let idleId: number | null = null;
|
||||
const warmupTimer = window.setTimeout(() => {
|
||||
if (document.visibilityState !== "visible") return;
|
||||
if (typeof idleWindow.requestIdleCallback === "function") {
|
||||
idleId = idleWindow.requestIdleCallback(preloadCanvas3DNodeRenderers, {
|
||||
timeout: 15000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
preloadCanvas3DNodeRenderers();
|
||||
}, 12000);
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(warmupTimer);
|
||||
if (idleId !== null) {
|
||||
idleWindow.cancelIdleCallback?.(idleId);
|
||||
}
|
||||
};
|
||||
}, [has3DNodes]);
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
if (pendingViewportRafRef.current !== null) {
|
||||
|
||||
@ -85,4 +85,25 @@ test.describe("home to canvas entry performance", () => {
|
||||
'canvasChatPanelModulePromise ??= import("./CanvasChatPanel")'
|
||||
);
|
||||
});
|
||||
|
||||
test("keeps heavy 3d renderers out of the normal canvas workspace chunk", () => {
|
||||
const workspaceSource = readSource("components/canvas/CanvasWorkspace.tsx");
|
||||
|
||||
expect(workspaceSource).not.toContain(
|
||||
'import { Model3DNode } from "./nodes/Model3DNode"'
|
||||
);
|
||||
expect(workspaceSource).not.toContain(
|
||||
'import { World3DNode } from "./nodes/World3DNode"'
|
||||
);
|
||||
expect(workspaceSource).toContain(
|
||||
'model3DNodeModulePromise ??= import("./nodes/Model3DNode")'
|
||||
);
|
||||
expect(workspaceSource).toContain(
|
||||
'world3DNodeModulePromise ??= import("./nodes/World3DNode")'
|
||||
);
|
||||
expect(workspaceSource).toContain("window.setTimeout(() => {");
|
||||
expect(workspaceSource).toContain('document.visibilityState !== "visible"');
|
||||
expect(workspaceSource).toContain("timeout: 15000");
|
||||
expect(workspaceSource).toContain("}, 12000)");
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user