vuejs / vue

This is the repo for Vue 2. For Vue 3, go to https://github.com/vuejs/core
http://v2.vuejs.org
MIT License
207.98k stars 33.68k forks source link

v-for performance issue #2000

Closed carbon-projects closed 8 years ago

carbon-projects commented 8 years ago

hi, I have a table with a 300 row *60 column, using V-for's rendering is very slow, how to improve efficiency?

amrlafi commented 5 years ago

Did you consider using functional components? also one-way binding (using :value instead of v-model) usually helps as it skips generating watchers for the model, would definitely help if you model is complex.

sh0ber commented 5 years ago

One optimization not mentioned here is Object.freeze() for data that doesn't need reactivity, such as that used in a static report. Even when displaying table rows with functional components, a large reactive data set carries its own reactivity overhead which can be removed by freezing data prior to using in the UI, or even immediately after load. And if your data is being stored in Vuex, it's reactive, because "a Vuex store's state is made reactive by Vue". All that's needed is to freeze the data before loading it into Vuex.

Without this optimization, each data value is tracked for changes by the reactivity system, which makes rendering slower. It's even possible to make changes by "unfreezing" data (by cloning the set), making a change to the clone, re-freezing, and replacing the original.

original0594 commented 5 years ago

For anyone who is having troubles with infinite scrolling with a bootstrap-vue table, an easy way to implement this is with Pagination and a onScroll listener, once a user scrolls to the bottom of the page simply increase the Pagination rows "perPage”. Not true virtual scrolling but works well for just listing out data. I also used this pagination method for shortening initial page load time. Default perPage is set to 50 when the page first renders, then in mounted() i set a 1s timeout(gives the page a chance to settle) and then increase the per page to 100

nkostadinov commented 5 years ago

I notice enormous penalty when destroying(set to []) a list of around 500 - 600 items in v-for cycle (each item has 5-6 child components). Can this become faster ? May be some framework optimization, if the lenght of the array is 0 then making innerHtml='' is instant, while destroying those items takes 5-6 seconds. I've migrated our product which is very large RSS reader(nearly 1.5mln users) to Vue, but the older version using jquery and plain js is MUCH faster. I'm depressed now :(

Akryum commented 5 years ago

@nkostadinov Vue still needs to properly destroy all the child/nested components. You should try a virtual scrolling solution as mentioned in the thread.

PierBover commented 5 years ago

@nkostadinov have you tried not using reactive data for that list?

nkostadinov commented 5 years ago

@Akryum I will try to use the list but I have a card view which will be difficult to implement with virtual scroller because the articles are ordered in lines up to 4 cards (card is item from v-for)

@PierBover the data for that list have to be reactive :)

julyamba commented 4 years ago

hello, I need help, chrome browser crashes when my app still processing the data. i have csv file which contains approximately 5000 rows and 20cells per row. then in vue js i have components that process the csv. the process includes multi mapping on each cell lots of mapping on array/object. the data reaches thousands of array and has thousands of nested array. when processing i used async so that it will just process and display if done processing. i put a spinner if ever the process isnt done, with 2000 rows data the app works, but when the data reaches 5000 rows the spinner show for about 25seconds and then the browser crashes. Please help. thank you and God Bless Sir.

KarmaBlackshaw commented 4 years ago

Hi! I am having the same problem of rendering a list of 200 rows. However, I think the problems is in the dynamic class binding. I use a method to dynamically add class based on the given parameter and every time I click the list, it recalls the function 200 times.

killo9ramm commented 3 years ago

Hi! Facing the same problem I have datalist in input with 650 option items. If I use v-for in html directly:

<datalist id="bxCityList">
             <option v-for="item in itemsList" :value="item.uniqName"></option>
</datalist> 

to fill values list I have 3 to 5 seconds lag to find an item while typing in input

But following solution became much more faster no lag at all: js file

Vue.component('cities-list-item', {
    props: ['city'],
    template: 
`     
  <option v-bind:data-value='city.code' v-bind:value="city.uniqName" />
`
  })

html

<datalist id="bxCityList">
    <cities-list-item v-for="item in itemsList" v-bind:city="item" v-bind:key="item.code">
    </cities-list-item>
</datalist>

I hope this helps

AllanOricil commented 3 years ago

I have a table with 18 rows and 66 columns. Some columns have inputs that can be either a text, select or checkbox type. The input value is bound to a data variable like :value=rowsData[rowIndex][columName] and when I type anything in the input the "render" function is called, and it takes around 110ms to finish for every character typed, even when it is not actually changing anything in the DOM. How I know that? I added a breakpoint in the table to look for DOM tree modifications, and it is never triggered. How can I improve this performance? I can't reduce the number of rows because this is already the minimum to fit the window view port height. And I also don't think 1118 table cells are hard for Vue to handle. But I can't identify what I'm doing wrong to have this poor performance. Could somebody help me?

I'm also not using v-model on the inputs!

Maybe I should make each table row its own component? Would it improve performance? I don't really understand why vue calls Render method for every single row even when it changes nothing in the DOM.

Help me! D:

AllanOricil commented 3 years ago

Finally I made it work. I just had to add a debounce function on the input handler. Now I can type anything, and wait the 110ms vue takes to rerender everything.

https://www.npmjs.com/package/debounce

AllanOricil commented 3 years ago

I also noticed that css is kind making the input to lag. When I remove all the css that I'm using to style inputs inside table cells, the input works really fine and I don't even need to use debounce. This css is not applied conditionally on the template and the DOM is also not being modified when typing. So I don't really know why the css is slowing down inputs. Does anyone here has any thoughts about it?

samuelkilada commented 1 year ago

I came up with my own form of "virtualization", since I couldn't find a good library for it. I am able to render 150,000 items with quite decent performance (at this point, the only thing that starts to slightly slow down is scrolling, but it's still quite good and once you stop scrolling, performance is buttery smooth).

This works by rendering in a for loop only the items that are currently in view. The rest of the items are "faked" with a table row before and after the viewable items, with the height of those table rows calculated to the height of the offscreen items. This makes it so that you only have to v-for through the visible items and not the whole array. Here's basically the code to do it. I'll be continuing to test and refine this but it's pretty stable already:

<script setup lang="ts">
const items = ref([] as Array<any>)
for (let i = 0; i < 50000; i++) {
  items.value.push({
    id: i,
    name: `Item ${i}`,
    date: '11/10/2023'
  })
}
const scrollY = ref(0)
// Put in the pixel height of one of your table rows. Inspect the DOM in the browser to find this out
const rowHeight = 50
// If you have some kind of header or something at the top that gets scrolled with the table items, put the height of that below, as a negative number
const tableExtraContent = -50
// The height of the scrollable part of the table that you want to display (based on max-height in styles)
const viewableHeight = 800
// Calculate what part of the array we're currently viewing
const viewingStartIndex = computed(() => {
  let startIndex = Math.floor((scrollY.value + tableExtraContent) / rowHeight)
  if (startIndex < 0) {
    startIndex = 0
  }
  return startIndex
})
const viewingItems = computed(() => {
  let endIndex = Math.ceil((scrollY.value + tableExtraContent + viewableHeight) / rowHeight)
  if (endIndex > items.value.length) {
    endIndex = items.value.length
  }
  return items.value.slice(viewingStartIndex.value, endIndex)
})

// Calculate how big the empty space above the table should be
const aboveItems = computed(() => {
  return items.value.slice(0, viewingStartIndex.value)
})
const aboveItemsStyle = computed(() => {
  const height = aboveItems.value.length * rowHeight
  return {
    height: `${height}px`
  }
})

// Calculate how big the empty space below the table should be
const belowItems = computed(() => {
  return items.value.slice(viewingStartIndex.value + viewingItems.value.length)
})

const belowItemsStyle = computed(() => {
  const height = belowItems.value.length * rowHeight
  return {
    height: `${height}px`
  }
})

// Table scroll event
const tableScrolled = (e: any) => {
  scrollY.value = e.target.scrollTop
}
</script>

<template>
    <div class="scrollable" @scroll="tableScrolled">
      <table>
        <thead>
          <tr>
            <td>ID</td>
            <td>Name</td>
            <td>Date</td>
          </tr>
        </thead>
        <tbody>
          <tr :style="aboveItemsStyle">
          </tr>
          <tr v-for="(item, rowIndex) in viewingItems" :key="item.id">
            <td class="id">
              {{ viewingStartIndex + rowIndex + 1 }}
            </td>
            <td>
              {{ item.name }}
            </td>
            <td>
              {{ item.date }}
            </td>
          </tr>
          <tr :style="belowItemsStyle">
          </tr>
        </tbody>
      </table>
    </div>
</template>
<style lang="scss" scoped>
  .scrollable {
    max-height: 800px;
    overflow-y: auto;

    table {
      tr {
        height: 50px;

        td {
          width: 20%;
        }
      }
    }
  }
</style>
AllanOricil commented 1 year ago

I came up with my own form of "virtualization", since I couldn't find a good library for it. I am able to render 150,000 items with quite decent performance (at this point, the only thing that starts to slightly slow down is scrolling, but it's still quite good and once you stop scrolling, performance is buttery smooth).

This works by rendering in a for loop only the items that are currently in view. The rest of the items are "faked" with a table row before and after the viewable items, with the height of those table rows calculated to the height of the offscreen items. This makes it so that you only have to v-for through the visible items and not the whole array. Here's basically the code to do it. I'll be continuing to test and refine this but it's pretty stable already:

<script setup lang="ts">
const items = ref([] as Array<any>)
for (let i = 0; i < 50000; i++) {
  items.value.push({
    id: i,
    name: `Item ${i}`,
    date: '11/10/2023'
  })
}
const scrollY = ref(0)
// Put in the pixel height of one of your table rows. Inspect the DOM in the browser to find this out
const rowHeight = 50
// If you have some kind of header or something at the top that gets scrolled with the table items, put the height of that below, as a negative number
const tableExtraContent = -50
// The height of the scrollable part of the table that you want to display (based on max-height in styles)
const viewableHeight = 800
// Calculate what part of the array we're currently viewing
const viewingStartIndex = computed(() => {
  let startIndex = Math.floor((scrollY.value + tableExtraContent) / rowHeight)
  if (startIndex < 0) {
    startIndex = 0
  }
  return startIndex
})
const viewingItems = computed(() => {
  let endIndex = Math.ceil((scrollY.value + tableExtraContent + viewableHeight) / rowHeight)
  if (endIndex > items.value.length) {
    endIndex = items.value.length
  }
  return items.value.slice(viewingStartIndex.value, endIndex)
})

// Calculate how big the empty space above the table should be
const aboveItems = computed(() => {
  return items.value.slice(0, viewingStartIndex.value)
})
const aboveItemsStyle = computed(() => {
  const height = aboveItems.value.length * rowHeight
  return {
    height: `${height}px`
  }
})

// Calculate how big the empty space below the table should be
const belowItems = computed(() => {
  return items.value.slice(viewingStartIndex.value + viewingItems.value.length)
})

const belowItemsStyle = computed(() => {
  const height = belowItems.value.length * rowHeight
  return {
    height: `${height}px`
  }
})

// Table scroll event
const tableScrolled = (e: any) => {
  scrollY.value = e.target.scrollTop
}
</script>

<template>
    <div class="scrollable" @scroll="tableScrolled">
      <table>
        <thead>
          <tr>
            <td>ID</td>
            <td>Name</td>
            <td>Date</td>
          </tr>
        </thead>
        <tbody>
          <tr :style="aboveItemsStyle">
          </tr>
          <tr v-for="(item, rowIndex) in viewingItems" :key="item.id">
            <td class="id">
              {{ viewingStartIndex + rowIndex + 1 }}
            </td>
            <td>
              {{ item.name }}
            </td>
            <td>
              {{ item.date }}
            </td>
          </tr>
          <tr :style="belowItemsStyle">
          </tr>
        </tbody>
      </table>
    </div>
</template>
<style lang="scss" scoped>
  .scrollable {
    max-height: 800px;
    overflow-y: auto;

    table {
      tr {
        height: 50px;

        td {
          width: 20%;
        }
      }
    }
  }
</style>

If you virtualize columns as well it would be amazin

samuelkilada commented 1 year ago

I think you shouldn't need to virtualize the columns, unless you're doing something super strange with your table!