diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index 137ba27ae2..fba443297d 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -95,6 +95,7 @@ const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => { export default memo(App); +// Running these hooks in a separate component ensures we do not inadvertently rerender the entire app when they change. const HookIsolator = memo( ({ config, studioInitAction }: { config: PartialAppConfig; studioInitAction?: StudioInitAction }) => { const language = useAppSelector(selectLanguage); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx index b41926f20b..3633cb51c1 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx @@ -178,6 +178,11 @@ export const GalleryImage = memo(({ imageDTO }: Props) => { ); }, [imageDTO, element, store, dndId]); + // Perf optimization: + // The gallery image component can be heavy and re-render often. We want to track hovering state without causing + // unnecessary re-renders. To do this, we use a local atom - which has a stable reference - in the image component - + // and then pass the atom to the hover icons component, which subscribes to the atom and re-renders when the atom + // changes. const $isHovered = useMemo(() => atom(false), []); const onMouseOver = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx index bbf60f6a33..334325d096 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx @@ -146,6 +146,7 @@ export const AddNodeCmdk = memo(() => { const [searchTerm, setSearchTerm] = useState(''); const addNode = useAddNode(); const tab = useAppSelector(selectActiveTab); + // Filtering the list is expensive - debounce the search term to avoid stutters const [debouncedSearchTerm] = useDebounce(searchTerm, 300); const isOpen = useStore($addNodeCmdk); const open = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx index d40e403f44..f40abfdd5e 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer.tsx @@ -123,6 +123,9 @@ export const InputFieldRenderer = memo(({ nodeId, fieldName, settings }: Props) const field = useInputFieldInstance(nodeId, fieldName); const template = useInputFieldTemplate(nodeId, fieldName); + // When deciding which component to render, first we check the type of the template, which is more efficient than the + // instance type check. The instance type check uses zod and is slower. + if (isStringFieldCollectionInputTemplate(template)) { if (!isStringFieldCollectionInputInstance(field)) { return null; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx index ae710cc802..180b2f7a7c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/common/NodeWrapper.tsx @@ -19,6 +19,9 @@ type NodeWrapperProps = PropsWithChildren & { width?: ChakraProps['w']; }; +// Animations are disabled as a performance optimization - they can cause massive slowdowns in large workflows - even +// when the animations are GPU-accelerated CSS. + const containerSx: SystemStyleObject = { h: 'full', position: 'relative', diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDataTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDataTab.tsx index b640070225..dd058e0f8b 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDataTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDataTab.tsx @@ -12,6 +12,7 @@ const selector = createSelector(selectNodesSlice, (nodes) => selectLastSelectedN const InspectorDataTab = () => { const { t } = useTranslation(); const lastSelectedNodeData = useAppSelector(selector); + // This is debounced to prevent re-rendering the whole component when the user changes the node's values quickly const [debouncedLastSelectedNodeData] = useDebounce(lastSelectedNodeData, 300); if (!debouncedLastSelectedNodeData) { diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/NodeTemplateGate.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/NodeTemplateGate.tsx index 18649c155e..87ea032368 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/NodeTemplateGate.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/NodeTemplateGate.tsx @@ -2,6 +2,9 @@ import { useNodeTemplateSafe } from 'features/nodes/hooks/useNodeTemplate'; import type { PropsWithChildren, ReactNode } from 'react'; import { memo } from 'react'; +// This component is used to gate the rendering of a component based on the existence of a template. It makes it +// easier to handle cases where we are missing a node template in the inspector. + export const TemplateGate = memo( ({ nodeId, fallback, children }: PropsWithChildren<{ nodeId: string; fallback: ReactNode }>) => { const template = useNodeTemplateSafe(nodeId); diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldIsInvalid.ts b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldIsInvalid.ts index 6bc834d6af..368efc465e 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldIsInvalid.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useInputFieldIsInvalid.ts @@ -50,6 +50,9 @@ export const useInputFieldIsInvalid = (nodeId: string, fieldName: string) => { // Else special handling for individual field types + // Check the template type first - it's the most efficient. If that passes, check the instance type, which uses + // zod and therefore is slower. + if (isImageFieldCollectionInputTemplate(template) && isImageFieldCollectionInputInstance(field)) { if (validateImageFieldCollectionValue(field.value, template).length > 0) { return true; diff --git a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts index 03c66ba8d8..aaeb10edfd 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/validateConnection.ts @@ -35,6 +35,10 @@ const getTargetEqualityPredicate = return e.target === c.target && e.targetHandle === c.targetHandle; }; +/** + * Validates a connection between two fields + * @returns A translation key for an error if the connection is invalid, otherwise null + */ export const validateConnection: ValidateConnectionFunc = ( c, nodes, diff --git a/invokeai/frontend/web/src/features/queue/store/readiness.ts b/invokeai/frontend/web/src/features/queue/store/readiness.ts index 0223e68936..d6b57c241d 100644 --- a/invokeai/frontend/web/src/features/queue/store/readiness.ts +++ b/invokeai/frontend/web/src/features/queue/store/readiness.ts @@ -67,6 +67,9 @@ import { assert } from 'tsafe'; * * For example, the canvas tab needs to check the status of the canvas manager before enqueuing, while the workflows * tab needs to check the status of the nodes and their connections. + * + * A global store that contains the reasons why the app is not ready to enqueue generations. State changes are debounced + * to reduce the number of times we run the fairly involved readiness checks. */ const LAYER_TYPE_TO_TKEY = {