# Enhancing Vuetify Data Table Performance with Infinite Scroll

## **Introduction**

Vue.js is a popular JavaScript framework that allows developers to build dynamic and interactive user interfaces. When it comes to UI components, Vuetify is a powerful library that provides a wide range of pre-designed, customizable components.

In this article, we will focus on:

* Vuetify's data table component,
    
* How to optimize its performance when dealing with large datasets, and
    
* How to address the issue of slow rendering by implementing an infinite scroll feature.
    

Let's dive in!

## **Prerequisites**

Before diving into the implementation, ensure you have a basic understanding of Vue.js and Vuetify. Familiarity with JavaScript and Vue.js single-file components will be helpful.

If you're new to these technologies, check out the official documentation for [Vue.js](https://vuejs.org/) and [Vuetify](https://vuetifyjs.com/en/) to get started.

### 1\. Setting up the Vuetify Data Table

To begin, let's set up the Vuetify data table component. In the template section, we define the structure of the data table using the `v-data-table` element. The `headers` array specifies the columns' text and properties, while the `items` property is bound to `itemsToDisplay`. This array initially holds all the items, and we will optimize the rendering process in the following steps.

I have disabled pagination using `disable-pagination` to implement infinite scroll and have used `hide-default-footer` to hide the default footer of the data table

* ***Make sure you add the ref prop as shown below, as it will be used to access the table properties.***
    
    ```plaintext
    <!-- Vue template -->
    <template>
      <v-data-table
        ref="dataTable"
        :headers="headers"
        :items="itemsToDisplay.slice(itemStartIndex, itemEndIndex)"
        class="elevation-1"
        disable-pagination
        hide-default-footer
      >
        <!-- ... -->
      </v-data-table>
    </template>
    
    <script>
    export default {
      data() {
        // This is the number of rows that should be shown in at any given moment in the data table
        const itemsPerPage = 40
        return {
          itemStartIndex: 0,
          itemEndIndex: itemsPerPage,
          itemsPerPage:itemsPerPage,
          headers: [
            { text: "Dessert (100g serving)", align: "start", sortable: false, value: "name" },
            { text: "Calories", value: "calories" },
            { text: "Fat (g)", value: "fat" },
            { text: "Carbs (g)", value: "carbs" },
            { text: "Protein (g)", value: "protein" },
            { text: "Iron (%)", value: "iron" },
            { text: "Calcium (%)", value: "calcium" },
            { text: "Magnesium (%)", value: "magnesium" },
            { text: "Potassium (%)", value: "potassium" },
          ],
          desserts: generateRandomItems(), // Generate random items
        };
      },
      computed:{
        // if needed you can implement search or filter etc. logics on this computed property
        itemsToDisplay(){
            return this.desserts
        }
      }
    };
    </script>
    ```
    
    ### 2\. Generating Random Data
    
    To simulate a large dataset, I created a `generateRandomItems()` function that generates 100,000 random items. Each item has properties such as name, calories, fat, carbs, protein, iron, calcium, magnesium, and potassium. This allows us to have a realistic dataset to work with.
    
    * ***This step only creates data for demonstration purposes and can be skipped in real use-case scenarios.***
        

```plaintext
<script>
export default {
  data() {
const itemsPerPage = 40
// Generate random items
function generateRandomItems() {
  const items = [];

  for (let i = 0; i < 100000; i++) {
    const item = {
      name: generateRandomString(),
      calories: getRandomNumber(100, 500),
      fat: getRandomFloat(0, 20),
      carbs: getRandomNumber(0, 50),
      protein: getRandomFloat(0, 10),
      iron: getRandomNumber(0, 5),
      calcium: getRandomNumber(0, 15),
      magnesium: getRandomNumber(0, 15),
      potassium: getRandomNumber(0, 15),
    };
    items.push(item);
  }

  return items;
}

// Helper functions to generate random data
function generateRandomString() {
  const characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
  let result = "";

  for (let i = 0; i < 10; i++) {
    result += characters.charAt(Math.floor(Math.random() * characters.length));
  }

  return result;
}

function getRandomNumber(min, max) {
  return Math.floor(Math.random() * (max - min + 1) + min);
}

function getRandomFloat(min, max) {
  return (Math.random() * (max - min) + min).toFixed(1);
}
 return {
        //...
        }
}

// ...
</script>
```

### 3\. Using `v-intersect` to Load and Display Items

Before we implement infinite scroll, let's understand how the `v-intersect` directive plays a role in loading and displaying items. The `v-intersect` directive is a powerful feature provided by Vuetify that allows us to detect when an element enters or exits the viewport. We can use this directive to trigger actions based on scrolling behavior.

In the code snippet below, we use the `v-intersect` directive to detect when the user is scrolling upward and trigger the `loadLess` function. Similarly, we use the `v-intersect` directive to detect when the user is scrolling downward and trigger the `loadMore` function:

* ﻿`body.append` and `body.prepend`slots are used to add loaders above and below the table body so that they will be displayed when scrolled.
    

```plaintext
<template>
  <v-data-table
    ref="dataTable"
    :headers="headers"
    :items="itemsToDisplay.slice(itemStartIndex, itemEndIndex)"
    class="elevation-1"
    disable-pagination
    hide-default-footer
  >
    <!-- Display loading skeleton when scrolling upwards -->
    <!-- Condition in v-if is to display this ui only when user scrolls beyond displayed items -->
    <template v-if="inventoryStartIndex > 0" v-slot:[`body.prepend`]>
      <tr v-intersect.quiet="loadLess">
        <td :colspan="headers.length" class="text-center">
          <v-skeleton-loader type="table-row" /><v-skeleton-loader type="table-row" />
        </td>
      </tr>
    </template>
    <!-- Display loading skeleton when scrolling downwards -->
    <!-- Condition in v-if is to display this ui only when user scrolls beyond displayed items -->
    <template v-if="inventoriesToDisplay.length > inventoryEndIndex" v-slot:[`body.append`]>
      <tr v-intersect.quiet="loadMore">
        <!-- Add skeleton loader while load more is executed -->
        <td :colspan="headers.length" class="text-center">
          <v-skeleton-loader type="table-row" />
        </td>
      </tr>
    </template>
 </v-data-table>
</template>
```

### 4\. Handling Visibility of `body.append` and `body.prepend`

We utilized the `v-intersect` directive to trigger the `loadMore` and `loadLess` functions on `body.append` and `body.prepend` elements when they intersect the viewport while scrolling down or up, respectively. However, once these elements become visible, they need to be hidden again to ensure that `v-intersect` can trigger the respective functions when they intersect the viewport during subsequent scrolling.

* ***For example, when*** `body.append` becomes visible after executing `loadMore`, we scroll the table to a row above using `scrollToRow`. This action hides the `body.append` element from the viewport. As a result, when the user scrolls down again, `body.append` will intersect the viewport again, triggering the `loadMore` function. The same principle applies to `body.prepend`.
    

```plaintext
<template>
 //...
</template>
<script>
export default {
//...
methods:{
    // scrolls the table to the row at specified index
    scrollToRow(index) {
     const table = this.$refs.dataTable.$el.querySelector("table");
     const row = table.rows[index];
     if (row) {
     row.scrollIntoView(true);
     } 
    }
}
//...
}
</script>
```

### 5\. Implementing Infinite Scroll

To address the performance issues related to rendering a large dataset, we implement infinite scroll. By updating the `itemStartIndex` and `itemEndIndex` variables, we can control which subset of items should be displayed in the data table. This approach significantly improves performance by rendering only the visible items.

```plaintext
<!-- Vue template -->
<template>
  <v-data-table
    ref="dataTable"
    :headers="headers"
    :items="itemsToDisplay.slice(itemStartIndex, itemEndIndex)"
    class="elevation-1"
    disable-pagination
    hide-default-footer
  >
    <!-- Display loading skeleton when scrolling upwards -->
    <template v-if="itemStartIndex > 0" v-slot:[`body.prepend`]>
      <tr v-intersect.quiet="loadLess">
        <td :colspan="headers.length" class="text-center">
          <v-skeleton-loader type="table-row" /><v-skeleton-loader type="table-row" />
        </td>
      </tr>
    </template>

    <!-- Display loading skeleton when scrolling downwards -->
    <template v-if="itemsToDisplay.length > itemEndIndex" v-slot:[`body.append`]>
      <tr v-intersect.quiet="loadMore">
        <td :colspan="headers.length" class="text-center">
          <v-skeleton-loader type="table-row" />
        </td>
      </tr>
    </template>
  </v-data-table>
</template>

<script>
export default {
  // ...
  methods: {
    // scrolls the table to the row at specified index
    scrollToRow(index) {
      const table = this.$refs.dataTable.$el.querySelector("table");
      const row = table.rows[index];
      if (row) {
        row.scrollIntoView(true);
      }
    },
    loadMore(entries, observer, isIntersecting) {
      //only change the indexes when isIntersecting is true i.e the component is visible in viewport
      if (isIntersecting) {
        const indexesLeft = this.itemsToDisplay.length - this.itemEndIndex;
        if (indexesLeft < this.itemsPerPage) {
          this.itemStartIndex = this.itemEndIndex + indexesLeft - this.itemsPerPage;
          this.itemEndIndex = this.itemsToDisplay.length;
        } else {
          this.itemStartIndex += this.itemsPerPage;
          this.itemEndIndex += this.itemsPerPage;
        }
        // these values need to be updated according to your use case and desired value for itemsPerPage
        this.scrollToRow(23);
      }
    },
    loadLess(entries, observer, isIntersecting) {
      //only change the indexes when isIntersecting is true i.e the component is visible in viewport
      if (isIntersecting) {
        if (this.itemStartIndex < this.itemsPerPage) {
          this.itemStartIndex = 0;
          this.itemEndIndex = this.itemsPerPage;
        } else {
          this.itemStartIndex -= this.itemsPerPage;
          this.itemEndIndex -= this.itemsPerPage;
        }
        // these values need to be updated according to your use case and desired value for itemsPerPage
        this.scrollToRow(5);
      }
    },
  },
};
</script>
```

## **Conclusion**

In this article, we explored how to enhance the performance of Vuetify data tables when dealing with large datasets. By implementing infinite scroll and leveraging the `v-intersect` directive, we significantly improved rendering speed by dynamically loading and displaying data as the user scrolls.

Note that the abovementioned implementation assumes the scenario where all the items are loaded beforehand and displayed in the `v-data-table`. However, the same approach can be modified to accommodate other use cases like the asynchronous fetching of items. By making a few adjustments, we can seamlessly integrate the infinite scroll behavior with the dynamic loading of data.

To explore the implementation and experiment with the code yourself, feel free to visit the following CodePen link: [CLICK HERE](https://codepen.io/takasivenkatasandeep-08/pen/jOQPGqN?editors=1010)

---

This article was written by [Takasi Venkata Sandeep](https://geekyants.com/takasi-venkata-sandeep/), Senior Software Engineer - I, for [the GeekyAnts blog.](https://geekyants.com/blog/enhancing-vuetify-data-table-performance-with-infinite-scroll/)
