oruga-ui / oruga

🐛 Oruga is a lightweight library of UI components without any CSS framework dependency
https://oruga-ui.com
MIT License
1.13k stars 172 forks source link

Performance issue with autocomplete when the data array is very large. #1057

Open seedds opened 2 months ago

seedds commented 2 months ago

Overview of the problem

Oruga version: [0.9.0-pre.1] Vuejs version: [3.5.4] OS/Browser: windows/chrome

Description

Performance of Autocomplete is very bad when the data is very large. I have an array which contains around 1000 strings. It feels very laggy when using the Autocomplete component.

Steps to reproduce

The base example on the page: https://oruga-ui.com/components/Autocomplete.html. First fill the data object with random string data. When the data object is large enough, the performance drops significantly.

Expected behavior

the filter function should perform very smoothly and feel snappy.

mlmoravek commented 2 months ago

Hey, I don't know if this is a real oruga issue or a vue issue.

The options are filtered by a change of the input value:

props.options.filter((option) =>getValue(option).toLowerCase().includes(vmodel.value.toLowerCase())),

You can provide your own filter function with the prop filter, if you have a more complex filter logic.


However, feel free to make a PR with an improved implemenation.

I would suggest just filtering your options by some pre-selection by the user beforehand.

seedds commented 2 months ago

It may be an oruga issue. Since I use Select component in Quasar at last, it is very snappy with the same data.

I made a code snippet to demonstrate the problem at https://codesandbox.io/p/sandbox/oruga-test-ktvqlg The array is 1000 length. when you select an item, then try to delete it. It will take seconds to complete. Or you enter one character, then tried to delete it, it will also take seconds to complete.

mlmoravek commented 2 months ago

Hmm ok, I did some investigating and agree, we could do some performance improvements here.

seedds commented 2 months ago

Great. Thank you very much.

Another problem I encounter when I am doing last project is that the memory usage of Table component was keeping growing when the data source of the table was updated probably once per 2 seconds.

I was using Websocket to get new data for a table about once per 2 seconds. During that process, the heap size kept growing larger and larger until the whole page crashed. I tried many ways to isolate the problem. In the end, I suspect that something in the Table component might be not properly destroyed when new data came and DOM refreshed.

I ended up using Table component from Quasar to solve the problem. The heap size used by Quasar component is about 40-50MiB, and it was very stable. Compared with Table component in Oruga, the heap size grew from 100 to 3000MiB, then crashed inevitably.

mlmoravek commented 2 months ago

Thanks for reporting this! I will check this. Could you make a reproducible example of the table problem?

seedds commented 2 months ago

I make an example at https://codesandbox.io/p/sandbox/oruga-table-test-vzcjk3?file=%2Fsrc%2FApp.vue I have to open https://114.132.182.150:59888/get_quote at my browser first to proceed with unsafe cert, since it is an adhoc ssl flask server hosted at my own computer. Otherwise, the axios can't make http request in codesandboxie.

The memory usage grows steadily. The memory usage goes over 1000MiB or 2000MiB with 3 or 4 tables like this. After switching to Quasar, I noticed that the memory usage is very low, which rarely goes over 50MiB.

I made a similar table in Quasar at https://codesandbox.io/p/sandbox/quasar-table-test-4rh6dz?file=%2Fsrc%2FApp.vue%3A31%2C59 You can see that the memory is very stable. And in production the memory usage is below 50MiB, the 300ish memory usage is probably due to the overhead of codesanboxie

dauriata commented 1 month ago

On this issue I found out that it is the Child / Parent provide mechanism that is slow during insertion or removal of item (and hence filtering) On a dropdown, if we insert 1000 items it takes many seconds but is almost instant if I comment this line in useParentProvider.ts :

function registerItem(
...
        // childItems.value.push(item as UnwrapNestedRefs<typeof item>);
...
}

because of this issue dropdown, autocomplete and taginput are almost unusable with dataset above 500 items

dauriata commented 1 month ago

Digging a little further it seems that it is that querySelector in that same function that is very slow on a large collection : const children = parent.querySelectorAll(ids); and it is rerun for every item and not just once

mlmoravek commented 1 month ago

By the main 0.9 release, I plan to have the options prop of various components redesigned #1008. I'm already working on it. Here I will reimplement how filtered options are handled. Instead of removing the item completely by filtering it out of the options, I will set a hidden state property and remove it from the view with v-show. This way, the piece of code you identified as causing the performance issues will only be called once, rather than every time the filtered options change.

We need to test this, but I could see how it might give better performance.

mlmoravek commented 1 week ago

@dauriata please try again with version 0.9.x

I updated your example

https://codesandbox.io/p/sandbox/oruga-test-ktvqlg

and everthing works well with the Autocomplete component :)

Example:

<template>
  <o-field label="Find a JS framework">
    <o-autocomplete
      v-model="name"
      clearable
      open-on-focus
      :options="data"
      @select="(option) => (selected = option)"
    />
  </o-field>
  <p><b>Selected:</b> {{ name }} {{ selected }}</p>
</template>

<script setup>
import { ref } from "vue";

const data = [...];

const name = ref("");
const selected = ref();
</script>

Feel free to open again if any futher problems appear.

dauriata commented 6 days ago

it is fast on first load, because the slow code we talked about above is not run while the dom is not mounted, but on options change it is still slow. Here is an example with a switch the update options, with 500 items it is lagging, above 1000 it hangs the browser. https://codesandbox.io/p/sandbox/oruga-test-forked-scq5t4

mlmoravek commented 6 days ago

Yeah true, hmm i will see what i can do. At first you can try to add :key="switchData", i think this will help for now.

dauriata commented 6 days ago

I took a look at this issue and I am sharing what I found : The code that slows down tries to make an ordered list of the childItems by matching them with the order in the DOM Problems :

I looked at how it was done in Buefy and the childItems list was matched with the slot list of the parent : https://github.com/buefy/buefy/blob/dev/src/utils/ProviderParentMixin.js

mlmoravek commented 6 days ago

Thanks for investigating!

One hint:

In vue3 we can use useSlots().default() to get the list of the slots

No, we can't, because it is not recommended to call any slot render function (like useSlots().default()) outside of the components render function. Vue logs a warning for this in dev mode.

dauriata commented 6 days ago

I think you can use useSlots() if you use wrap it in a watch or computed. At least I didn't have the warning when I tested it.

mlmoravek commented 6 days ago

I think you can use useSlots() if you use wrap it in a watch or computed. At least I didn't have the warning when I tested it.

Sure you can use useSlots() to check if a slot is used but you can't useSlots().default() to react on inner slot template.