From 97643fbbfa5bbcaaf5e14f82e1880dd0359f9429 Mon Sep 17 00:00:00 2001 From: Josh Wooding <12938082+joshwooding@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:18:30 +0100 Subject: [PATCH] Further improve CB performance (#4251) --- .../core/src/list-control/ListControlState.ts | 44 +++++++++++---- .../stories/combo-box/combo-box.stories.tsx | 53 +++++++++++++++++-- 2 files changed, 81 insertions(+), 16 deletions(-) diff --git a/packages/core/src/list-control/ListControlState.ts b/packages/core/src/list-control/ListControlState.ts index 4d045b5863..4464aa1f0f 100644 --- a/packages/core/src/list-control/ListControlState.ts +++ b/packages/core/src/list-control/ListControlState.ts @@ -58,28 +58,51 @@ function findElementPosition( element: HTMLElement, ) { if (elements.length === 0) { - return 0; + return [0, false] as const; } if ( element.compareDocumentPosition(elements[elements.length - 1].element) & Node.DOCUMENT_POSITION_PRECEDING ) { - return -1; + return [-1, false] as const; } if ( element.compareDocumentPosition(elements[0].element) & Node.DOCUMENT_POSITION_FOLLOWING ) { - return 0; + return [0, false] as const; } - return elements.findIndex( - (option) => - option.element.compareDocumentPosition(element) & - Node.DOCUMENT_POSITION_PRECEDING, - ); + let left = 0; + let right = elements.length; + let leftLast = 0; + let rightLast = right; + + let exists = false; + + while (left < right) { + const inPos = Math.floor((right + left) / 2); + const compared = element.compareDocumentPosition(elements[inPos].element); + if (compared & Node.DOCUMENT_POSITION_PRECEDING) { + left = inPos; + } else if (compared & Node.DOCUMENT_POSITION_FOLLOWING) { + right = inPos; + } else { + right = inPos; + left = inPos; + exists = true; + } + // nothing has changed, must have found limits. insert between. + if (leftLast === left && rightLast === right) { + break; + } + leftLast = left; + rightLast = right; + } + + return [right, exists] as const; } export function defaultValueToString(item: Item): string { @@ -193,10 +216,9 @@ export function useListControl(props: ListControlProps) { const register = useCallback( (optionValue: OptionValue, element: HTMLElement) => { const { id } = optionValue; - const option = optionsRef.current.find((item) => item.data.id === id); - const index = findElementPosition(optionsRef.current, element); + const [index, exists] = findElementPosition(optionsRef.current, element); - if (!option) { + if (!exists) { if (index === -1) { optionsRef.current.push({ data: optionValue, element }); } else { diff --git a/packages/core/stories/combo-box/combo-box.stories.tsx b/packages/core/stories/combo-box/combo-box.stories.tsx index 63521e5657..d0467c84b7 100644 --- a/packages/core/stories/combo-box/combo-box.stories.tsx +++ b/packages/core/stories/combo-box/combo-box.stories.tsx @@ -912,12 +912,55 @@ export const Bordered = () => { ); }; -export const PerformanceTest = () => { +const hugeArray = Array.from({ length: 10000 }).map( + (_, index) => `Option ${index}`, +); + +export const PerformanceTest: StoryFn = (args) => { + const [value, setValue] = useState(getTemplateDefaultValue(args)); + + const handleChange = (event: ChangeEvent) => { + // React 16 backwards compatibility + event.persist(); + + const value = event.target.value; + setValue(value); + }; + + const handleSelectionChange = ( + event: SyntheticEvent, + newSelected: string[], + ) => { + // React 16 backwards compatibility + event.persist(); + + args.onSelectionChange?.(event, newSelected); + + if (args.multiselect) { + setValue(""); + return; + } + + if (newSelected.length === 1) { + setValue(newSelected[0]); + } else { + setValue(""); + } + }; + + const filteredItems = hugeArray.filter((item) => + item.toLowerCase().includes(value.trim().toLowerCase()), + ); + return ( - - {Array.from({ length: 10000 }).map((_, index) => ( -