huntabyte / cmdk-sv

cmdk, but for Svelte ✨
https://cmdk-sv.com
MIT License
410 stars 17 forks source link

Large `Command.List` cannot be filtered w/o crashing #45

Closed lukeed closed 3 months ago

lukeed commented 7 months ago

I have a looped Command/popover w/ 3200 items in it. It takes a while to render, but the main issue is that as soon as I attempt to type into the Command.Input the tab freezes with 100% CPU

This example was ported over from React and works fine after the initial slow render.

<Popover.Root bind:open>
    <Popover.Trigger {disabled} asChild let:builder>
        <Button
            builders={[builder]}
            variant="outline" role="combobox"
        >
            {display || 'Select item'}
            <ChevronsUpDown class="ml-2 h-4 w-4 shrink-0 opacity-50" />
        </Button>
    </Popover.Trigger>

    <input {name} {value} hidden type="hidden" />

    <Popover.Content class="p-0 w-[400px]">
        <Command.Root loop>
            <Command.Input placeholder="Search items..." />
            <Command.Empty>No item found.</Command.Empty>

            <Command.List class="h-[var(--cmdk-list-height)] max-h-96">
                {#each $Names as txt (txt)}
                    <Command.Item value={txt} onSelect={() => onItem(txt)}>
                        <Check
                            class={cn(
                                'mr-2 h-4 w-4',
                                txt === value ? 'opacity-100' : 'opacity-0'
                            )}
                        /> {name}
                    </Command.Item>
                {/each}
            </Command.List>
        </Command.Root>
    </Popover.Content>
</Popover.Root>
lukeed commented 7 months ago

My naiive guess is that there's too much store logic happening (or not granular enough) which is why the initial render is also taking so long.

Rendering the same 3200 items into a plain <ul> completes almost instantly, which is also true even if I match the same DOM structure (attributes and all).

The initial render with the Command.Root list takes at least 8s.

huntabyte commented 7 months ago

Hey @lukeed, thanks for raising this issue!

I will try to find some time to investigate further. I'm hoping it is something small I've overlooked that is causing this behavior.

csjh commented 5 months ago

My own little investigation made it seem like the major factor is a sort being run excessively; each Command.Item creation calls context.value(id, value):

image

which in turn triggers a sort during a state.update:

image

which is repeated n times, resulting in approx n * nlogn = n^2 * logn operations run

Not sure the best way to handle - I think the ideal solution would be sorting after all the Command.Items are created (once the slot is done mounting?), but as long as the sorting isn't run every time, any solution would probably be fine.

Also, more concentrated repro for testing purposes:

<script>
    import { Command } from "cmdk-sv";

    const ten_first_names = ["John", "Doe", "Jane", "Smith", "Michael", "Brown", "William", "Johnson", "David", "Williams"];
    const ten_middle_names = ["James", "Lee", "Robert", "Michael", "David", "Joseph", "Thomas", "Charles", "Christopher", "Daniel"];
    const ten_last_names = ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez"];

    const names = ten_first_names.map((first, i) => {
        return ten_middle_names.map((middle, j) => {
            return ten_last_names.map((last, k) => {
                return `${first} ${middle} ${last}`;
            });
        });
    }).flat(2);
</script>

<Command.Root loop>
    <Command.Input placeholder="Search items..." />
    <Command.Empty>No item found.</Command.Empty>

    <Command.List class="h-[var(--cmdk-list-height)] max-h-96">
        {#each names as txt (txt)}
            <Command.Item value={txt}>{txt}</Command.Item>
        {/each}
    </Command.List>
</Command.Root>
huntabyte commented 5 months ago

Hey @csjh, thanks a ton for the detailed comment and reproduction.

I'll try to find some time to dig into this further and come to some sort of solution for this issue!