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.



