perf(canvas): 画布视口 - 交互期避免强制布局和整树样式重算

拖拽和缩放时用世界坐标定位浮层,暂停更新继承缩放变量,概览优先用缩略图并限制解码缓存。
This commit is contained in:
ddcat666
2026-10-10 20:19:54 +08:00
parent dd66be15cb
commit 1f5ecca33a
6 changed files with 173 additions and 49 deletions

View File

@@ -53,6 +53,7 @@ export function ImageNodeContent(props: CanvasNodeContentProps) {
if (!hasImage) return <EmptyImageContent {...props} />;
return (
<ImageContent
renderLOD={props.renderLOD}
batchPreviewNodes={props.batchPreviewNodes}
node={props.node}
theme={props.theme}
@@ -463,6 +464,7 @@ export function EmptyMediaContent({ icon, label, color }: { icon: ReactNode; lab
}
export function ImageContent({
renderLOD = "full",
node,
theme,
isBatchRoot,
@@ -472,10 +474,11 @@ export function ImageContent({
batchOpening,
batchRecovering,
onToggleBatch,
}: Pick<CanvasNodeContentProps, "node" | "theme" | "isBatchRoot" | "batchCount" | "batchPreviewNodes" | "batchExpanded" | "batchOpening" | "batchRecovering" | "onToggleBatch">) {
}: Pick<CanvasNodeContentProps, "renderLOD" | "node" | "theme" | "isBatchRoot" | "batchCount" | "batchPreviewNodes" | "batchExpanded" | "batchOpening" | "batchRecovering" | "onToggleBatch">) {
const imageContainerRef = useRef<HTMLDivElement>(null);
const nearViewport = useNearViewport(imageContainerRef);
const { url, loading, originalWidth, originalHeight } = useNodeResourceUrl(node, nearViewport, node.metadata?.imageLayer || node.metadata?.imageLayerGroup ? "original" : "thumbnail");
const nearViewport = useNearViewport(imageContainerRef, renderLOD !== "shell");
const resourceVariant = renderLOD === "full" && (node.metadata?.imageLayer || node.metadata?.imageLayerGroup) ? "original" : "thumbnail";
const { url, loading, originalWidth, originalHeight } = useNodeResourceUrl(node, nearViewport, resourceVariant);
const importedFromLibTV = node.metadata?.importSource?.provider === "libtv";
const { updateMediaNode } = useCanvasNodeActions();
const measuredSizeRef = useRef<{ width: number; height: number } | null>(null);
@@ -518,7 +521,15 @@ export function ImageContent({
};
return (
<BatchFrame batchPreviewNodes={batchPreviewNodes} batchCount={isBatchRoot ? batchCount : 0} batchExpanded={batchExpanded} batchOpening={batchOpening} batchRecovering={batchRecovering} theme={theme} onToggleBatch={onToggleBatch}>
<BatchFrame
batchPreviewNodes={renderLOD === "full" ? batchPreviewNodes : undefined}
batchCount={isBatchRoot && renderLOD === "full" ? batchCount : 0}
batchExpanded={batchExpanded}
batchOpening={batchOpening}
batchRecovering={batchRecovering}
theme={theme}
onToggleBatch={onToggleBatch}
>
<div ref={imageContainerRef} className="relative h-full w-full overflow-hidden rounded-[var(--node-radius)]">
<RetainedCanvasImage
identity={`${getActiveUserScope()}:${node.id}:${node.metadata?.storageKey || node.metadata?.content || "empty"}`}
@@ -691,9 +702,10 @@ export function useNodeResourceUrl(node: CanvasNodeData, eager: boolean, variant
return { url: current.url, loading: current.loading, originalWidth: isThumbnail ? current.originalWidth : undefined, originalHeight: isThumbnail ? current.originalHeight : undefined };
}
export function useNearViewport(ref: RefObject<Element | null>) {
export function useNearViewport(ref: RefObject<Element | null>, enabled = true) {
const [nearViewport, setNearViewport] = useState(false);
useEffect(() => {
if (!enabled || nearViewport) return;
const element = ref.current;
if (!element || typeof IntersectionObserver === "undefined") {
setNearViewport(true);
@@ -710,7 +722,7 @@ export function useNearViewport(ref: RefObject<Element | null>) {
);
observer.observe(element);
return () => observer.disconnect();
}, [ref]);
}, [enabled, nearViewport, ref]);
return nearViewport;
}