From 684dc1e25f7b00223a7e48edd28f7a7d2315a70a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 23 Jan 2026 16:08:33 +0100 Subject: [PATCH] app-visalizer: simplify progressive rendering Signed-off-by: Patrik Oldsberg --- .../AppVisualizerPage/DetailedVisualizer.tsx | 218 +++++------------- 1 file changed, 63 insertions(+), 155 deletions(-) diff --git a/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx b/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx index 8881904819..259ee143c9 100644 --- a/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx +++ b/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx @@ -31,7 +31,7 @@ import { RiCloseCircleLine as DisabledIcon, } from '@remixicon/react'; import { Focusable } from 'react-aria-components'; -import { memo, useMemo, useState, useEffect, useRef } from 'react'; +import { memo, useMemo, useState, useEffect, useRef, Fragment } from 'react'; function getContrastColor(bgColor: string): string { const hex = bgColor.replace('#', ''); @@ -106,73 +106,48 @@ function getFullPath(node?: AppNode): string { return getFullPath(parent) + part; } -function collectAllNodes(node: AppNode): AppNode[] { - const nodes: AppNode[] = [node]; - for (const children of node.edges.attachments.values()) { - for (const child of children) { - nodes.push(...collectAllNodes(child)); - } - } - return nodes; -} - -function useProgressiveRender(rootNode: AppNode) { - const [renderedNodes, setRenderedNodes] = useState>(new Set()); - const [isComplete, setIsComplete] = useState(false); - const processingRef = useRef(false); +function ProgressiveCollection({ + items, + batchSize = 10, +}: { + items: Array<() => React.ReactElement>; + batchSize?: number; +}) { + const [maxIndex, setMaxIndex] = useState(0); const timeoutRef = useRef | null>(null); useEffect(() => { - if (processingRef.current) { + if (maxIndex >= items.length) { return undefined; } - processingRef.current = true; - const allNodes = collectAllNodes(rootNode); - - const batchSize = 10; - let currentIndex = 0; - const rendered = new Set(); - const processBatch = () => { - const endIndex = Math.min(currentIndex + batchSize, allNodes.length); - for (let i = currentIndex; i < endIndex; i++) { - rendered.add(allNodes[i].spec.id); - } - currentIndex = endIndex; + const nextIndex = Math.min(maxIndex + batchSize, items.length); + setMaxIndex(nextIndex); - setRenderedNodes(new Set(rendered)); - - if (currentIndex < allNodes.length) { + if (nextIndex < items.length) { timeoutRef.current = setTimeout(processBatch, 0); - } else { - setIsComplete(true); - processingRef.current = false; - timeoutRef.current = null; } }; - rendered.add(rootNode.spec.id); - setRenderedNodes(new Set(rendered)); - currentIndex = 1; - - if (allNodes.length > 1) { - timeoutRef.current = setTimeout(processBatch, 0); - } else { - setIsComplete(true); - processingRef.current = false; - } + timeoutRef.current = setTimeout(processBatch, 0); + // eslint-disable-next-line consistent-return return () => { - processingRef.current = false; if (timeoutRef.current !== null) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } }; - }, [rootNode]); + }, [maxIndex, items.length, batchSize]); - return { renderedNodes, isComplete }; + return ( + <> + {items.slice(0, maxIndex).map((item, index) => ( + {item()} + ))} + + ); } const Output = memo( @@ -245,66 +220,7 @@ const Output = memo( }, ); -function AttachmentsComponent(props: { - node: AppNode; - enabled: boolean; - depth: number; - renderedNodes: Set; -}) { - const { node, depth, renderedNodes } = props; - const { attachments } = node.edges; - - const sortedAttachments = useMemo(() => { - return [...attachments.entries()].sort(([a], [b]) => a.localeCompare(b)); - }, [attachments]); - - if (attachments.size === 0) { - return null; - } - - return ( - - {sortedAttachments.map(([key, children], idx) => { - return ( - - - -
{key}
-
- - {children.map(childNode => ( - - ))} - -
- ); - })} -
- ); -} - -function ExtensionComponent(props: { - node: AppNode; - depth: number; - renderedNodes: Set; -}) { - const { node, depth, renderedNodes } = props; - +function Extension({ node, depth }: { node: AppNode; depth: number }) { const enabled = Boolean(node.instance); const tooltipText = useMemo(() => { @@ -328,11 +244,11 @@ function ExtensionComponent(props: { return dataRefs.sort((a, b) => a.id.localeCompare(b.id)); }, [node.instance]); - const shouldRender = renderedNodes.has(node.spec.id); - - if (!shouldRender) { - return null; - } + const sortedAttachments = useMemo(() => { + return [...node.edges.attachments.entries()].sort(([a], [b]) => + a.localeCompare(b), + ); + }, [node.edges.attachments]); return ( } - + {sortedAttachments.length > 0 && ( + + {sortedAttachments.map(([key, children], idx) => ( + + + +
{key}
+
+ + () => ( + + ))} + /> + +
+ ))} +
+ )}
); } -const Extension = memo(ExtensionComponent, (prevProps, nextProps) => { - if ( - prevProps.node.spec.id !== nextProps.node.spec.id || - prevProps.depth !== nextProps.depth - ) { - return false; - } - - const nodeId = prevProps.node.spec.id; - const wasRendered = prevProps.renderedNodes.has(nodeId); - const isRendered = nextProps.renderedNodes.has(nodeId); - - if (wasRendered !== isRendered) { - return false; - } - - if (prevProps.renderedNodes.size !== nextProps.renderedNodes.size) { - return false; - } - - for (const children of prevProps.node.edges.attachments.values()) { - for (const child of children) { - const wasChildRendered = prevProps.renderedNodes.has(child.spec.id); - const isChildRendered = nextProps.renderedNodes.has(child.spec.id); - if (wasChildRendered !== isChildRendered) { - return false; - } - } - } - - return true; -}); - const legendMap = { 'React Element': coreExtensionData.reactElement, 'Utility API': ApiBlueprint.dataRefs.factory, @@ -445,12 +355,10 @@ function Legend() { } export function DetailedVisualizer({ tree }: { tree: AppTree }) { - const { renderedNodes } = useProgressiveRender(tree.root); - return ( - +