Skip to content

Commit

Permalink
feat(ui): improve IAIDndImage performance
Browse files Browse the repository at this point in the history
`dnd-kit` has a problem where, when drag events start and stop, every item that uses the library rerenders. This occurs due to its use of context.

The dnd library needs to listen for pointer events to handle dragging. Because our images are both clickable (selectable) and draggable, every time you click an image, the dnd necessarily sees this event, its context updates and all other dnd-enabled components rerender.

With a lot of images in gallery and/or batch manager, this leads to some jank.

There is an open PR to address this: clauderic/dnd-kit#1096

But unfortunately, the maintainer hasn't accepted any changes for a few months, and its not clear if this will be merged any time soon :/

This change simply extracts the draggable and droppable logic out of IAIDndImage into their own minimal components. Now only these need to rerender when the dnd context is changed. The rerenders are far less impactful now.

Hopefully the linked PR is accepted and we get even more efficient dnd functionality in the future.
  • Loading branch information
psychedelicious committed Jul 9, 2023
1 parent dc8a222 commit f337666
Showing 1 changed file with 86 additions and 42 deletions.
128 changes: 86 additions & 42 deletions invokeai/frontend/web/src/common/components/IAIDndImage.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,34 @@
import {
Box,
ChakraProps,
Flex,
Icon,
Image,
useColorMode,
useColorModeValue,
} from '@chakra-ui/react';
import { useCombinedRefs } from '@dnd-kit/utilities';
import {
TypesafeDraggableData,
TypesafeDroppableData,
isValidDrop,
useDraggable,
useDroppable,
} from 'app/components/ImageDnd/typesafeDnd';
import IAIIconButton from 'common/components/IAIIconButton';
import {
IAILoadingImageFallback,
IAINoContentFallback,
} from 'common/components/IAIImageFallback';
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import { AnimatePresence } from 'framer-motion';
import { MouseEvent, ReactElement, SyntheticEvent } from 'react';
import { memo, useRef } from 'react';
import { MouseEvent, ReactElement, SyntheticEvent, memo, useRef } from 'react';
import { FaImage, FaUndo, FaUpload } from 'react-icons/fa';
import { PostUploadAction } from 'services/api/thunks/image';
import { ImageDTO } from 'services/api/types';
import { mode } from 'theme/util/mode';
import { v4 as uuidv4 } from 'uuid';
import IAIDropOverlay from './IAIDropOverlay';
import { PostUploadAction } from 'services/api/thunks/image';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import { mode } from 'theme/util/mode';
import {
TypesafeDraggableData,
TypesafeDroppableData,
isValidDrop,
useDraggable,
useDroppable,
} from 'app/components/ImageDnd/typesafeDnd';

type IAIDndImageProps = {
imageDTO: ImageDTO | undefined;
Expand Down Expand Up @@ -83,28 +82,6 @@ const IAIDndImage = (props: IAIDndImageProps) => {

const { colorMode } = useColorMode();

const dndId = useRef(uuidv4());

const {
attributes,
listeners,
setNodeRef: setDraggableRef,
isDragging,
active,
} = useDraggable({
id: dndId.current,
disabled: isDragDisabled || !imageDTO,
data: draggableData,
});

const { isOver, setNodeRef: setDroppableRef } = useDroppable({
id: dndId.current,
disabled: isDropDisabled,
data: droppableData,
});

const setDndRef = useCombinedRefs(setDroppableRef, setDraggableRef);

const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({
postUploadAction,
isDisabled: isUploadDisabled,
Expand Down Expand Up @@ -139,9 +116,6 @@ const IAIDndImage = (props: IAIDndImageProps) => {
userSelect: 'none',
cursor: isDragDisabled || !imageDTO ? 'default' : 'pointer',
}}
{...attributes}
{...listeners}
ref={setDndRef}
>
{imageDTO && (
<Flex
Expand All @@ -154,7 +128,6 @@ const IAIDndImage = (props: IAIDndImageProps) => {
}}
>
<Image
onClick={onClick}
src={thumbnail ? imageDTO.thumbnail_url : imageDTO.image_url}
fallbackStrategy="beforeLoadOrError"
fallback={<IAILoadingImageFallback image={imageDTO} />}
Expand Down Expand Up @@ -225,13 +198,84 @@ const IAIDndImage = (props: IAIDndImageProps) => {
</>
)}
{!imageDTO && isUploadDisabled && noContentFallback}
<Droppable
data={droppableData}
disabled={isDropDisabled}
dropLabel={dropLabel}
/>
<Draggable
data={draggableData}
disabled={isDragDisabled || !imageDTO}
onClick={onClick}
/>
</Flex>
);
};

export default memo(IAIDndImage);

type DroppableProps = {
dropLabel?: string;
disabled?: boolean;
data?: TypesafeDroppableData;
};

const Droppable = memo((props: DroppableProps) => {
const { dropLabel, data, disabled } = props;
const dndId = useRef(uuidv4());

const { isOver, setNodeRef, active } = useDroppable({
id: dndId.current,
disabled,
data,
});

return (
<Box
ref={setNodeRef}
position="absolute"
w="full"
h="full"
pointerEvents="none"
>
<AnimatePresence>
{isValidDrop(droppableData, active) && !isDragging && (
{isValidDrop(data, active) && (
<IAIDropOverlay isOver={isOver} label={dropLabel} />
)}
</AnimatePresence>
</Flex>
</Box>
);
});

Droppable.displayName = 'Droppable';

type DraggableProps = {
disabled?: boolean;
data?: TypesafeDraggableData;
onClick?: (event: MouseEvent<HTMLDivElement>) => void;
};

export default memo(IAIDndImage);
const Draggable = memo((props: DraggableProps) => {
const { data, disabled, onClick } = props;
const dndId = useRef(uuidv4());

const { attributes, listeners, setNodeRef } = useDraggable({
id: dndId.current,
disabled,
data,
});

return (
<Box
onClick={onClick}
ref={setNodeRef}
position="absolute"
w="full"
h="full"
{...attributes}
{...listeners}
/>
);
});

Draggable.displayName = 'Draggable';

0 comments on commit f337666

Please sign in to comment.