huntabyte / cmdk-sv

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

[performance] Virtualise CommandItem when trying to display large lists? #80

Open NathanAdhitya opened 5 months ago

NathanAdhitya commented 5 months ago

As title says.

I'm personally trying to make it working by integrating TanStack Virtual. It's not going very well. Extremely janky, but cut initial mount time (around 600 elements, Combobox element from shadcn-svelte) from 2-3s after clicking the Popover.Trigger to perceivably instant. I haven't gotten filtering to work yet.

Has anyone gotten this issue, if so, what solutions helped?

huntabyte commented 2 months ago

This is going to be improved in the Svelte 5 version for sure 😃

For example, here's a list of 2,000 items running on Svelte 5 version (coming soon):

https://github.com/user-attachments/assets/db3190a8-c6e2-45d0-b060-be7123dd10ae

Something to note about this project that differs from a traditional combobox is that it's actually sorting/ranking the dom elements whereas the combobox simply filters.

gustavomorinaga commented 2 months ago

@NathanAdhitya I had the same issue. For this, I created an component that wraps the virtua lib (because of shadcn-svelte folder convention, but you can call directly if you want).

src/lib/ui/virtual-list/virtual-list.svelte

<script lang="ts" context="module">
  import { VList } from 'virtua/svelte';
  import type { ComponentEvents, ComponentProps } from 'svelte';
</script>

<script lang="ts">
  type T = $$Generic;
  type $$Props = ComponentProps<VList<T>>;
  type $$Events = ComponentEvents<VList<T>>;
  type $$Slots = { default: { item: T; index: number } };

  export let data: $$Props['data'] = [];
</script>

{#key data.length}
  <VList {data} {...$$restProps} let:item let:index>
    <slot {item} {index} />
  </VList>
{/key}

Then, I use it together with the Command component (in my case, I needed to use a derived store for filtered items to display the correct info and debounce the search input):

<script>
  import * as Command from '$lib/components/ui/command';
  import { VirtualList } from '$lib/components/ui/virtual-list';
</script>

<Command.Root shouldFilter={false} class="size-auto rounded-none bg-transparent">
  <Command.Input
    placeholder="Search item..."
    value={$searchTerm}
    on:input={debounceSearch.call}
    class="h-10 py-2"
  />
  <Command.List class="h-[20svh] [&>div]:contents">
    <Command.Empty class="py-4">No results</Command.Empty>
    {#if $filtered.length}
      <Command.Group alwaysRender class="h-full p-0 [&>div]:h-full">
        <VirtualList
          data={$filtered}
          getKey={(item) => item.name}
          let:item={option}
          class="overflow-y-auto p-2"
        >
          <Command.Item value={option.name} class="shrink-0 gap-3 !bg-transparent">
            <Checkbox
              id={option.name}
              aria-labelledby={option.name}
              bind:checked={$options[option.name].checked}
              on:click={debounceFilter.call}
            />
            <Label id={option.name} for={option.name} class="w-full">
              {option.name}
            </Label>
          </Command.Item>
        </VirtualList>
      </Command.Group>
    {/if}
  </Command.List>
</Command.Root>