Measuring First
Turn on Vue's performance markers, record a profile, and produce a number you can compare before and after a change.
app.config.performance = trueOptimising without measuring is guessing. Before changing anything, get a number you can compare: the same interaction, timed the same way, before and after.
Vue's own markers
Set app.config.performance = true on the application instance in main.ts. Vue then emits performance.mark entries for component init, compile, render and patch, and they appear in the browser's performance panel next to everything else. It works in development builds only, and only in browsers that support the performance.mark API.
Recording a profile
Open the browser's Performance panel, press record, do one thing — type one character, open one panel — then stop. The flame chart shows which function ran and for how long. Vue DevTools, installed as a browser extension or added to the project with the vite-plugin-vue-devtools plugin, shows the component tree, so you can tell which component a frame belongs to.
Getting a number
Flame charts are hard to compare between runs. Your own marks are not. Mark before a state change, await nextTick() so Vue has patched the DOM, then call performance.measure and read its duration. Run it five times and keep the median.
What trips people up
Development numbers are not production numbers. The dev build does extra work — warnings, prop checks, devtools hooks — so a dev profile tells you where the time goes, not how long a user waits. Confirm the win with vite build followed by vite preview.
Example
<script setup lang="ts">
import { nextTick, ref } from 'vue'
const rows = ref<string[]>([])
async function load(count: number): Promise<void> {
performance.mark('rows-start')
rows.value = Array.from({ length: count }, (_, index) => `Row ${index + 1}`)
await nextTick()
const measure = performance.measure('rows-render', 'rows-start')
console.log(`${count} rows patched in ${measure.duration.toFixed(1)}ms`)
}
</script>
<template>
<section>
<button type="button" @click="load(2000)">Load 2000 rows</button>
<p>{{ rows.length }} rows</p>
<ul>
<li v-for="row in rows" :key="row">{{ row }}</li>
</ul>
</section>
</template>
<style scoped>
ul {
max-height: 16rem;
overflow-y: auto;
}
</style>When to use it
- Proving that a change to a list actually made it faster, rather than assuming it did
- Finding which component's render is responsible for a slow keystroke in a form
- Capturing a before number so a performance ticket has something to close against
- Telling the difference between slow Vue rendering and a slow API response
More examples
Enabling Vue's markers in development only
app.config.performance is a per-application flag, so it is set on the instance returned by createApp, before mount. Vite replaces import.meta.env.DEV with a literal at build time, so the branch disappears from the production bundle. Reach for this the first time you record a profile and cannot tell which frames are Vue's.
// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
if (import.meta.env.DEV) {
app.config.performance = true
}
app.mount('#app')Adding Vue DevTools to the project instead of the browser
The Vite plugin serves DevTools from the dev server, so everyone on the team gets the same version without installing an extension. Useful on a machine where you cannot install extensions, or when you want DevTools available on a colleague's browser during a review. The plugin only applies to the dev server, not to vite build.
// vite.config.ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
export default defineConfig({
plugins: [vue(), vueDevTools()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})Logging long tasks while you click around
A long task is any block of main-thread work over 50ms, which is where dropped frames and unresponsive clicks come from. Importing this file in main.ts during an investigation turns 'the app feels sluggish' into a list of timestamps you can line up with a recording. Remove it, or guard it behind a dev check, before shipping.
// src/perf/longTasks.ts
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(`long task: ${entry.duration.toFixed(0)}ms`)
}
})
observer.observe({ type: 'longtask', buffered: true })
// Output: long task: 180ms
Discussion