Vue.js Performance Optimization: From 3s to 800ms Load Time
Our Vue app was slow. Initial load took 3 seconds, scrolling was janky, and users complained about lag.
I spent a week optimizing. Now it loads in 800ms, scrolling is smooth, and users are happy. Here’s what I did.
Table of Contents
The Problem
Performance metrics:
- Initial load: 3.2s
- Bundle size: 2.5MB
- Time to interactive: 4.1s
- Scroll FPS: 30-40
Users on slow connections waited 10+ seconds.
Code Splitting
Split bundle by route:
Before:
import UserList from './components/UserList.vue'
import UserDetail from './components/UserDetail.vue'
import Dashboard from './components/Dashboard.vue'
const routes = [
{ path: '/users', component: UserList },
{ path: '/users/:id', component: UserDetail },
{ path: '/dashboard', component: Dashboard }
]
One big bundle: 2.5MB
After:
const routes = [
{
path: '/users',
component: () => import('./components/UserList.vue')
},
{
path: '/users/:id',
component: () => import('./components/UserDetail.vue')
},
{
path: '/dashboard',
component: () => import('./components/Dashboard.vue')
}
]
Multiple small bundles:
- app.js: 200KB
- users.js: 150KB
- dashboard.js: 180KB
Load only what’s needed!
Lazy Loading Components
Load components on demand:
<template>
<div>
<button @click="showModal = true">Open Modal</button>
<modal v-if="showModal" @close="showModal = false"></modal>
</div>
</template>
<script>
export default {
components: {
Modal: () => import('./Modal.vue')
},
data() {
return {
showModal: false
}
}
}
</script>
Modal only loads when button clicked.
Computed Properties Caching
Bad (method, recalculates every time):
<template>
<div>{{ expensiveCalculation() }}</div>
</template>
<script>
export default {
methods: {
expensiveCalculation() {
return this.items.reduce((sum, item) => sum + item.price, 0)
}
}
}
</script>
Good (computed, cached):
<template>
<div>{{ totalPrice }}</div>
</template>
<script>
export default {
computed: {
totalPrice() {
return this.items.reduce((sum, item) => sum + item.price, 0)
}
}
}
</script>
Computed properties cache until dependencies change.
Virtual Scrolling
Rendering 10,000 items:
Before (renders all):
<template>
<div class="list">
<div v-for="item in items" :key="item.id" class="item">
{{ item.name }}
</div>
</div>
</template>
10,000 DOM nodes = slow!
After (virtual scrolling):
<template>
<virtual-list
:size="50"
:remain="10"
:bench="5"
>
<div v-for="item in items" :key="item.id" class="item">
{{ item.name }}
</div>
</virtual-list>
</template>
<script>
import VirtualList from 'vue-virtual-scroll-list'
export default {
components: { VirtualList }
}
</script>
Only renders visible items (~20 DOM nodes).
v-if vs v-show
v-if (removes from DOM):
<modal v-if="showModal"></modal>
Use for rarely toggled elements.
v-show (CSS display):
<sidebar v-show="sidebarOpen"></sidebar>
Use for frequently toggled elements.
Functional Components
Stateless components:
Before:
<template>
<div class="item">
<span>{{ item.name }}</span>
<span>{{ item.price }}</span>
</div>
</template>
<script>
export default {
props: ['item']
}
</script>
After (functional):
<template functional>
<div class="item">
<span>{{ props.item.name }}</span>
<span>{{ props.item.price }}</span>
</div>
</template>
No instance, faster rendering.
Object.freeze for Static Data
Large static data:
export default {
data() {
return {
items: Object.freeze([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
// ... 10,000 items
])
}
}
}
Vue won’t make it reactive, saves memory.
Debounce Input
Search input:
Before (searches on every keystroke):
<template>
<input v-model="searchQuery" @input="search">
</template>
<script>
export default {
methods: {
search() {
// API call on every keystroke!
this.fetchResults(this.searchQuery)
}
}
}
</script>
After (debounced):
<template>
<input v-model="searchQuery" @input="debouncedSearch">
</template>
<script>
import _ from 'lodash'
export default {
created() {
this.debouncedSearch = _.debounce(this.search, 300)
},
methods: {
search() {
this.fetchResults(this.searchQuery)
}
}
}
</script>
Waits 300ms after typing stops.
Keep-Alive for Cached Components
Cache component state:
<template>
<keep-alive>
<component :is="currentView"></component>
</keep-alive>
</template>
Component state preserved when switching.
Production Build
Enable production mode:
// webpack.config.js
const webpack = require('webpack')
module.exports = {
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
})
]
}
Or use Vue CLI:
npm run build
Removes warnings, enables optimizations.
Bundle Analysis
Analyze bundle size:
npm install --save-dev webpack-bundle-analyzer
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
Visualize what’s in your bundle.
Tree Shaking
Import only what you need:
Bad:
import _ from 'lodash'
_.debounce(fn, 300)
Imports entire lodash (70KB).
Good:
import debounce from 'lodash/debounce'
debounce(fn, 300)
Imports only debounce (5KB).
Image Optimization
Lazy load images:
<template>
<img v-lazy="imageUrl" alt="Product">
</template>
<script>
import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload)
</script>
Use WebP format:
<picture>
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Fallback">
</picture>
Prefetch/Preload
Prefetch next route:
const routes = [
{
path: '/users',
component: () => import(/* webpackPrefetch: true */ './UserList.vue')
}
]
Preloads during idle time.
Results
Before:
- Initial load: 3.2s
- Bundle size: 2.5MB
- Time to interactive: 4.1s
- Scroll FPS: 30-40
After:
- Initial load: 800ms (75% faster)
- Bundle size: 200KB initial (92% smaller)
- Time to interactive: 1.2s (71% faster)
- Scroll FPS: 60 (smooth)
Lessons Learned
- Code splitting - Biggest impact
- Lazy loading - Load on demand
- Virtual scrolling - For long lists
- Computed caching - Free optimization
- Measure first - Use profiler
Conclusion
Vue.js performance optimization is about loading less and rendering smarter.
Key takeaways:
- Code split by route
- Lazy load components
- Use computed properties
- Virtual scroll long lists
- Analyze and optimize bundle
Performance matters. Users notice the difference.