7.8: Vue Best Practices & Production Build
Build and deploy production-ready Vue.js applications using modern tooling and best practices. Learn build optimization, environment configuration, deployment strategies, and how to ship your Vue app to production.
1. Code Organization Best Practices
Component Organization
Good folder structure:
src/
├── assets/ # Static assets
│ ├── images/
│ ├── fonts/
│ └── styles/
│ ├── main.css
│ └── variables.css
├── components/ # Reusable components
│ ├── common/ # Shared across app
│ │ ├── Button.vue
│ │ ├── Card.vue
│ │ └── Modal.vue
│ ├── layout/ # Layout components
│ │ ├── Header.vue
│ │ ├── Footer.vue
│ │ └── Sidebar.vue
│ └── features/ # Feature-specific
│ ├── auth/
│ │ ├── LoginForm.vue
│ │ └── RegisterForm.vue
│ └── products/
│ ├── ProductCard.vue
│ └── ProductList.vue
├── composables/ # Reusable logic
│ ├── useFetch.js
│ ├── useAuth.js
│ └── useLocalStorage.js
├── stores/ # Pinia stores
│ ├── auth.js
│ ├── cart.js
│ └── products.js
├── router/ # Router config
│ └── index.js
├── views/ # Page components
│ ├── Home.vue
│ ├── About.vue
│ └── Dashboard.vue
├── utils/ # Utility functions
│ ├── formatters.js
│ ├── validators.js
│ └── constants.js
├── App.vue
└── main.js
Component Naming
<!-- ✓ Good: PascalCase, descriptive -->
<template>
<UserProfile />
<ProductCard />
<ShoppingCart />
</template>
<!-- ✗ Bad: unclear, inconsistent -->
<template>
<profile />
<product />
<cart />
</template>
Props Definition
<script setup>
// ✓ Good: Explicit types and defaults
const props = defineProps({
title: {
type: String,
required: true
},
count: {
type: Number,
default: 0
},
items: {
type: Array,
default: () => []
},
user: {
type: Object,
default: () => ({})
}
})
// ✗ Bad: No types or validation
const props = defineProps(['title', 'count', 'items', 'user'])
</script>
Event Naming
<script setup>
// ✓ Good: Descriptive event names
const emit = defineEmits([
'update:modelValue',
'submit',
'close',
'item-selected'
])
// ✗ Bad: Generic or unclear
const emit = defineEmits(['update', 'click', 'change'])
</script>
2. Performance Optimization
Computed vs Methods
<script setup>
import { ref, computed } from 'vue'
const items = ref([...])
// ✓ Good: Cached, only recalculates when items change
const filteredItems = computed(() => {
return items.value.filter(item => item.active)
})
// ✗ Bad: Runs on every render
function getFilteredItems() {
return items.value.filter(item => item.active)
}
</script>
<template>
<!-- Good: Computed property (cached) -->
<div v-for="item in filteredItems" :key="item.id">
{{ item.name }}
</div>
<!-- Bad: Method (runs every render) -->
<div v-for="item in getFilteredItems()" :key="item.id">
{{ item.name }}
</div>
</template>
v-show vs v-if
<template>
<!-- ✓ Use v-show for frequent toggles -->
<Modal v-show="isModalOpen" />
<!-- ✓ Use v-if for rare changes -->
<AdminPanel v-if="user.isAdmin" />
<!-- ✗ Bad: v-if for frequent toggles (expensive) -->
<Sidebar v-if="sidebarOpen" />
</template>
Key Attribute
<template>
<!-- ✓ Good: Unique, stable keys -->
<div v-for="user in users" :key="user.id">
{{ user.name }}
</div>
<!-- ✗ Bad: Index as key (can cause bugs) -->
<div v-for="(user, index) in users" :key="index">
{{ user.name }}
</div>
<!-- ✗ Bad: No key -->
<div v-for="user in users">
{{ user.name }}
</div>
</template>
Lazy Loading Components
// router/index.js
// ✗ Bad: All components loaded upfront
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'
import Dashboard from '@/views/Dashboard.vue'
// ✓ Good: Lazy load routes
const routes = [
{
path: '/',
component: () => import('@/views/Home.vue')
},
{
path: '/about',
component: () => import('@/views/About.vue')
},
{
path: '/dashboard',
component: () => import('@/views/Dashboard.vue')
}
]
Component lazy loading:
<script setup>
import { defineAsyncComponent } from 'vue'
// Heavy component loaded only when needed
const HeavyComponent = defineAsyncComponent(() =>
import('@/components/HeavyComponent.vue')
)
</script>
<template>
<button @click="showHeavy = true">Load Heavy Component</button>
<HeavyComponent v-if="showHeavy" />
</template>
List Virtualization
For long lists, use virtual scrolling:
npm install vue-virtual-scroller
<script setup>
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
const items = ref([...]) // 10,000 items
</script>
<template>
<RecycleScroller
:items="items"
:item-size="50"
key-field="id"
v-slot="{ item }"
>
<div class="item">{{ item.name }}</div>
</RecycleScroller>
</template>
3. Error Handling
Global Error Handler
// main.js
const app = createApp(App)
app.config.errorHandler = (err, instance, info) => {
console.error('Global error:', err)
console.error('Component:', instance)
console.error('Error info:', info)
// Send to error tracking service
// trackError(err, { component: instance, info })
}
Try-Catch in Async Actions
<script setup>
import { ref } from 'vue'
const data = ref(null)
const error = ref(null)
const isLoading = ref(false)
async function fetchData() {
isLoading.value = true
error.value = null
try {
const response = await fetch('/api/data')
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
data.value = await response.json()
} catch (err) {
error.value = err.message
console.error('Fetch error:', err)
} finally {
isLoading.value = false
}
}
</script>
<template>
<div>
<div v-if="isLoading">Loading...</div>
<div v-else-if="error" class="error">
Error: {{ error }}
<button @click="fetchData">Retry</button>
</div>
<div v-else-if="data">
<!-- Display data -->
</div>
</div>
</template>
Error Boundaries (Vue 3.3+)
<script setup>
import { onErrorCaptured } from 'vue'
const error = ref(null)
onErrorCaptured((err, instance, info) => {
error.value = err
console.error('Captured error:', err)
// Prevent error from propagating
return false
})
</script>
<template>
<div v-if="error" class="error-boundary">
<h2>Something went wrong</h2>
<p>{{ error.message }}</p>
<button @click="error = null">Try again</button>
</div>
<slot v-else />
</template>
4. Environment Variables
Setting Up Environment Files
# .env.development
VITE_API_URL=http://localhost:3000
VITE_APP_TITLE=My App (Dev)
VITE_ENABLE_DEBUG=true
# .env.production
VITE_API_URL=https://api.production.com
VITE_APP_TITLE=My App
VITE_ENABLE_DEBUG=false
Important: Vite requires VITE_ prefix!
Using Environment Variables
// config.js
export const config = {
apiUrl: import.meta.env.VITE_API_URL,
appTitle: import.meta.env.VITE_APP_TITLE,
isDevelopment: import.meta.env.DEV,
isProduction: import.meta.env.PROD,
enableDebug: import.meta.env.VITE_ENABLE_DEBUG === 'true'
}
<script setup>
import { config } from '@/config'
async function fetchData() {
const response = await fetch(`${config.apiUrl}/data`)
// ...
}
</script>
<template>
<h1>{{ config.appTitle }}</h1>
</template>
Never commit .env.local or sensitive keys!
# .gitignore
.env.local
.env.*.local
5. Production Build
Build Configuration
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
build: {
// Output directory
outDir: 'dist',
// Generate source maps for debugging
sourcemap: true,
// Minification
minify: 'terser',
terserOptions: {
compress: {
drop_console: true, // Remove console.log in production
drop_debugger: true
}
},
// Chunk size warnings
chunkSizeWarningLimit: 1000,
// Rollup options
rollupOptions: {
output: {
manualChunks: {
// Separate vendor chunks
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'ui-vendor': ['element-plus'] // If using UI library
}
}
}
}
})
Building for Production
# Build for production
npm run build
# Preview production build locally
npm run preview
Build Output Analysis
# Install analyzer
npm install -D rollup-plugin-visualizer
# Add to vite.config.js
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
vue(),
visualizer({
open: true,
gzipSize: true,
brotliSize: true
})
]
})
6. Code Quality Tools
ESLint Configuration
npm install -D eslint @vue/eslint-config-prettier
// .eslintrc.cjs
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-recommended',
'@vue/eslint-config-prettier'
],
rules: {
'vue/multi-word-component-names': 'off',
'vue/require-default-prop': 'error',
'vue/require-prop-types': 'error',
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
}
}
Prettier Configuration
// .prettierrc.json
{
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"arrowParens": "avoid",
"vueIndentScriptAndStyle": false
}
Git Hooks with Husky
npm install -D husky lint-staged
npx husky install
npx husky add .husky/pre-commit "npx lint-staged"
// package.json
{
"lint-staged": {
"*.{js,vue}": [
"eslint --fix",
"prettier --write"
]
}
}
7. Accessibility Best Practices
Semantic HTML
<template>
<!-- ✓ Good: Semantic elements -->
<header>
<nav>
<ul>
<li><RouterLink to="/">Home</RouterLink></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>Page Title</h1>
<section>Content</section>
</article>
</main>
<footer>
<p>© 2025</p>
</footer>
<!-- ✗ Bad: Only divs -->
<div class="header">
<div class="nav">...</div>
</div>
</template>
ARIA Attributes
<template>
<!-- Button with aria-label -->
<button
@click="close"
aria-label="Close dialog"
>
×
</button>
<!-- Modal with proper ARIA -->
<div
v-if="isOpen"
role="dialog"
aria-labelledby="modal-title"
aria-modal="true"
>
<h2 id="modal-title">Modal Title</h2>
<p>Content</p>
</div>
<!-- Form with labels -->
<form>
<label for="email">Email</label>
<input id="email" type="email" required>
</form>
</template>
Keyboard Navigation
<script setup>
function handleKeydown(event) {
if (event.key === 'Escape') {
closeModal()
}
}
</script>
<template>
<div
@keydown="handleKeydown"
tabindex="0"
>
Keyboard accessible content
</div>
</template>
8. Security Best Practices
XSS Prevention
<template>
<!-- ✓ Good: Vue escapes by default -->
<p>{{ userInput }}</p>
<!-- ✗ Dangerous: Raw HTML from user input -->
<div v-html="userInput"></div>
<!-- ✓ Good: Sanitize before using v-html -->
<div v-html="sanitize(userInput)"></div>
</template>
API Requests
// ✓ Good: Validate and sanitize inputs
async function updateUser(userId, data) {
// Validate user ID
if (!Number.isInteger(userId) || userId <= 0) {
throw new Error('Invalid user ID')
}
// Sanitize data
const sanitizedData = {
name: data.name?.trim(),
email: data.email?.toLowerCase().trim()
}
const response = await fetch(`/api/users/${userId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(sanitizedData)
})
return response.json()
}
Environment Variables Security
// ✗ Bad: Exposing secrets in frontend
const API_KEY = 'secret-key-123' // Never do this!
// ✓ Good: Secrets stay on backend
const response = await fetch('/api/data', {
// Backend adds API key
credentials: 'include'
})
9. Deployment Checklist
Pre-Deployment
- Remove all
console.logstatements - Run
npm run buildsuccessfully - Test production build locally (
npm run preview) - Check bundle size
- Test on multiple browsers
- Test on mobile devices
- Validate HTML/CSS
- Check accessibility
- Set up error tracking (Sentry, LogRocket)
- Configure analytics (Google Analytics, Plausible)
Performance Checklist
- Lazy load routes
- Optimize images (WebP, compression)
- Enable gzip/brotli compression
- Use CDN for static assets
- Implement caching headers
- Minimize bundle size
- Code splitting implemented
- Remove unused dependencies
SEO Checklist
- Set page titles dynamically
- Add meta descriptions
- Configure Open Graph tags
- Create sitemap.xml
- Add robots.txt
- Ensure proper heading hierarchy
- Add structured data (JSON-LD)
10. Practical Exercises
Exercise 5.8.1: Production Build
Prepare your Vue app for production:
- Configure environment variables
- Optimize build configuration
- Analyze bundle size
- Remove console.logs
- Test production build
Exercise 5.8.2: Performance Audit
Optimize application performance:
- Convert methods to computed where appropriate
- Implement lazy loading for routes
- Add virtual scrolling for long lists
- Optimize images
- Measure improvement with Lighthouse
Exercise 5.8.3: Error Handling
Implement comprehensive error handling:
- Global error handler
- API error handling with retry
- Error boundary component
- User-friendly error messages
- Error logging service integration
Exercise 5.8.4: Accessibility Audit
Improve accessibility:
- Add ARIA labels where needed
- Ensure keyboard navigation works
- Test with screen reader
- Check color contrast
- Validate semantic HTML
11. Knowledge Check
Question 1: When should you use computed vs methods?
Show answer
Use computed for values derived from reactive data that are used multiple times - they're cached and only recalculate when dependencies change. Use methods for operations that need to run every time or accept arguments.Question 2: Why prefix environment variables with VITE_?
Show answer
Vite only exposes variables prefixed with `VITE_` to prevent accidentally exposing sensitive server-side variables to the client.Question 3: What's code splitting and why is it important?
Show answer
Code splitting breaks your app into smaller chunks loaded on-demand, reducing initial bundle size and improving load time. Essential for large applications.Question 4: How do you prevent XSS attacks in Vue?
Show answer
Vue escapes content by default in templates. Never use `v-html` with user input. If you must use HTML, sanitize it first. Validate and sanitize all user inputs.12. Key Takeaways
- Code organization improves maintainability
- Computed properties are cached and performant
- Lazy loading reduces initial bundle size
- Environment variables configure different environments
- Production builds are optimized and minified
- Error handling improves user experience
- ESLint/Prettier maintain code quality
- Accessibility makes apps usable for everyone
- Security prevents XSS and other attacks
- Bundle analysis identifies optimization opportunities
13. Congratulations!
You've completed Phase 5: Vue.js!
You now have the skills to:
- Build modern Vue 3 applications
- Use Composition API effectively
- Manage state with Pinia
- Create multi-page apps with Vue Router
- Write reusable composables
- Handle forms and events
- Build production-ready applications
Next: Apply your knowledge in Phase 5 Milestone Project - build a complete Vue.js SPA with routing, state management, and API integration!
14. Further Resources
Official:
Tools:
Learning: