Skip to content

Component Performance Optimization Strategies

Overview

This document explores comprehensive performance optimization strategies for Element Plus components. We'll cover rendering optimization, memory management, bundle size reduction, and runtime performance improvements to ensure your applications run smoothly and efficiently.

Rendering Performance Optimization

1. Virtual Scrolling Implementation

For large datasets, virtual scrolling is essential to maintain smooth performance.

typescript
// Virtual List Component with Performance Optimizations
import { defineComponent, ref, computed, onMounted, onUpdated, nextTick } from 'vue'
import { useResizeObserver, useThrottleFn } from '@vueuse/core'

interface VirtualListProps {
  items: any[]
  itemHeight: number | ((index: number) => number)
  height: number
  buffer?: number
  horizontal?: boolean
  keepAlive?: boolean
}

export const VirtualList = defineComponent({
  name: 'VirtualList',
  props: {
    items: {
      type: Array,
      required: true
    },
    itemHeight: {
      type: [Number, Function],
      required: true
    },
    height: {
      type: Number,
      required: true
    },
    buffer: {
      type: Number,
      default: 5
    },
    horizontal: {
      type: Boolean,
      default: false
    },
    keepAlive: {
      type: Boolean,
      default: false
    }
  },
  setup(props, { slots }) {
    const containerRef = ref<HTMLElement>()
    const scrollOffset = ref(0)
    const clientSize = ref(0)
    
    // Cache for dynamic heights
    const heightCache = new Map<number, number>()
    const positionCache = new Map<number, number>()
    
    // Get item height (supports dynamic heights)
    const getItemHeight = (index: number): number => {
      if (typeof props.itemHeight === 'function') {
        if (!heightCache.has(index)) {
          heightCache.set(index, props.itemHeight(index))
        }
        return heightCache.get(index)!
      }
      return props.itemHeight as number
    }
    
    // Get item position
    const getItemPosition = (index: number): number => {
      if (positionCache.has(index)) {
        return positionCache.get(index)!
      }
      
      let position = 0
      for (let i = 0; i < index; i++) {
        position += getItemHeight(i)
      }
      
      positionCache.set(index, position)
      return position
    }
    
    // Calculate visible range
    const visibleRange = computed(() => {
      const containerSize = props.horizontal ? clientSize.value : props.height
      const offset = scrollOffset.value
      
      let startIndex = 0
      let endIndex = props.items.length - 1
      
      // Binary search for start index
      let low = 0
      let high = props.items.length - 1
      
      while (low <= high) {
        const mid = Math.floor((low + high) / 2)
        const position = getItemPosition(mid)
        
        if (position < offset) {
          low = mid + 1
        } else {
          high = mid - 1
          startIndex = mid
        }
      }
      
      // Find end index
      let currentPosition = getItemPosition(startIndex)
      endIndex = startIndex
      
      while (endIndex < props.items.length && currentPosition < offset + containerSize) {
        currentPosition += getItemHeight(endIndex)
        endIndex++
      }
      
      // Apply buffer
      startIndex = Math.max(0, startIndex - props.buffer)
      endIndex = Math.min(props.items.length - 1, endIndex + props.buffer)
      
      return { startIndex, endIndex }
    })
    
    // Visible items
    const visibleItems = computed(() => {
      const { startIndex, endIndex } = visibleRange.value
      const items = []
      
      for (let i = startIndex; i <= endIndex; i++) {
        items.push({
          index: i,
          item: props.items[i],
          position: getItemPosition(i),
          height: getItemHeight(i)
        })
      }
      
      return items
    })
    
    // Total size
    const totalSize = computed(() => {
      return getItemPosition(props.items.length)
    })
    
    // Throttled scroll handler
    const handleScroll = useThrottleFn((event: Event) => {
      const target = event.target as HTMLElement
      scrollOffset.value = props.horizontal ? target.scrollLeft : target.scrollTop
    }, 16) // ~60fps
    
    // Resize observer
    useResizeObserver(containerRef, (entries) => {
      const entry = entries[0]
      if (entry) {
        clientSize.value = props.horizontal 
          ? entry.contentRect.width 
          : entry.contentRect.height
      }
    })
    
    // Scroll to item
    const scrollToItem = (index: number, align: 'start' | 'center' | 'end' = 'start') => {
      if (!containerRef.value) return
      
      const position = getItemPosition(index)
      const itemHeight = getItemHeight(index)
      const containerSize = props.horizontal ? clientSize.value : props.height
      
      let scrollTo = position
      
      if (align === 'center') {
        scrollTo = position - (containerSize - itemHeight) / 2
      } else if (align === 'end') {
        scrollTo = position - containerSize + itemHeight
      }
      
      scrollTo = Math.max(0, Math.min(scrollTo, totalSize.value - containerSize))
      
      if (props.horizontal) {
        containerRef.value.scrollLeft = scrollTo
      } else {
        containerRef.value.scrollTop = scrollTo
      }
    }
    
    // Update item height (for dynamic heights)
    const updateItemHeight = (index: number, height: number) => {
      if (heightCache.get(index) !== height) {
        heightCache.set(index, height)
        
        // Clear position cache for items after this one
        for (let i = index + 1; i < props.items.length; i++) {
          positionCache.delete(i)
        }
      }
    }
    
    return {
      containerRef,
      visibleItems,
      totalSize,
      handleScroll,
      scrollToItem,
      updateItemHeight
    }
  },
  render() {
    const containerStyle = {
      height: `${this.height}px`,
      overflow: 'auto'
    }
    
    const contentStyle = this.horizontal ? {
      width: `${this.totalSize}px`,
      height: '100%',
      display: 'flex'
    } : {
      height: `${this.totalSize}px`,
      position: 'relative'
    }
    
    return (
      <div
        ref="containerRef"
        class="virtual-list"
        style={containerStyle}
        onScroll={this.handleScroll}
      >
        <div class="virtual-list__content" style={contentStyle}>
          {this.visibleItems.map(({ index, item, position, height }) => {
            const itemStyle = this.horizontal ? {
              width: `${height}px`,
              height: '100%'
            } : {
              position: 'absolute',
              top: `${position}px`,
              height: `${height}px`,
              width: '100%'
            }
            
            return (
              <div
                key={index}
                class="virtual-list__item"
                style={itemStyle}
              >
                {this.$slots.default?.({ item, index })}
              </div>
            )
          })}
        </div>
      </div>
    )
  }
})

2. Optimized Table Component

typescript
// High-performance table with virtual scrolling and column virtualization
import { defineComponent, ref, computed, shallowRef, triggerRef } from 'vue'
import { useVirtualList } from './useVirtualList'

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

interface TableProps {
  data: any[]
  columns: TableColumn[]
  height?: number
  rowHeight?: number
  showHeader?: boolean
  sortBy?: string
  sortOrder?: 'asc' | 'desc'
  loading?: boolean
}

export const PerformantTable = defineComponent({
  name: 'PerformantTable',
  props: {
    data: {
      type: Array,
      required: true
    },
    columns: {
      type: Array,
      required: true
    },
    height: {
      type: Number,
      default: 400
    },
    rowHeight: {
      type: Number,
      default: 40
    },
    showHeader: {
      type: Boolean,
      default: true
    },
    sortBy: String,
    sortOrder: {
      type: String,
      default: 'asc'
    },
    loading: Boolean
  },
  emits: ['sort-change', 'row-click'],
  setup(props, { emit }) {
    const tableRef = ref<HTMLElement>()
    const headerRef = ref<HTMLElement>()
    
    // Use shallow ref for large datasets
    const sortedData = shallowRef(props.data)
    
    // Column calculations
    const columnWidths = computed(() => {
      const totalWidth = tableRef.value?.clientWidth || 800
      const fixedWidth = props.columns
        .filter(col => col.width)
        .reduce((sum, col) => sum + (col.width || 0), 0)
      
      const flexColumns = props.columns.filter(col => !col.width)
      const flexWidth = (totalWidth - fixedWidth) / flexColumns.length
      
      return props.columns.map(col => ({
        ...col,
        computedWidth: col.width || Math.max(col.minWidth || 100, flexWidth)
      }))
    })
    
    // Virtual scrolling for rows
    const {
      containerProps,
      wrapperProps,
      list: visibleRows
    } = useVirtualList(
      computed(() => sortedData.value),
      {
        itemHeight: props.rowHeight,
        overscan: 5
      }
    )
    
    // Sorting
    const handleSort = (column: TableColumn) => {
      if (!column.sortable) return
      
      let newOrder: 'asc' | 'desc' = 'asc'
      if (props.sortBy === column.key) {
        newOrder = props.sortOrder === 'asc' ? 'desc' : 'asc'
      }
      
      // Perform sorting
      const sorted = [...props.data].sort((a, b) => {
        const aVal = a[column.key]
        const bVal = b[column.key]
        
        if (aVal === bVal) return 0
        
        const result = aVal > bVal ? 1 : -1
        return newOrder === 'asc' ? result : -result
      })
      
      sortedData.value = sorted
      triggerRef(sortedData)
      
      emit('sort-change', {
        column: column.key,
        order: newOrder
      })
    }
    
    // Row click handler
    const handleRowClick = (record: any, index: number) => {
      emit('row-click', record, index)
    }
    
    // Render cell content
    const renderCell = (column: TableColumn, record: any, index: number) => {
      if (column.render) {
        return column.render(record[column.key], record, index)
      }
      return record[column.key]
    }
    
    return {
      tableRef,
      headerRef,
      columnWidths,
      visibleRows,
      containerProps,
      wrapperProps,
      handleSort,
      handleRowClick,
      renderCell
    }
  },
  render() {
    return (
      <div ref="tableRef" class="performant-table" v-loading={this.loading}>
        {/* Header */}
        {this.showHeader && (
          <div ref="headerRef" class="table-header">
            <div class="table-row">
              {this.columnWidths.map(column => (
                <div
                  key={column.key}
                  class={[
                    'table-cell',
                    'table-header-cell',
                    {
                      'sortable': column.sortable,
                      'sorted': this.sortBy === column.key
                    }
                  ]}
                  style={{ width: `${column.computedWidth}px` }}
                  onClick={() => this.handleSort(column)}
                >
                  {column.title}
                  {column.sortable && (
                    <span class="sort-indicator">
                      {this.sortBy === column.key && this.sortOrder === 'asc' ? '↑' : '↓'}
                    </span>
                  )}
                </div>
              ))}
            </div>
          </div>
        )}
        
        {/* Body with virtual scrolling */}
        <div
          class="table-body"
          style={{ height: `${this.height - (this.showHeader ? 40 : 0)}px` }}
          {...this.containerProps}
        >
          <div {...this.wrapperProps}>
            {this.visibleRows.map(({ data: record, index }) => (
              <div
                key={index}
                class="table-row"
                style={{ height: `${this.rowHeight}px` }}
                onClick={() => this.handleRowClick(record, index)}
              >
                {this.columnWidths.map(column => (
                  <div
                    key={column.key}
                    class="table-cell"
                    style={{ width: `${column.computedWidth}px` }}
                  >
                    {this.renderCell(column, record, index)}
                  </div>
                ))}
              </div>
            ))}
          </div>
        </div>
      </div>
    )
  }
})

Memory Management

1. Component Cleanup Strategies

typescript
// Comprehensive cleanup composable
import { onBeforeUnmount, onUnmounted, ref } from 'vue'

export function useCleanup() {
  const timers = ref(new Set<NodeJS.Timeout>())
  const intervals = ref(new Set<NodeJS.Timeout>())
  const observers = ref(new Set<any>())
  const eventListeners = ref(new Map<EventTarget, Map<string, EventListener>>())
  const abortControllers = ref(new Set<AbortController>())
  
  // Timer management
  const setTimeout = (callback: () => void, delay: number) => {
    const timer = globalThis.setTimeout(callback, delay)
    timers.value.add(timer)
    return timer
  }
  
  const setInterval = (callback: () => void, delay: number) => {
    const interval = globalThis.setInterval(callback, delay)
    intervals.value.add(interval)
    return interval
  }
  
  const clearTimeout = (timer: NodeJS.Timeout) => {
    globalThis.clearTimeout(timer)
    timers.value.delete(timer)
  }
  
  const clearInterval = (interval: NodeJS.Timeout) => {
    globalThis.clearInterval(interval)
    intervals.value.delete(interval)
  }
  
  // Observer management
  const addObserver = (observer: any) => {
    observers.value.add(observer)
    return observer
  }
  
  // Event listener management
  const addEventListener = (
    target: EventTarget,
    type: string,
    listener: EventListener,
    options?: AddEventListenerOptions
  ) => {
    target.addEventListener(type, listener, options)
    
    if (!eventListeners.value.has(target)) {
      eventListeners.value.set(target, new Map())
    }
    eventListeners.value.get(target)!.set(type, listener)
  }
  
  // Abort controller management
  const createAbortController = () => {
    const controller = new AbortController()
    abortControllers.value.add(controller)
    return controller
  }
  
  // Cleanup function
  const cleanup = () => {
    // Clear timers
    timers.value.forEach(timer => globalThis.clearTimeout(timer))
    timers.value.clear()
    
    // Clear intervals
    intervals.value.forEach(interval => globalThis.clearInterval(interval))
    intervals.value.clear()
    
    // Disconnect observers
    observers.value.forEach(observer => {
      if (observer.disconnect) observer.disconnect()
      if (observer.unobserve) observer.unobserve()
    })
    observers.value.clear()
    
    // Remove event listeners
    eventListeners.value.forEach((listeners, target) => {
      listeners.forEach((listener, type) => {
        target.removeEventListener(type, listener)
      })
    })
    eventListeners.value.clear()
    
    // Abort ongoing requests
    abortControllers.value.forEach(controller => {
      if (!controller.signal.aborted) {
        controller.abort()
      }
    })
    abortControllers.value.clear()
  }
  
  // Auto cleanup on unmount
  onBeforeUnmount(cleanup)
  
  return {
    setTimeout,
    setInterval,
    clearTimeout,
    clearInterval,
    addObserver,
    addEventListener,
    createAbortController,
    cleanup
  }
}

// Memory-efficient data management
export function useMemoryEfficientData<T>(initialData: T[] = []) {
  const data = shallowRef<T[]>(initialData)
  const cache = new Map<string, T[]>()
  const maxCacheSize = 10
  
  // LRU cache implementation
  const getCachedData = (key: string): T[] | undefined => {
    if (cache.has(key)) {
      const value = cache.get(key)!
      // Move to end (most recently used)
      cache.delete(key)
      cache.set(key, value)
      return value
    }
    return undefined
  }
  
  const setCachedData = (key: string, value: T[]) => {
    if (cache.size >= maxCacheSize) {
      // Remove least recently used
      const firstKey = cache.keys().next().value
      cache.delete(firstKey)
    }
    cache.set(key, value)
  }
  
  // Batch updates for better performance
  const batchUpdate = (updates: (() => void)[]) => {
    updates.forEach(update => update())
    triggerRef(data)
  }
  
  // Efficient filtering without creating new arrays
  const createFilteredView = (predicate: (item: T) => boolean) => {
    return computed(() => {
      const result: T[] = []
      for (const item of data.value) {
        if (predicate(item)) {
          result.push(item)
        }
      }
      return result
    })
  }
  
  return {
    data: readonly(data),
    getCachedData,
    setCachedData,
    batchUpdate,
    createFilteredView
  }
}

2. Lazy Loading and Code Splitting

typescript
// Advanced lazy loading with preloading
import { defineAsyncComponent, ref, onMounted } from 'vue'

interface LazyComponentOptions {
  loader: () => Promise<any>
  loadingComponent?: any
  errorComponent?: any
  delay?: number
  timeout?: number
  preload?: boolean
  retries?: number
}

export function createLazyComponent(options: LazyComponentOptions) {
  const {
    loader,
    loadingComponent,
    errorComponent,
    delay = 200,
    timeout = 3000,
    preload = false,
    retries = 3
  } = options
  
  let componentPromise: Promise<any> | null = null
  let retryCount = 0
  
  const loadComponent = async (): Promise<any> => {
    if (componentPromise) {
      return componentPromise
    }
    
    componentPromise = loader().catch(async (error) => {
      if (retryCount < retries) {
        retryCount++
        componentPromise = null
        // Exponential backoff
        await new Promise(resolve => setTimeout(resolve, Math.pow(2, retryCount) * 1000))
        return loadComponent()
      }
      throw error
    })
    
    return componentPromise
  }
  
  // Preload if requested
  if (preload) {
    loadComponent()
  }
  
  return defineAsyncComponent({
    loader: loadComponent,
    loadingComponent,
    errorComponent,
    delay,
    timeout
  })
}

// Route-based code splitting
export const createRouteComponent = (importFn: () => Promise<any>) => {
  return createLazyComponent({
    loader: importFn,
    loadingComponent: () => h('div', { class: 'route-loading' }, 'Loading...'),
    errorComponent: () => h('div', { class: 'route-error' }, 'Failed to load page'),
    preload: true
  })
}

// Component preloading strategy
export function useComponentPreloader() {
  const preloadedComponents = new Map<string, Promise<any>>()
  
  const preloadComponent = (name: string, loader: () => Promise<any>) => {
    if (!preloadedComponents.has(name)) {
      preloadedComponents.set(name, loader())
    }
    return preloadedComponents.get(name)!
  }
  
  const getPreloadedComponent = (name: string) => {
    return preloadedComponents.get(name)
  }
  
  // Preload on idle
  const preloadOnIdle = (components: Record<string, () => Promise<any>>) => {
    if ('requestIdleCallback' in window) {
      requestIdleCallback(() => {
        Object.entries(components).forEach(([name, loader]) => {
          preloadComponent(name, loader)
        })
      })
    } else {
      setTimeout(() => {
        Object.entries(components).forEach(([name, loader]) => {
          preloadComponent(name, loader)
        })
      }, 1000)
    }
  }
  
  return {
    preloadComponent,
    getPreloadedComponent,
    preloadOnIdle
  }
}

Bundle Size Optimization

1. Tree-shaking Configuration

typescript
// Optimized Element Plus imports
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
  plugins: [
    vue(),
    AutoImport({
      resolvers: [ElementPlusResolver()],
      imports: [
        'vue',
        'vue-router',
        'pinia'
      ],
      dts: true
    }),
    Components({
      resolvers: [
        ElementPlusResolver({
          importStyle: 'sass', // or 'css'
          directives: true,
          version: '2.3.0'
        })
      ],
      dts: true
    })
  ],
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'element-plus': ['element-plus'],
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'utils': ['lodash-es', 'dayjs']
        }
      }
    },
    // Enable tree-shaking
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true,
        pure_funcs: ['console.log']
      }
    }
  },
  optimizeDeps: {
    include: [
      'element-plus/es/components/button/style/css',
      'element-plus/es/components/input/style/css'
      // Add other frequently used components
    ]
  }
})

// Manual imports for maximum tree-shaking
// utils/element-plus.ts
export { ElButton } from 'element-plus/es/components/button'
export { ElInput } from 'element-plus/es/components/input'
export { ElForm, ElFormItem } from 'element-plus/es/components/form'
export { ElTable, ElTableColumn } from 'element-plus/es/components/table'

// Import only needed styles
import 'element-plus/es/components/button/style/css'
import 'element-plus/es/components/input/style/css'
import 'element-plus/es/components/form/style/css'
import 'element-plus/es/components/table/style/css'

// Custom build script for minimal bundle
// scripts/build-minimal.js
import { build } from 'vite'
import { resolve } from 'path'

const components = [
  'button',
  'input',
  'form',
  'table'
  // Add only components you actually use
]

const buildMinimal = async () => {
  await build({
    build: {
      lib: {
        entry: resolve(__dirname, '../src/element-plus-minimal.ts'),
        name: 'ElementPlusMinimal',
        fileName: 'element-plus-minimal'
      },
      rollupOptions: {
        external: ['vue'],
        output: {
          globals: {
            vue: 'Vue'
          }
        }
      }
    }
  })
}

buildMinimal()

2. Dynamic Imports and Route Splitting

typescript
// Route-based code splitting
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('../views/Home.vue')
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('../views/Dashboard.vue'),
    children: [
      {
        path: 'analytics',
        component: () => import('../views/dashboard/Analytics.vue')
      },
      {
        path: 'reports',
        component: () => import('../views/dashboard/Reports.vue')
      }
    ]
  },
  {
    path: '/admin',
    name: 'Admin',
    component: () => import('../views/Admin.vue'),
    meta: { requiresAuth: true }
  }
]

// Component-level dynamic imports
// components/LazyComponents.ts
export const LazyDataTable = defineAsyncComponent({
  loader: () => import('./DataTable.vue'),
  loadingComponent: () => h('div', 'Loading table...'),
  delay: 200
})

export const LazyChart = defineAsyncComponent({
  loader: () => import('./Chart.vue'),
  loadingComponent: () => h('div', 'Loading chart...'),
  delay: 200
})

// Feature-based splitting
export const AdminFeatures = {
  UserManagement: () => import('../features/admin/UserManagement.vue'),
  SystemSettings: () => import('../features/admin/SystemSettings.vue'),
  AuditLogs: () => import('../features/admin/AuditLogs.vue')
}

Runtime Performance

1. Event Handling Optimization

typescript
// Optimized event handling
import { ref, onMounted, onBeforeUnmount } from 'vue'

// Event delegation for better performance
export function useEventDelegation(containerRef: Ref<HTMLElement | undefined>) {
  const eventHandlers = new Map<string, Map<string, (event: Event) => void>>()
  
  const addHandler = (selector: string, eventType: string, handler: (event: Event) => void) => {
    if (!eventHandlers.has(eventType)) {
      eventHandlers.set(eventType, new Map())
    }
    eventHandlers.get(eventType)!.set(selector, handler)
  }
  
  const handleEvent = (event: Event) => {
    const handlers = eventHandlers.get(event.type)
    if (!handlers) return
    
    for (const [selector, handler] of handlers) {
      const target = (event.target as Element).closest(selector)
      if (target && containerRef.value?.contains(target)) {
        handler(event)
        break
      }
    }
  }
  
  onMounted(() => {
    if (containerRef.value) {
      eventHandlers.forEach((_, eventType) => {
        containerRef.value!.addEventListener(eventType, handleEvent)
      })
    }
  })
  
  onBeforeUnmount(() => {
    if (containerRef.value) {
      eventHandlers.forEach((_, eventType) => {
        containerRef.value!.removeEventListener(eventType, handleEvent)
      })
    }
  })
  
  return { addHandler }
}

// Throttled and debounced event handlers
export function useOptimizedEvents() {
  const throttle = <T extends (...args: any[]) => any>(
    func: T,
    limit: number
  ): T => {
    let inThrottle: boolean
    return ((...args: any[]) => {
      if (!inThrottle) {
        func.apply(this, args)
        inThrottle = true
        setTimeout(() => inThrottle = false, limit)
      }
    }) as T
  }
  
  const debounce = <T extends (...args: any[]) => any>(
    func: T,
    delay: number
  ): T => {
    let timeoutId: NodeJS.Timeout
    return ((...args: any[]) => {
      clearTimeout(timeoutId)
      timeoutId = setTimeout(() => func.apply(this, args), delay)
    }) as T
  }
  
  // Optimized scroll handler
  const createScrollHandler = (callback: (event: Event) => void, options: {
    throttle?: number
    passive?: boolean
  } = {}) => {
    const { throttle: throttleMs = 16, passive = true } = options
    
    const handler = throttleMs > 0 ? throttle(callback, throttleMs) : callback
    
    return {
      handler,
      options: { passive }
    }
  }
  
  return {
    throttle,
    debounce,
    createScrollHandler
  }
}

2. Computed Property Optimization

typescript
// Optimized computed properties
import { computed, ref, shallowRef, triggerRef } from 'vue'

// Memoized computed with custom equality
export function useMemoizedComputed<T>(
  getter: () => T,
  deps: any[],
  isEqual: (a: T, b: T) => boolean = Object.is
) {
  const cache = ref<{ value: T; deps: any[] }>()
  
  return computed(() => {
    const currentDeps = deps.map(dep => unref(dep))
    
    if (cache.value && 
        cache.value.deps.length === currentDeps.length &&
        cache.value.deps.every((dep, index) => Object.is(dep, currentDeps[index]))) {
      return cache.value.value
    }
    
    const newValue = getter()
    
    if (!cache.value || !isEqual(cache.value.value, newValue)) {
      cache.value = {
        value: newValue,
        deps: currentDeps
      }
    }
    
    return cache.value.value
  })
}

// Computed with async data
export function useAsyncComputed<T>(
  getter: () => Promise<T>,
  defaultValue: T,
  deps: any[] = []
) {
  const data = ref<T>(defaultValue)
  const loading = ref(false)
  const error = ref<Error | null>(null)
  
  const execute = async () => {
    loading.value = true
    error.value = null
    
    try {
      const result = await getter()
      data.value = result
    } catch (err) {
      error.value = err as Error
    } finally {
      loading.value = false
    }
  }
  
  // Watch dependencies
  watchEffect(() => {
    // Access deps to create reactivity
    deps.forEach(dep => unref(dep))
    execute()
  })
  
  return {
    data: readonly(data),
    loading: readonly(loading),
    error: readonly(error),
    refresh: execute
  }
}

// Batch computed updates
export function useBatchedComputed() {
  const computedRefs = new Set<ComputedRef<any>>()
  const pendingUpdate = ref(false)
  
  const addComputed = (computedRef: ComputedRef<any>) => {
    computedRefs.add(computedRef)
  }
  
  const batchUpdate = (callback: () => void) => {
    if (pendingUpdate.value) return
    
    pendingUpdate.value = true
    
    nextTick(() => {
      callback()
      
      // Trigger all computed refs
      computedRefs.forEach(ref => {
        ref.value // Access to trigger reactivity
      })
      
      pendingUpdate.value = false
    })
  }
  
  return {
    addComputed,
    batchUpdate
  }
}

Performance Monitoring

1. Performance Metrics Collection

typescript
// Performance monitoring composable
import { ref, onMounted, onBeforeUnmount } from 'vue'

interface PerformanceMetrics {
  renderTime: number
  updateTime: number
  memoryUsage: number
  componentCount: number
  eventListenerCount: number
}

export function usePerformanceMonitor(componentName: string) {
  const metrics = ref<PerformanceMetrics>({
    renderTime: 0,
    updateTime: 0,
    memoryUsage: 0,
    componentCount: 0,
    eventListenerCount: 0
  })
  
  const startTime = ref(0)
  const observer = ref<PerformanceObserver>()
  
  // Measure render time
  const startRenderMeasure = () => {
    startTime.value = performance.now()
  }
  
  const endRenderMeasure = () => {
    metrics.value.renderTime = performance.now() - startTime.value
  }
  
  // Memory usage monitoring
  const measureMemoryUsage = () => {
    if ('memory' in performance) {
      const memory = (performance as any).memory
      metrics.value.memoryUsage = memory.usedJSHeapSize
    }
  }
  
  // Performance observer for detailed metrics
  const setupPerformanceObserver = () => {
    if ('PerformanceObserver' in window) {
      observer.value = new PerformanceObserver((list) => {
        const entries = list.getEntries()
        entries.forEach(entry => {
          if (entry.name.includes(componentName)) {
            console.log(`${componentName} performance:`, entry)
          }
        })
      })
      
      observer.value.observe({ entryTypes: ['measure', 'navigation'] })
    }
  }
  
  // Component lifecycle tracking
  onMounted(() => {
    startRenderMeasure()
    setupPerformanceObserver()
    
    nextTick(() => {
      endRenderMeasure()
      measureMemoryUsage()
    })
  })
  
  onBeforeUnmount(() => {
    observer.value?.disconnect()
  })
  
  // Report metrics
  const reportMetrics = () => {
    console.group(`Performance Metrics - ${componentName}`)
    console.log('Render Time:', metrics.value.renderTime, 'ms')
    console.log('Memory Usage:', (metrics.value.memoryUsage / 1024 / 1024).toFixed(2), 'MB')
    console.groupEnd()
  }
  
  return {
    metrics: readonly(metrics),
    startRenderMeasure,
    endRenderMeasure,
    measureMemoryUsage,
    reportMetrics
  }
}

// Bundle size analyzer
export function analyzeBundleSize() {
  const bundleInfo = {
    totalSize: 0,
    gzippedSize: 0,
    chunks: [] as Array<{ name: string; size: number }>
  }
  
  // This would typically be used in build process
  const analyzeDependencies = () => {
    // Implementation would depend on build tool
    console.log('Bundle analysis:', bundleInfo)
  }
  
  return {
    bundleInfo,
    analyzeDependencies
  }
}

Best Practices

1. Performance Guidelines

  • Virtual Scrolling: Use for lists with >100 items
  • Lazy Loading: Implement for routes and heavy components
  • Memory Management: Always clean up timers, observers, and event listeners
  • Bundle Optimization: Use tree-shaking and code splitting
  • Event Handling: Use delegation and throttling for frequent events

2. Monitoring and Debugging

  • Performance Metrics: Monitor render times and memory usage
  • Bundle Analysis: Regularly analyze bundle size and dependencies
  • Profiling: Use Vue DevTools and browser profiling tools
  • Error Tracking: Implement comprehensive error monitoring

3. Optimization Checklist

  • [ ] Implement virtual scrolling for large lists
  • [ ] Use lazy loading for routes and components
  • [ ] Optimize bundle size with tree-shaking
  • [ ] Implement proper cleanup in components
  • [ ] Use throttling/debouncing for frequent events
  • [ ] Monitor performance metrics
  • [ ] Optimize computed properties and watchers
  • [ ] Use shallow reactivity for large datasets

Conclusion

Effective performance optimization in Element Plus applications requires:

  • Rendering Optimization: Virtual scrolling and efficient updates
  • Memory Management: Proper cleanup and efficient data structures
  • Bundle Optimization: Tree-shaking and code splitting
  • Runtime Performance: Optimized event handling and computed properties
  • Monitoring: Continuous performance tracking and analysis

By implementing these strategies, you can ensure your Element Plus applications remain fast and responsive even as they scale.

Element Plus Study Guide