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.