docs(ui): add comments for recent perf optimizations

This commit is contained in:
psychedelicious
2025-02-17 08:16:59 +10:00
parent 65eabde297
commit 62e5b9da18
10 changed files with 27 additions and 0 deletions

View File

@@ -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);

View File

@@ -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(() => {

View File

@@ -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(() => {

View File

@@ -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;

View File

@@ -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',

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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;

View File

@@ -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,

View File

@@ -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 = {