How to profile Vue performance

Profiling Vue application performance identifies rendering bottlenecks, slow components, and unnecessary re-renders for targeted optimization. As the creator of CoreUI with over 12 years of Vue.js experience since 2014, I’ve profiled and optimized numerous production Vue applications. Vue DevTools combined with browser Performance API provides detailed insights into component render times, lifecycle hooks, and reactive updates. This approach reveals performance issues enabling data-driven optimization decisions for faster user experiences.

Use Vue DevTools Performance tab, browser Performance API, and custom timing to profile component rendering and identify bottlenecks.

Vue DevTools performance profiling:

<script setup>
// Enable performance tracking in main.js
// main.js
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

if (import.meta.env.MODE === 'development') {
  app.config.performance = true // Enable Vue DevTools performance tracking
}

app.mount('#app')
</script>

Custom performance composable:

// usePerformance.js
import { onMounted, onUpdated, onUnmounted } from 'vue'

export function usePerformance(componentName) {
  const marks = {
    mountStart: `${componentName}-mount-start`,
    mountEnd: `${componentName}-mount-end`,
    updateStart: `${componentName}-update-start`,
    updateEnd: `${componentName}-update-end`
  }

  // Track mount time
  performance.mark(marks.mountStart)

  onMounted(() => {
    performance.mark(marks.mountEnd)
    performance.measure(
      `${componentName} mount`,
      marks.mountStart,
      marks.mountEnd
    )

    const measure = performance.getEntriesByName(`${componentName} mount`)[0]
    console.log(`${componentName} mounted in ${measure.duration.toFixed(2)}ms`)

    if (measure.duration > 100) {
      console.warn(`${componentName} mount time exceeds 100ms`)
    }
  })

  // Track updates
  let updateCount = 0
  onUpdated(() => {
    updateCount++
    performance.mark(`${marks.updateEnd}-${updateCount}`)

    if (updateCount > 1) {
      performance.measure(
        `${componentName} update ${updateCount}`,
        `${marks.updateEnd}-${updateCount - 1}`,
        `${marks.updateEnd}-${updateCount}`
      )

      const measure = performance.getEntriesByName(
        `${componentName} update ${updateCount}`
      )[0]
      console.log(`${componentName} update #${updateCount}: ${measure.duration.toFixed(2)}ms`)
    }
  })

  onUnmounted(() => {
    console.log(`${componentName} total updates: ${updateCount}`)
    performance.clearMarks()
    performance.clearMeasures()
  })
}

// Usage
import { usePerformance } from './usePerformance'

usePerformance('UserList')

Component render profiling:

<script setup>
import { ref, computed, watch } from 'vue'
import { usePerformance } from './usePerformance'

const items = ref([])
const filter = ref('')

usePerformance('FilteredList')

// Profile computed properties
const filteredItems = computed(() => {
  const start = performance.now()
  const result = items.value.filter(item =>
    item.name.toLowerCase().includes(filter.value.toLowerCase())
  )
  const duration = performance.now() - start

  if (duration > 10) {
    console.warn(`filteredItems computed took ${duration.toFixed(2)}ms`)
  }

  return result
})

// Profile watchers
watch(filter, (newValue) => {
  console.log('Filter changed, will trigger re-render')
}, { flush: 'post' }) // Run after component updates
</script>

<template>
  <div>
    <input v-model="filter" placeholder="Search..." />
    <div v-for="item in filteredItems" :key="item.id">
      {{ item.name }}
    </div>
  </div>
</template>

Render count tracking:

<script setup>
import { ref, watchEffect } from 'vue'

const renderCount = ref(0)
const data = ref({ count: 0 })

watchEffect(() => {
  renderCount.value++
  console.log(`Component rendered ${renderCount.value} times`)
  console.log('Triggered by:', data.value)
})

// Check for excessive renders
if (import.meta.env.MODE === 'development') {
  watchEffect(() => {
    if (renderCount.value > 100) {
      console.error('Component has rendered 100+ times - possible performance issue')
    }
  })
}
</script>

<template>
  <div>
    <p>Render count: {{ renderCount }}</p>
    <button @click="data.count++">Increment</button>
  </div>
</template>

Profiling large lists:

<script setup>
import { ref, onMounted } from 'vue'

const items = ref([])

onMounted(async () => {
  // Profile data loading
  performance.mark('data-load-start')

  const response = await fetch('/api/items')
  const data = await response.json()

  performance.mark('data-load-end')
  performance.measure('data-load', 'data-load-start', 'data-load-end')

  // Profile array processing
  performance.mark('process-start')
  items.value = data.map(item => ({
    ...item,
    processed: true
  }))
  performance.mark('process-end')
  performance.measure('process', 'process-start', 'process-end')

  // Profile initial render
  await new Promise(resolve => setTimeout(resolve, 0))
  performance.mark('render-complete')
  performance.measure('initial-render', 'data-load-start', 'render-complete')

  // Log results
  const measures = performance.getEntriesByType('measure')
  measures.forEach(measure => {
    console.log(`${measure.name}: ${measure.duration.toFixed(2)}ms`)
  })
})
</script>

<template>
  <div>
    <div v-for="item in items" :key="item.id">
      {{ item.name }}
    </div>
  </div>
</template>

Memory profiling:

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const data = ref([])
let initialMemory = 0

onMounted(() => {
  if (performance.memory) {
    initialMemory = performance.memory.usedJSHeapSize
    console.log('Initial memory:', (initialMemory / 1048576).toFixed(2), 'MB')
  }

  // Load data
  data.value = Array.from({ length: 10000 }, (_, i) => ({
    id: i,
    name: `Item ${i}`,
    data: new Array(100).fill('data')
  }))

  if (performance.memory) {
    const currentMemory = performance.memory.usedJSHeapSize
    const memoryIncrease = currentMemory - initialMemory
    console.log('Memory increase:', (memoryIncrease / 1048576).toFixed(2), 'MB')
  }
})

onUnmounted(() => {
  if (performance.memory) {
    const finalMemory = performance.memory.usedJSHeapSize
    console.log('Final memory:', (finalMemory / 1048576).toFixed(2), 'MB')
    console.log('Memory cleanup:', ((initialMemory - finalMemory) / 1048576).toFixed(2), 'MB')
  }
})
</script>

FPS monitoring:

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const fps = ref(0)
let frameCount = 0
let lastTime = performance.now()
let animationId

function measureFPS() {
  frameCount++
  const currentTime = performance.now()
  const elapsed = currentTime - lastTime

  if (elapsed >= 1000) {
    fps.value = Math.round((frameCount * 1000) / elapsed)

    if (fps.value < 30) {
      console.warn(`Low FPS detected: ${fps.value}`)
    }

    frameCount = 0
    lastTime = currentTime
  }

  animationId = requestAnimationFrame(measureFPS)
}

onMounted(() => {
  measureFPS()
})

onUnmounted(() => {
  cancelAnimationFrame(animationId)
})
</script>

<template>
  <div class="fps-counter">
    FPS: {{ fps }}
  </div>
</template>

Performance observer:

// performanceObserver.js
export function setupPerformanceObserver() {
  if (!window.PerformanceObserver) return

  const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      if (entry.entryType === 'measure') {
        console.log(`Performance: ${entry.name} - ${entry.duration.toFixed(2)}ms`)
      }

      if (entry.entryType === 'longtask') {
        console.warn(`Long task detected: ${entry.duration.toFixed(2)}ms`)
      }
    }
  })

  observer.observe({ entryTypes: ['measure', 'longtask'] })

  return observer
}

// main.js
import { setupPerformanceObserver } from './performanceObserver'

if (import.meta.env.MODE === 'development') {
  setupPerformanceObserver()
}

Custom profiler component:

<!-- Profiler.vue -->
<script setup>
import { ref, onMounted, onUpdated, onUnmounted } from 'vue'

const props = defineProps({
  id: String,
  onRender: Function
})

const mountTime = ref(0)
const renderCount = ref(0)
const lastRenderTime = ref(0)

onMounted(() => {
  mountTime.value = performance.now()
  logProfile('mount')
})

onUpdated(() => {
  renderCount.value++
  lastRenderTime.value = performance.now()
  logProfile('update')
})

onUnmounted(() => {
  logProfile('unmount')
})

function logProfile(phase) {
  const profile = {
    id: props.id,
    phase,
    timestamp: performance.now(),
    mountTime: mountTime.value,
    renderCount: renderCount.value,
    lastRenderTime: lastRenderTime.value
  }

  console.log('Profile:', profile)
  props.onRender?.(profile)
}
</script>

<template>
  <slot />
</template>

<!-- Usage -->
<Profiler id="UserList" :onRender="handleRender">
  <UserList />
</Profiler>

Production monitoring:

// monitoring.js
export function setupProductionMonitoring() {
  if (import.meta.env.MODE !== 'production') return

  // Track route changes
  let routeChangeStart = 0
  window.addEventListener('beforeunload', () => {
    routeChangeStart = performance.now()
  })

  // Track long tasks
  if (PerformanceObserver.supportedEntryTypes?.includes('longtask')) {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        // Send to analytics
        console.log('Long task:', entry.duration)
      }
    })
    observer.observe({ entryTypes: ['longtask'] })
  }

  // Track Core Web Vitals
  if (PerformanceObserver.supportedEntryTypes?.includes('largest-contentful-paint')) {
    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries()
      const lastEntry = entries[entries.length - 1]
      console.log('LCP:', lastEntry.renderTime || lastEntry.loadTime)
    })
    observer.observe({ entryTypes: ['largest-contentful-paint'] })
  }
}

Best Practice Note

Enable app.config.performance in development for Vue DevTools profiling. Use Performance API to measure mount and update times. Track render counts to identify unnecessary updates. Profile computed properties and watchers for slow operations. Monitor FPS for smooth animations. Use PerformanceObserver for long tasks detection. Measure memory usage for large datasets. Set performance budgets and warn when exceeded. This is how we profile CoreUI Vue applications—DevTools performance tracking, custom timing hooks, and production monitoring identifying bottlenecks and ensuring components render under 16ms for 60fps smooth experiences.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.
How to Center a Button in CSS
How to Center a Button in CSS

How to validate an email address in JavaScript
How to validate an email address in JavaScript

How to Disable Right Click on a Website Using JavaScript
How to Disable Right Click on a Website Using JavaScript

How to Get Unique Values from a JavaScript Array
How to Get Unique Values from a JavaScript Array

Answers by CoreUI Core Team