Skip to content

Large Data Processing and Virtual Scrolling

Overview

This document provides comprehensive strategies for handling large datasets in Element Plus applications. We'll explore virtual scrolling implementations, data pagination techniques, memory-efficient data structures, and performance optimization patterns for applications dealing with thousands or millions of records.

Virtual Scrolling Implementation

1. Advanced Virtual List Component

typescript
// Advanced virtual scrolling with dynamic heights and horizontal support
import { defineComponent, ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { useResizeObserver, useThrottleFn } from '@vueuse/core'

interface VirtualScrollItem {
  index: number
  data: any
  height?: number
  width?: number
  offset: number
}

interface VirtualScrollProps {
  items: any[]
  itemSize: number | ((index: number) => number)
  containerSize: number
  direction?: 'vertical' | 'horizontal'
  overscan?: number
  getItemKey?: (item: any, index: number) => string | number
  estimatedItemSize?: number
  onScroll?: (scrollOffset: number) => void
  onVisibleRangeChange?: (startIndex: number, endIndex: number) => void
}

export const VirtualScroller = defineComponent({
  name: 'VirtualScroller',
  props: {
    items: {
      type: Array,
      required: true
    },
    itemSize: {
      type: [Number, Function],
      required: true
    },
    containerSize: {
      type: Number,
      required: true
    },
    direction: {
      type: String,
      default: 'vertical'
    },
    overscan: {
      type: Number,
      default: 5
    },
    getItemKey: {
      type: Function,
      default: (item: any, index: number) => index
    },
    estimatedItemSize: {
      type: Number,
      default: 50
    }
  },
  emits: ['scroll', 'visible-range-change'],
  setup(props, { emit, slots }) {
    const containerRef = ref<HTMLElement>()
    const scrollOffset = ref(0)
    const isHorizontal = computed(() => props.direction === 'horizontal')
    
    // Cache for measured sizes
    const sizeCache = new Map<number, number>()
    const offsetCache = new Map<number, number>()
    
    // Get item size with caching
    const getItemSize = (index: number): number => {
      if (sizeCache.has(index)) {
        return sizeCache.get(index)!
      }
      
      let size: number
      if (typeof props.itemSize === 'function') {
        size = props.itemSize(index)
      } else {
        size = props.itemSize
      }
      
      sizeCache.set(index, size)
      return size
    }
    
    // Get item offset with caching
    const getItemOffset = (index: number): number => {
      if (offsetCache.has(index)) {
        return offsetCache.get(index)!
      }
      
      let offset = 0
      for (let i = 0; i < index; i++) {
        offset += getItemSize(i)
      }
      
      offsetCache.set(index, offset)
      return offset
    }
    
    // Clear cache for items after the given index
    const clearCacheAfter = (index: number) => {
      for (let i = index; i < props.items.length; i++) {
        offsetCache.delete(i)
      }
    }
    
    // Update item size and clear dependent cache
    const updateItemSize = (index: number, size: number) => {
      const currentSize = sizeCache.get(index)
      if (currentSize !== size) {
        sizeCache.set(index, size)
        clearCacheAfter(index + 1)
      }
    }
    
    // Calculate total size
    const totalSize = computed(() => {
      if (props.items.length === 0) return 0
      return getItemOffset(props.items.length - 1) + getItemSize(props.items.length - 1)
    })
    
    // Binary search for start index
    const findStartIndex = (scrollOffset: number): number => {
      let low = 0
      let high = props.items.length - 1
      
      while (low <= high) {
        const mid = Math.floor((low + high) / 2)
        const offset = getItemOffset(mid)
        
        if (offset < scrollOffset) {
          low = mid + 1
        } else {
          high = mid - 1
        }
      }
      
      return Math.max(0, low - 1)
    }
    
    // Calculate visible range
    const visibleRange = computed(() => {
      const containerSize = props.containerSize
      const scrollStart = scrollOffset.value
      const scrollEnd = scrollStart + containerSize
      
      let startIndex = findStartIndex(scrollStart)
      let endIndex = startIndex
      
      // Find end index
      let currentOffset = getItemOffset(startIndex)
      while (endIndex < props.items.length && currentOffset < scrollEnd) {
        currentOffset += getItemSize(endIndex)
        endIndex++
      }
      
      // Apply overscan
      startIndex = Math.max(0, startIndex - props.overscan)
      endIndex = Math.min(props.items.length - 1, endIndex + props.overscan)
      
      return { startIndex, endIndex }
    })
    
    // Visible items with positioning
    const visibleItems = computed((): VirtualScrollItem[] => {
      const { startIndex, endIndex } = visibleRange.value
      const items: VirtualScrollItem[] = []
      
      for (let i = startIndex; i <= endIndex; i++) {
        items.push({
          index: i,
          data: props.items[i],
          height: isHorizontal.value ? undefined : getItemSize(i),
          width: isHorizontal.value ? getItemSize(i) : undefined,
          offset: getItemOffset(i)
        })
      }
      
      return items
    })
    
    // Throttled scroll handler
    const handleScroll = useThrottleFn((event: Event) => {
      const target = event.target as HTMLElement
      const newOffset = isHorizontal.value ? target.scrollLeft : target.scrollTop
      
      scrollOffset.value = newOffset
      emit('scroll', newOffset)
      
      const { startIndex, endIndex } = visibleRange.value
      emit('visible-range-change', startIndex, endIndex)
    }, 16)
    
    // Scroll to specific item
    const scrollToItem = (index: number, align: 'start' | 'center' | 'end' = 'start') => {
      if (!containerRef.value || index < 0 || index >= props.items.length) return
      
      const itemOffset = getItemOffset(index)
      const itemSize = getItemSize(index)
      const containerSize = props.containerSize
      
      let scrollTo = itemOffset
      
      if (align === 'center') {
        scrollTo = itemOffset - (containerSize - itemSize) / 2
      } else if (align === 'end') {
        scrollTo = itemOffset - containerSize + itemSize
      }
      
      scrollTo = Math.max(0, Math.min(scrollTo, totalSize.value - containerSize))
      
      if (isHorizontal.value) {
        containerRef.value.scrollLeft = scrollTo
      } else {
        containerRef.value.scrollTop = scrollTo
      }
    }
    
    // Scroll to offset
    const scrollToOffset = (offset: number) => {
      if (!containerRef.value) return
      
      const clampedOffset = Math.max(0, Math.min(offset, totalSize.value - props.containerSize))
      
      if (isHorizontal.value) {
        containerRef.value.scrollLeft = clampedOffset
      } else {
        containerRef.value.scrollTop = clampedOffset
      }
    }
    
    // Resize observer for dynamic sizing
    const itemRefs = ref(new Map<number, HTMLElement>())
    
    const observeItemSize = (index: number, element: HTMLElement) => {
      itemRefs.value.set(index, element)
      
      if (typeof props.itemSize === 'function') {
        useResizeObserver(element, (entries) => {
          const entry = entries[0]
          if (entry) {
            const size = isHorizontal.value 
              ? entry.contentRect.width 
              : entry.contentRect.height
            updateItemSize(index, size)
          }
        })
      }
    }
    
    return {
      containerRef,
      visibleItems,
      totalSize,
      handleScroll,
      scrollToItem,
      scrollToOffset,
      observeItemSize,
      isHorizontal
    }
  },
  render() {
    const containerStyle = {
      [this.isHorizontal ? 'width' : 'height']: `${this.containerSize}px`,
      overflow: 'auto'
    }
    
    const contentStyle = {
      [this.isHorizontal ? 'width' : 'height']: `${this.totalSize}px`,
      [this.isHorizontal ? 'height' : 'width']: '100%',
      position: 'relative'
    }
    
    return (
      <div
        ref="containerRef"
        class="virtual-scroller"
        style={containerStyle}
        onScroll={this.handleScroll}
      >
        <div class="virtual-scroller__content" style={contentStyle}>
          {this.visibleItems.map((item) => {
            const itemStyle = {
              position: 'absolute',
              [this.isHorizontal ? 'left' : 'top']: `${item.offset}px`,
              [this.isHorizontal ? 'width' : 'height']: `${item.height || item.width}px`,
              [this.isHorizontal ? 'height' : 'width']: '100%'
            }
            
            return (
              <div
                key={this.getItemKey(item.data, item.index)}
                class="virtual-scroller__item"
                style={itemStyle}
                ref={(el) => el && this.observeItemSize(item.index, el as HTMLElement)}
              >
                {this.$slots.default?.({ item: item.data, index: item.index })}
              </div>
            )
          })}
        </div>
      </div>
    )
  }
})

2. Virtual Table with Column Virtualization

typescript
// Virtual table with both row and column virtualization
import { defineComponent, ref, computed, reactive } from 'vue'

interface TableColumn {
  key: string
  title: string
  width: number
  fixed?: 'left' | 'right'
  render?: (value: any, record: any, index: number) => any
}

interface VirtualTableProps {
  data: any[]
  columns: TableColumn[]
  height: number
  width: number
  rowHeight: number
  headerHeight?: number
  overscanRowCount?: number
  overscanColumnCount?: number
}

export const VirtualTable = defineComponent({
  name: 'VirtualTable',
  props: {
    data: {
      type: Array,
      required: true
    },
    columns: {
      type: Array,
      required: true
    },
    height: {
      type: Number,
      required: true
    },
    width: {
      type: Number,
      required: true
    },
    rowHeight: {
      type: Number,
      default: 40
    },
    headerHeight: {
      type: Number,
      default: 40
    },
    overscanRowCount: {
      type: Number,
      default: 5
    },
    overscanColumnCount: {
      type: Number,
      default: 2
    }
  },
  setup(props) {
    const tableRef = ref<HTMLElement>()
    const scrollState = reactive({
      scrollTop: 0,
      scrollLeft: 0
    })
    
    // Column calculations
    const columnMetrics = computed(() => {
      let offset = 0
      const metrics = props.columns.map(column => {
        const metric = {
          ...column,
          offset,
          width: column.width
        }
        offset += column.width
        return metric
      })
      
      return {
        columns: metrics,
        totalWidth: offset
      }
    })
    
    // Visible column range
    const visibleColumnRange = computed(() => {
      const { columns } = columnMetrics.value
      const scrollLeft = scrollState.scrollLeft
      const viewportWidth = props.width
      
      let startIndex = 0
      let endIndex = columns.length - 1
      
      // Find start column
      for (let i = 0; i < columns.length; i++) {
        if (columns[i].offset + columns[i].width > scrollLeft) {
          startIndex = i
          break
        }
      }
      
      // Find end column
      for (let i = startIndex; i < columns.length; i++) {
        if (columns[i].offset > scrollLeft + viewportWidth) {
          endIndex = i - 1
          break
        }
      }
      
      // Apply overscan
      startIndex = Math.max(0, startIndex - props.overscanColumnCount)
      endIndex = Math.min(columns.length - 1, endIndex + props.overscanColumnCount)
      
      return { startIndex, endIndex }
    })
    
    // Visible row range
    const visibleRowRange = computed(() => {
      const scrollTop = scrollState.scrollTop
      const viewportHeight = props.height - props.headerHeight
      
      const startIndex = Math.floor(scrollTop / props.rowHeight)
      const endIndex = Math.min(
        props.data.length - 1,
        Math.ceil((scrollTop + viewportHeight) / props.rowHeight)
      )
      
      // Apply overscan
      const overscanStartIndex = Math.max(0, startIndex - props.overscanRowCount)
      const overscanEndIndex = Math.min(props.data.length - 1, endIndex + props.overscanRowCount)
      
      return {
        startIndex: overscanStartIndex,
        endIndex: overscanEndIndex
      }
    })
    
    // Visible columns
    const visibleColumns = computed(() => {
      const { startIndex, endIndex } = visibleColumnRange.value
      return columnMetrics.value.columns.slice(startIndex, endIndex + 1)
    })
    
    // Visible rows
    const visibleRows = computed(() => {
      const { startIndex, endIndex } = visibleRowRange.value
      return props.data.slice(startIndex, endIndex + 1).map((row, index) => ({
        data: row,
        index: startIndex + index
      }))
    })
    
    // Scroll handler
    const handleScroll = (event: Event) => {
      const target = event.target as HTMLElement
      scrollState.scrollTop = target.scrollTop
      scrollState.scrollLeft = target.scrollLeft
    }
    
    // Scroll to row
    const scrollToRow = (rowIndex: number) => {
      if (!tableRef.value) return
      
      const scrollTop = rowIndex * props.rowHeight
      tableRef.value.scrollTop = scrollTop
    }
    
    // Scroll to column
    const scrollToColumn = (columnIndex: number) => {
      if (!tableRef.value) return
      
      const column = columnMetrics.value.columns[columnIndex]
      if (column) {
        tableRef.value.scrollLeft = column.offset
      }
    }
    
    return {
      tableRef,
      columnMetrics,
      visibleColumns,
      visibleRows,
      visibleRowRange,
      scrollState,
      handleScroll,
      scrollToRow,
      scrollToColumn
    }
  },
  render() {
    const tableStyle = {
      width: `${this.width}px`,
      height: `${this.height}px`,
      overflow: 'auto'
    }
    
    const contentStyle = {
      width: `${this.columnMetrics.totalWidth}px`,
      height: `${this.data.length * this.rowHeight + this.headerHeight}px`,
      position: 'relative'
    }
    
    const headerStyle = {
      position: 'absolute',
      top: '0px',
      left: `${-this.scrollState.scrollLeft}px`,
      height: `${this.headerHeight}px`,
      display: 'flex',
      backgroundColor: '#f5f7fa',
      borderBottom: '1px solid #ebeef5'
    }
    
    return (
      <div
        ref="tableRef"
        class="virtual-table"
        style={tableStyle}
        onScroll={this.handleScroll}
      >
        <div class="virtual-table__content" style={contentStyle}>
          {/* Header */}
          <div class="virtual-table__header" style={headerStyle}>
            {this.visibleColumns.map(column => (
              <div
                key={column.key}
                class="virtual-table__header-cell"
                style={{
                  width: `${column.width}px`,
                  height: `${this.headerHeight}px`,
                  display: 'flex',
                  alignItems: 'center',
                  padding: '0 12px',
                  borderRight: '1px solid #ebeef5'
                }}
              >
                {column.title}
              </div>
            ))}
          </div>
          
          {/* Rows */}
          {this.visibleRows.map(({ data: row, index }) => {
            const rowStyle = {
              position: 'absolute',
              top: `${index * this.rowHeight + this.headerHeight}px`,
              left: `${-this.scrollState.scrollLeft}px`,
              height: `${this.rowHeight}px`,
              display: 'flex',
              borderBottom: '1px solid #ebeef5'
            }
            
            return (
              <div
                key={index}
                class="virtual-table__row"
                style={rowStyle}
              >
                {this.visibleColumns.map(column => (
                  <div
                    key={column.key}
                    class="virtual-table__cell"
                    style={{
                      width: `${column.width}px`,
                      height: `${this.rowHeight}px`,
                      display: 'flex',
                      alignItems: 'center',
                      padding: '0 12px',
                      borderRight: '1px solid #ebeef5'
                    }}
                  >
                    {column.render 
                      ? column.render(row[column.key], row, index)
                      : row[column.key]
                    }
                  </div>
                ))}
              </div>
            )
          })}
        </div>
      </div>
    )
  }
})

Data Pagination and Lazy Loading

1. Advanced Pagination with Caching

typescript
// Advanced pagination system with intelligent caching
import { ref, computed, reactive, watch } from 'vue'

interface PaginationOptions {
  pageSize: number
  prefetchPages?: number
  cacheSize?: number
  enableInfiniteScroll?: boolean
}

interface PageData<T> {
  data: T[]
  total: number
  hasMore: boolean
  timestamp: number
}

interface DataFetcher<T> {
  (page: number, pageSize: number, filters?: any): Promise<{
    data: T[]
    total: number
    hasMore: boolean
  }>
}

export function usePaginatedData<T>(
  fetcher: DataFetcher<T>,
  options: PaginationOptions
) {
  const {
    pageSize,
    prefetchPages = 1,
    cacheSize = 10,
    enableInfiniteScroll = false
  } = options
  
  // State
  const currentPage = ref(1)
  const loading = ref(false)
  const error = ref<Error | null>(null)
  const filters = ref<any>({})
  
  // Cache management
  const cache = reactive(new Map<string, PageData<T>>())
  const cacheKeys = ref<string[]>([])
  
  // Generate cache key
  const getCacheKey = (page: number, filters: any) => {
    return `${page}-${JSON.stringify(filters)}`
  }
  
  // Cache operations
  const setCache = (key: string, data: PageData<T>) => {
    if (cache.size >= cacheSize) {
      // Remove oldest cache entry
      const oldestKey = cacheKeys.value.shift()
      if (oldestKey) {
        cache.delete(oldestKey)
      }
    }
    
    cache.set(key, data)
    cacheKeys.value.push(key)
  }
  
  const getCache = (key: string): PageData<T> | undefined => {
    const data = cache.get(key)
    if (data) {
      // Move to end (LRU)
      const index = cacheKeys.value.indexOf(key)
      if (index > -1) {
        cacheKeys.value.splice(index, 1)
        cacheKeys.value.push(key)
      }
    }
    return data
  }
  
  // Fetch data for a specific page
  const fetchPage = async (page: number, useCache = true): Promise<PageData<T>> => {
    const cacheKey = getCacheKey(page, filters.value)
    
    // Check cache first
    if (useCache) {
      const cached = getCache(cacheKey)
      if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) { // 5 minutes
        return cached
      }
    }
    
    try {
      const result = await fetcher(page, pageSize, filters.value)
      const pageData: PageData<T> = {
        ...result,
        timestamp: Date.now()
      }
      
      setCache(cacheKey, pageData)
      return pageData
    } catch (err) {
      throw err as Error
    }
  }
  
  // Current page data
  const currentPageData = computed(() => {
    const cacheKey = getCacheKey(currentPage.value, filters.value)
    return getCache(cacheKey)
  })
  
  // Load current page
  const loadPage = async (page: number) => {
    if (loading.value) return
    
    loading.value = true
    error.value = null
    
    try {
      await fetchPage(page)
      currentPage.value = page
      
      // Prefetch adjacent pages
      if (prefetchPages > 0) {
        const prefetchPromises: Promise<any>[] = []
        
        for (let i = 1; i <= prefetchPages; i++) {
          if (page + i <= Math.ceil((currentPageData.value?.total || 0) / pageSize)) {
            prefetchPromises.push(fetchPage(page + i, false).catch(() => {}))
          }
          if (page - i >= 1) {
            prefetchPromises.push(fetchPage(page - i, false).catch(() => {}))
          }
        }
        
        // Don't wait for prefetch
        Promise.all(prefetchPromises).catch(() => {})
      }
    } catch (err) {
      error.value = err as Error
    } finally {
      loading.value = false
    }
  }
  
  // Infinite scroll data
  const allData = ref<T[]>([])
  const hasMore = ref(true)
  
  // Load more for infinite scroll
  const loadMore = async () => {
    if (loading.value || !hasMore.value) return
    
    const nextPage = Math.floor(allData.value.length / pageSize) + 1
    
    loading.value = true
    error.value = null
    
    try {
      const pageData = await fetchPage(nextPage)
      allData.value.push(...pageData.data)
      hasMore.value = pageData.hasMore
    } catch (err) {
      error.value = err as Error
    } finally {
      loading.value = false
    }
  }
  
  // Navigation methods
  const goToPage = (page: number) => {
    if (page >= 1) {
      loadPage(page)
    }
  }
  
  const nextPage = () => {
    const totalPages = Math.ceil((currentPageData.value?.total || 0) / pageSize)
    if (currentPage.value < totalPages) {
      goToPage(currentPage.value + 1)
    }
  }
  
  const prevPage = () => {
    if (currentPage.value > 1) {
      goToPage(currentPage.value - 1)
    }
  }
  
  // Filter management
  const updateFilters = (newFilters: any) => {
    filters.value = { ...newFilters }
    if (enableInfiniteScroll) {
      allData.value = []
      hasMore.value = true
      loadMore()
    } else {
      goToPage(1)
    }
  }
  
  // Refresh current page
  const refresh = () => {
    const cacheKey = getCacheKey(currentPage.value, filters.value)
    cache.delete(cacheKey)
    
    if (enableInfiniteScroll) {
      allData.value = []
      hasMore.value = true
      loadMore()
    } else {
      loadPage(currentPage.value)
    }
  }
  
  // Clear cache
  const clearCache = () => {
    cache.clear()
    cacheKeys.value = []
  }
  
  // Computed properties
  const totalPages = computed(() => {
    return Math.ceil((currentPageData.value?.total || 0) / pageSize)
  })
  
  const canGoNext = computed(() => {
    return currentPage.value < totalPages.value
  })
  
  const canGoPrev = computed(() => {
    return currentPage.value > 1
  })
  
  // Initialize
  if (enableInfiniteScroll) {
    loadMore()
  } else {
    loadPage(1)
  }
  
  return {
    // State
    currentPage: readonly(currentPage),
    loading: readonly(loading),
    error: readonly(error),
    filters: readonly(filters),
    
    // Data
    currentPageData,
    allData: readonly(allData),
    hasMore: readonly(hasMore),
    
    // Computed
    totalPages,
    canGoNext,
    canGoPrev,
    
    // Methods
    goToPage,
    nextPage,
    prevPage,
    loadMore,
    updateFilters,
    refresh,
    clearCache
  }
}

2. Infinite Scroll Implementation

typescript
// Infinite scroll with intersection observer
import { ref, onMounted, onBeforeUnmount } from 'vue'

interface InfiniteScrollOptions {
  threshold?: number
  rootMargin?: string
  immediate?: boolean
  distance?: number
  disabled?: boolean
}

export function useInfiniteScroll(
  target: Ref<HTMLElement | undefined>,
  onLoadMore: () => void | Promise<void>,
  options: InfiniteScrollOptions = {}
) {
  const {
    threshold = 0,
    rootMargin = '0px',
    immediate = true,
    distance = 0,
    disabled = false
  } = options
  
  const isLoading = ref(false)
  const observer = ref<IntersectionObserver>()
  
  const stop = () => {
    if (observer.value) {
      observer.value.disconnect()
      observer.value = undefined
    }
  }
  
  const start = () => {
    if (disabled || !target.value) return
    
    stop()
    
    observer.value = new IntersectionObserver(
      async ([entry]) => {
        if (entry.isIntersecting && !isLoading.value) {
          isLoading.value = true
          
          try {
            await onLoadMore()
          } catch (error) {
            console.error('Infinite scroll load error:', error)
          } finally {
            isLoading.value = false
          }
        }
      },
      {
        threshold,
        rootMargin
      }
    )
    
    // Create sentinel element
    const sentinel = document.createElement('div')
    sentinel.style.height = '1px'
    sentinel.style.position = 'absolute'
    sentinel.style.bottom = `${distance}px`
    sentinel.style.width = '100%'
    
    target.value.appendChild(sentinel)
    observer.value.observe(sentinel)
  }
  
  onMounted(() => {
    if (immediate) {
      start()
    }
  })
  
  onBeforeUnmount(stop)
  
  return {
    isLoading: readonly(isLoading),
    start,
    stop
  }
}

// Infinite scroll component
export const InfiniteScrollList = defineComponent({
  name: 'InfiniteScrollList',
  props: {
    items: {
      type: Array,
      required: true
    },
    loading: Boolean,
    hasMore: Boolean,
    loadMore: {
      type: Function,
      required: true
    },
    threshold: {
      type: Number,
      default: 0.1
    },
    distance: {
      type: Number,
      default: 100
    }
  },
  setup(props) {
    const listRef = ref<HTMLElement>()
    
    const { isLoading } = useInfiniteScroll(
      listRef,
      props.loadMore,
      {
        threshold: props.threshold,
        distance: props.distance,
        disabled: computed(() => props.loading || !props.hasMore)
      }
    )
    
    return {
      listRef,
      isLoading
    }
  },
  render() {
    return (
      <div ref="listRef" class="infinite-scroll-list">
        {/* Render items */}
        {this.items.map((item, index) => (
          <div key={index} class="infinite-scroll-item">
            {this.$slots.default?.({ item, index })}
          </div>
        ))}
        
        {/* Loading indicator */}
        {(this.loading || this.isLoading) && (
          <div class="infinite-scroll-loading">
            {this.$slots.loading?.() || 'Loading...'}
          </div>
        )}
        
        {/* No more data indicator */}
        {!this.hasMore && this.items.length > 0 && (
          <div class="infinite-scroll-finished">
            {this.$slots.finished?.() || 'No more data'}
          </div>
        )}
      </div>
    )
  }
})

Memory-Efficient Data Structures

1. Chunked Data Management

typescript
// Chunked data structure for memory efficiency
class ChunkedDataManager<T> {
  private chunks = new Map<number, T[]>()
  private chunkSize: number
  private maxChunks: number
  private accessOrder: number[] = []
  
  constructor(chunkSize = 1000, maxChunks = 10) {
    this.chunkSize = chunkSize
    this.maxChunks = maxChunks
  }
  
  private getChunkIndex(index: number): number {
    return Math.floor(index / this.chunkSize)
  }
  
  private getItemIndex(index: number): number {
    return index % this.chunkSize
  }
  
  private evictOldestChunk(): void {
    if (this.accessOrder.length >= this.maxChunks) {
      const oldestChunk = this.accessOrder.shift()
      if (oldestChunk !== undefined) {
        this.chunks.delete(oldestChunk)
      }
    }
  }
  
  private updateAccessOrder(chunkIndex: number): void {
    const existingIndex = this.accessOrder.indexOf(chunkIndex)
    if (existingIndex > -1) {
      this.accessOrder.splice(existingIndex, 1)
    }
    this.accessOrder.push(chunkIndex)
  }
  
  setChunk(chunkIndex: number, data: T[]): void {
    this.evictOldestChunk()
    this.chunks.set(chunkIndex, data)
    this.updateAccessOrder(chunkIndex)
  }
  
  getItem(index: number): T | undefined {
    const chunkIndex = this.getChunkIndex(index)
    const itemIndex = this.getItemIndex(index)
    
    const chunk = this.chunks.get(chunkIndex)
    if (chunk) {
      this.updateAccessOrder(chunkIndex)
      return chunk[itemIndex]
    }
    
    return undefined
  }
  
  hasChunk(chunkIndex: number): boolean {
    return this.chunks.has(chunkIndex)
  }
  
  getChunk(chunkIndex: number): T[] | undefined {
    const chunk = this.chunks.get(chunkIndex)
    if (chunk) {
      this.updateAccessOrder(chunkIndex)
    }
    return chunk
  }
  
  clear(): void {
    this.chunks.clear()
    this.accessOrder = []
  }
  
  getMemoryUsage(): { chunks: number; totalItems: number } {
    let totalItems = 0
    this.chunks.forEach(chunk => {
      totalItems += chunk.length
    })
    
    return {
      chunks: this.chunks.size,
      totalItems
    }
  }
}

// Composable for chunked data
export function useChunkedData<T>(
  fetcher: (chunkIndex: number, chunkSize: number) => Promise<T[]>,
  chunkSize = 1000,
  maxChunks = 10
) {
  const dataManager = new ChunkedDataManager<T>(chunkSize, maxChunks)
  const loading = ref(new Set<number>())
  const error = ref<Error | null>(null)
  
  const loadChunk = async (chunkIndex: number): Promise<T[]> => {
    if (dataManager.hasChunk(chunkIndex)) {
      return dataManager.getChunk(chunkIndex)!
    }
    
    if (loading.value.has(chunkIndex)) {
      // Wait for existing request
      return new Promise((resolve, reject) => {
        const checkLoading = () => {
          if (!loading.value.has(chunkIndex)) {
            const chunk = dataManager.getChunk(chunkIndex)
            if (chunk) {
              resolve(chunk)
            } else {
              reject(new Error('Failed to load chunk'))
            }
          } else {
            setTimeout(checkLoading, 10)
          }
        }
        checkLoading()
      })
    }
    
    loading.value.add(chunkIndex)
    
    try {
      const data = await fetcher(chunkIndex, chunkSize)
      dataManager.setChunk(chunkIndex, data)
      return data
    } catch (err) {
      error.value = err as Error
      throw err
    } finally {
      loading.value.delete(chunkIndex)
    }
  }
  
  const getItem = async (index: number): Promise<T | undefined> => {
    const chunkIndex = Math.floor(index / chunkSize)
    
    try {
      await loadChunk(chunkIndex)
      return dataManager.getItem(index)
    } catch (err) {
      console.error('Failed to get item:', err)
      return undefined
    }
  }
  
  const getItems = async (startIndex: number, count: number): Promise<T[]> => {
    const items: T[] = []
    const promises: Promise<void>[] = []
    
    for (let i = 0; i < count; i++) {
      const index = startIndex + i
      promises.push(
        getItem(index).then(item => {
          if (item !== undefined) {
            items[i] = item
          }
        })
      )
    }
    
    await Promise.all(promises)
    return items.filter(item => item !== undefined)
  }
  
  const preloadChunks = async (chunkIndices: number[]): Promise<void> => {
    const promises = chunkIndices.map(index => 
      loadChunk(index).catch(() => {})
    )
    await Promise.all(promises)
  }
  
  const clear = () => {
    dataManager.clear()
    loading.value.clear()
    error.value = null
  }
  
  const getMemoryUsage = () => {
    return dataManager.getMemoryUsage()
  }
  
  return {
    getItem,
    getItems,
    loadChunk,
    preloadChunks,
    clear,
    getMemoryUsage,
    loading: readonly(loading),
    error: readonly(error)
  }
}

2. Data Streaming and Progressive Loading

typescript
// Progressive data loading with streaming
export class DataStream<T> {
  private buffer: T[] = []
  private bufferSize: number
  private onData: (items: T[]) => void
  private onError: (error: Error) => void
  private onComplete: () => void
  
  constructor(
    bufferSize = 100,
    callbacks: {
      onData: (items: T[]) => void
      onError: (error: Error) => void
      onComplete: () => void
    }
  ) {
    this.bufferSize = bufferSize
    this.onData = callbacks.onData
    this.onError = callbacks.onError
    this.onComplete = callbacks.onComplete
  }
  
  push(item: T): void {
    this.buffer.push(item)
    
    if (this.buffer.length >= this.bufferSize) {
      this.flush()
    }
  }
  
  pushBatch(items: T[]): void {
    this.buffer.push(...items)
    
    while (this.buffer.length >= this.bufferSize) {
      const batch = this.buffer.splice(0, this.bufferSize)
      this.onData(batch)
    }
  }
  
  flush(): void {
    if (this.buffer.length > 0) {
      const batch = this.buffer.splice(0)
      this.onData(batch)
    }
  }
  
  complete(): void {
    this.flush()
    this.onComplete()
  }
  
  error(err: Error): void {
    this.onError(err)
  }
}

// Progressive loading composable
export function useProgressiveLoading<T>(
  fetcher: (offset: number, limit: number) => Promise<{ data: T[]; hasMore: boolean }>,
  options: {
    batchSize?: number
    bufferSize?: number
    autoStart?: boolean
  } = {}
) {
  const {
    batchSize = 100,
    bufferSize = 50,
    autoStart = true
  } = options
  
  const data = ref<T[]>([])
  const loading = ref(false)
  const error = ref<Error | null>(null)
  const hasMore = ref(true)
  const progress = ref(0)
  
  let offset = 0
  let stream: DataStream<T> | null = null
  
  const createStream = () => {
    return new DataStream<T>(bufferSize, {
      onData: (items: T[]) => {
        data.value.push(...items)
        progress.value = data.value.length
      },
      onError: (err: Error) => {
        error.value = err
        loading.value = false
      },
      onComplete: () => {
        loading.value = false
      }
    })
  }
  
  const loadBatch = async (): Promise<boolean> => {
    try {
      const result = await fetcher(offset, batchSize)
      
      if (stream) {
        stream.pushBatch(result.data)
      }
      
      offset += result.data.length
      hasMore.value = result.hasMore
      
      return result.hasMore
    } catch (err) {
      if (stream) {
        stream.error(err as Error)
      }
      return false
    }
  }
  
  const start = async () => {
    if (loading.value) return
    
    loading.value = true
    error.value = null
    stream = createStream()
    
    try {
      while (hasMore.value) {
        const shouldContinue = await loadBatch()
        if (!shouldContinue) break
        
        // Allow UI to update
        await nextTick()
      }
      
      stream.complete()
    } catch (err) {
      if (stream) {
        stream.error(err as Error)
      }
    }
  }
  
  const reset = () => {
    data.value = []
    offset = 0
    hasMore.value = true
    progress.value = 0
    error.value = null
    loading.value = false
    stream = null
  }
  
  const loadMore = async () => {
    if (loading.value || !hasMore.value) return
    
    loading.value = true
    
    if (!stream) {
      stream = createStream()
    }
    
    await loadBatch()
    loading.value = false
  }
  
  if (autoStart) {
    start()
  }
  
  return {
    data: readonly(data),
    loading: readonly(loading),
    error: readonly(error),
    hasMore: readonly(hasMore),
    progress: readonly(progress),
    start,
    reset,
    loadMore
  }
}

Performance Optimization Patterns

1. Data Processing Optimization

typescript
// Optimized data processing with Web Workers
export class DataProcessor {
  private worker: Worker | null = null
  private taskQueue: Array<{
    id: string
    data: any
    resolve: (result: any) => void
    reject: (error: Error) => void
  }> = []
  
  constructor(workerScript: string) {
    this.initWorker(workerScript)
  }
  
  private initWorker(script: string) {
    try {
      this.worker = new Worker(script)
      this.worker.onmessage = this.handleWorkerMessage.bind(this)
      this.worker.onerror = this.handleWorkerError.bind(this)
    } catch (error) {
      console.warn('Web Worker not supported, falling back to main thread')
    }
  }
  
  private handleWorkerMessage(event: MessageEvent) {
    const { id, result, error } = event.data
    const task = this.taskQueue.find(t => t.id === id)
    
    if (task) {
      this.taskQueue = this.taskQueue.filter(t => t.id !== id)
      
      if (error) {
        task.reject(new Error(error))
      } else {
        task.resolve(result)
      }
    }
  }
  
  private handleWorkerError(error: ErrorEvent) {
    console.error('Worker error:', error)
    this.taskQueue.forEach(task => {
      task.reject(new Error('Worker error'))
    })
    this.taskQueue = []
  }
  
  async process<T, R>(data: T, operation: string): Promise<R> {
    if (!this.worker) {
      // Fallback to main thread
      return this.processInMainThread(data, operation)
    }
    
    return new Promise((resolve, reject) => {
      const id = Math.random().toString(36).substr(2, 9)
      
      this.taskQueue.push({ id, data, resolve, reject })
      
      this.worker!.postMessage({
        id,
        operation,
        data
      })
    })
  }
  
  private async processInMainThread<T, R>(data: T, operation: string): Promise<R> {
    // Implement fallback processing
    switch (operation) {
      case 'sort':
        return this.sortData(data as any) as R
      case 'filter':
        return this.filterData(data as any) as R
      case 'transform':
        return this.transformData(data as any) as R
      default:
        throw new Error(`Unknown operation: ${operation}`)
    }
  }
  
  private sortData(data: any[]): any[] {
    return [...data].sort((a, b) => {
      // Implement sorting logic
      return a.id - b.id
    })
  }
  
  private filterData(data: any[]): any[] {
    return data.filter(item => {
      // Implement filtering logic
      return item.active
    })
  }
  
  private transformData(data: any[]): any[] {
    return data.map(item => {
      // Implement transformation logic
      return { ...item, processed: true }
    })
  }
  
  destroy() {
    if (this.worker) {
      this.worker.terminate()
      this.worker = null
    }
    this.taskQueue = []
  }
}

// Composable for data processing
export function useDataProcessor(workerScript?: string) {
  const processor = ref<DataProcessor | null>(null)
  
  onMounted(() => {
    if (workerScript) {
      processor.value = new DataProcessor(workerScript)
    }
  })
  
  onBeforeUnmount(() => {
    processor.value?.destroy()
  })
  
  const processData = async <T, R>(data: T, operation: string): Promise<R> => {
    if (!processor.value) {
      throw new Error('Data processor not initialized')
    }
    
    return processor.value.process<T, R>(data, operation)
  }
  
  return {
    processData
  }
}

2. Batch Operations

typescript
// Batch operation manager
export class BatchOperationManager {
  private batches = new Map<string, {
    operations: Array<() => Promise<any>>
    timer: NodeJS.Timeout | null
    resolve: (results: any[]) => void
    reject: (error: Error) => void
  }>()
  
  private batchSize: number
  private delay: number
  
  constructor(batchSize = 10, delay = 100) {
    this.batchSize = batchSize
    this.delay = delay
  }
  
  addOperation<T>(
    batchKey: string,
    operation: () => Promise<T>
  ): Promise<T> {
    return new Promise((resolve, reject) => {
      if (!this.batches.has(batchKey)) {
        this.batches.set(batchKey, {
          operations: [],
          timer: null,
          resolve: () => {},
          reject: () => {}
        })
      }
      
      const batch = this.batches.get(batchKey)!
      batch.operations.push(operation)
      
      // Clear existing timer
      if (batch.timer) {
        clearTimeout(batch.timer)
      }
      
      // Execute immediately if batch is full
      if (batch.operations.length >= this.batchSize) {
        this.executeBatch(batchKey)
      } else {
        // Set timer for delayed execution
        batch.timer = setTimeout(() => {
          this.executeBatch(batchKey)
        }, this.delay)
      }
      
      // Store resolve/reject for this specific operation
      const operationIndex = batch.operations.length - 1
      const originalResolve = batch.resolve
      const originalReject = batch.reject
      
      batch.resolve = (results: any[]) => {
        resolve(results[operationIndex])
        originalResolve(results)
      }
      
      batch.reject = (error: Error) => {
        reject(error)
        originalReject(error)
      }
    })
  }
  
  private async executeBatch(batchKey: string) {
    const batch = this.batches.get(batchKey)
    if (!batch) return
    
    const { operations, timer } = batch
    
    if (timer) {
      clearTimeout(timer)
    }
    
    this.batches.delete(batchKey)
    
    try {
      const results = await Promise.all(
        operations.map(op => op())
      )
      batch.resolve(results)
    } catch (error) {
      batch.reject(error as Error)
    }
  }
  
  flush(batchKey?: string) {
    if (batchKey) {
      this.executeBatch(batchKey)
    } else {
      // Flush all batches
      for (const key of this.batches.keys()) {
        this.executeBatch(key)
      }
    }
  }
  
  clear() {
    for (const batch of this.batches.values()) {
      if (batch.timer) {
        clearTimeout(batch.timer)
      }
    }
    this.batches.clear()
  }
}

// Composable for batch operations
export function useBatchOperations(batchSize = 10, delay = 100) {
  const manager = new BatchOperationManager(batchSize, delay)
  
  onBeforeUnmount(() => {
    manager.clear()
  })
  
  const addToBatch = <T>(
    batchKey: string,
    operation: () => Promise<T>
  ): Promise<T> => {
    return manager.addOperation(batchKey, operation)
  }
  
  const flushBatch = (batchKey?: string) => {
    manager.flush(batchKey)
  }
  
  return {
    addToBatch,
    flushBatch
  }
}

Best Practices

1. Performance Guidelines

  • Virtual Scrolling: Essential for lists with >1000 items
  • Chunked Loading: Use for datasets with >10,000 items
  • Memory Management: Implement LRU caching for large datasets
  • Progressive Loading: Stream data for better perceived performance
  • Batch Operations: Group similar operations to reduce overhead

2. Memory Optimization

  • Lazy Loading: Load data only when needed
  • Data Cleanup: Remove unused data from memory
  • Efficient Data Structures: Use appropriate data structures for your use case
  • Garbage Collection: Be mindful of object references

3. User Experience

  • Loading States: Provide clear feedback during data loading
  • Error Handling: Gracefully handle network and data errors
  • Smooth Scrolling: Maintain 60fps during scrolling
  • Responsive Design: Ensure performance across different devices

Conclusion

Effective large data processing in Element Plus applications requires:

  • Virtual Scrolling: For rendering performance with large lists
  • Smart Pagination: With caching and prefetching strategies
  • Memory Management: Efficient data structures and cleanup
  • Progressive Loading: For better user experience
  • Performance Monitoring: Continuous optimization based on metrics

By implementing these patterns, you can handle millions of records while maintaining excellent performance and user experience.

Element Plus Study Guide