High-Performance Virtual Scrolling Select Component for Vue 2 Tables

When embedding input fields or dropdown selects inside table cells, rendering large datasest can freeze the browser or exhaust memory before the options even finish loading. A virtual scrolling strategy miitgates this by only rendering visible items.

Below is an implementation using vue-virtual-scroller to build a performant select popover suitable for massive option lists in Vue 2 environments.

Component Usage

<virtual-scroll-select
  :options="records"
  identifier="uid"
  field="selectedUid"
  :value.sync="row.selectedUid"
  @selection="handleUidChange($index, 'selectedUid', row.selectedUid, row)">
</virtual-scroll-select>

Template Structure

<template>
  <div class="vscroll-select">
    <el-popover
      ref="dropdown"
      placement="bottom"
      popper-class="vscroll-popover"
      :visible-arrow="false"
      @show="handleOpen"
      @hide="handleClose"
      v-model="visible"
      trigger="focus">
      <div class="viewport" :style="{ '--viewH': viewHeight }">
        <RecycleScroller
          ref="scroller"
          class="scroller-area"
          :key="fullPageKey"
          :items="matchedItems"
          :item-size="null"
          :page-mode="false"
          size-field="height"
          :key-field="identifier"
          item-class="entry-item"
        >
          <template #default="{ item }">
            <div
              class="entry"
              :class="{ 'chosen': item[identifier] === activeUid }"
              @click="pickOption(item)"
            >
              <span>{{ item[identifier] }}</span>
            </div>
          </template>
          <template #empty v-if="matchedItems.length === 0">
            <div class="no-data">No matching entries</div>
          </template>
        </RecycleScroller>
      </div>
    </el-popover>
    <custom-input
      v-popover:dropdown
      v-model="query"
      placeholder="Select..."
      @input="performFilter">
    </custom-input>
  </div>
</template>

Script Logic

<script>
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
import CustomInput from '@/components/CustomInput'
import debounce from 'lodash/debounce'

export default {
  name: 'VirtualScrollSelect',
  components: {
    RecycleScroller,
    CustomInput
  },
  props: {
    options: {
      type: Array,
      required: true
    },
    value: {
      type: String,
      default: ''
    },
    identifier: {
      type: String,
      default: 'id'
    },
    field: {
      type: String,
      default: ''
    }
  },
  data () {
    return {
      query: '',
      matchedItems: [],
      visible: false,
      activeUid: '',
      fullPageKey: true
    }
  },
  computed: {
    viewHeight () {
      const count = this.matchedItems.length
      if (count > 6) return '200px'
      if (count === 0) return '40px'
      return `${count * 32}px`
    }
  },
  watch: {
    options: {
      handler (newList) {
        this.matchedItems = newList
      },
      deep: true
    },
    value (newVal) {
      this.activeUid = newVal
      this.query = newVal
    }
  },
  methods: {
    handleOpen () {
      this.$nextTick(() => {
        const idx = this.options.findIndex(opt => opt[this.identifier] === this.value)
        this.$refs.scroller.scrollToItem(idx)
      })
    },
    performFilter: debounce(function (val) {
      this.$emit(`update:${this.field}`, val)
      const term = val.toLowerCase()
      this.matchedItems = this.options.filter(opt =>
        opt[this.identifier].toLowerCase().includes(term)
      )
    }, 300),
    pickOption (opt) {
      this.activeUid = opt[this.identifier]
      this.query = opt[this.identifier]
      this.visible = false
      this.$emit(`update:${this.field}`, opt[this.identifier])
      this.$emit('selection', opt)
    },
    handleClose () {
      if (this.query !== this.activeUid) {
        this.query = this.activeUid
        this.$emit(`update:${this.field}`, this.activeUid)
      }
      this.matchedItems = this.options
    }
  },
  mounted () {
    this.activeUid = this.value
    this.query = this.value
    this.matchedItems = this.options
  }
}
</script>

Styling

<style lang="less" scoped>
.vscroll-select {
  width: 100%;
  position: relative;
}
.viewport {
  height: var(--viewH);
  background: #fefefe;
  .scroller-area {
    width: 100%;
    height: 100%;
    ::v-deep .entry-item {
      border-bottom: 1px solid #e9eaf0;
      font-size: 14px;
      cursor: pointer;
      height: 32px;
      line-height: 19px;
      padding: 6px 12px;
      box-sizing: border-box;
      .entry {
        color: #616163;
        text-align: center;
        background: #fff;
        border-radius: 4px;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        box-sizing: border-box;
        &:hover {
          color: #fff;
          background: #6d71f9;
        }
        span {
          display: inline-block;
          width: 100%;
          height: 100%;
          padding: 0 10px;
          box-sizing: border-box;
        }
      }
      .chosen {
        color: #fff;
        background: #6d71f9;
      }
    }
    .no-data {
      padding: 10px 0;
      text-align: center;
      color: #999;
      font-size: 14px;
    }
    &::-webkit-scrollbar {
      width: 6px;
      height: 6px;
      transition: background-color .2s linear, width .2s ease-in-out;
    }
    &::-webkit-scrollbar-thumb {
      border-radius: 10px;
      background: rgba(0,0,0,0.1);
    }
    &::-webkit-scrollbar-thumb:hover {
      width: 10px;
      background: rgba(0,0,0,0.3);
    }
    &::-webkit-scrollbar-track {
      border-radius: 10px;
      background: rgba(0,0,0,0.2);
    }
    &::-webkit-scrollbar-thumb:active {
      width: 10px;
      background: rgba(0,0,0,.6);
    }
  }
}
</style>

<style lang="less">
.vscroll-popover {
  margin: 0;
  border-color: #e9eaf0;
  border-radius: 0;
  box-shadow: 0 0 10px rgba(168, 174, 187, 0.25);
  border: 1px solid #E4E7ED;
  padding: 0;
  box-sizing: border-box;
}
</style>

Tags: vue2 Virtual Scrolling Select Component performance optimization Large Dataset Rendering

Posted on Sun, 21 Jun 2026 16:59:49 +0000 by Aethaellyn