timc1 / kbar

fast, portable, and extensible cmd+k interface for your site
https://kbar.vercel.app
MIT License
4.87k stars 184 forks source link

Kbar very slow under certain condition #252

Closed raRaRa closed 2 years ago

raRaRa commented 2 years ago

When you have many actions (maybe 50-100ish), and some of them contain keywords such as "bla is a member in {teamName}", then kbar becomes almost unresponsive when typing "bla is a". This happens immediately after typing "a".

I have a feeling this is related to command score that is used under the hood. Would it be possible to somehow disable command score in kbar?

Thanks.

raRaRa commented 2 years ago

Here's a snippet that reproduces this issue:

for (let i = 0; i < 100; i++) {
      updatedPeopleActions.push(
        createAction({
          name: `John${i} is a member in Team Test`,
          icon: 'layers-three-01',
          keywords: [
            `John${i} is a member in ${i}`,
            `Teams where John${i} is a member`,
          ]
            .filter((word) => !!word)
            .join(', '),
          perform: () => console.log('Hello'),
        })
      );
    }

In my actual code John${i} is an actual person name and it's not repeating like that. But this code is enough to show how unresponsive kbar becomes with these actions.

Just type something like 'John is a ..." and it becomes very sluggish after typing "a".

timc1 commented 2 years ago

Hey @raRaRa 👋

How are you building updatedPeopleActions? ie; is the updatedPeopleActions list getting rebuilt every time you type something into the search bar?

timc1 commented 2 years ago

Here's an example with 1000 dynamically added actions, with 10 keywords each:

https://user-images.githubusercontent.com/12195101/200686759-f36f3117-914e-4bec-b47a-4e7f6b02d204.mov

raRaRa commented 2 years ago

Can you try it with a similar query as I am using, where you use something like "Where bla is ...". Everything is fast for me as well until I use words such as "is".

timc1 commented 2 years ago

Ah, I am able to reproduce it; it is due to command-score.

So command-score starts to degrade pretty heavily when the search term crosses ~ 15 characters and the data set is still quite large.

In your case, since a query of "John is a team member in team test etc.." will still match every single action, command-score takes ~ 15ms for each action to resolve. That's not the best!

I think you can, for now, approach this with 2 temporary solutions:

function useInternalMatches(filtered: ActionImpl[], search: string) {
  const value = React.useMemo(() => {
    // 1. trim down long words
    const split = search.split(/\s/).map((word) => word.substring(0, 8));
    // 2. just match against the last few
    const refinedSearch = split.slice(split.length - 3).join(" ");
    return {
      filtered,
      search: refinedSearch,
    };
  }, [filtered, search]);

  const { filtered: throttledFiltered, search: throttledSearch } = useThrottledValue(value);
  ...
}

Again, these are temp solutions and we'll probably need to do a little further optimization within the lib.

raRaRa commented 2 years ago

Ah cool thanks, is it possible to bypass command-score completely, or replace it with my own function?

timc1 commented 2 years ago

You can always just fork a copy of useMatches and add your own custom matching logic! Pass the results to KBarResults and you should be good to go.

timc1 commented 2 years ago

Following up here: https://github.com/timc1/kbar/issues/255