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>&copy; 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.log statements
  • Run npm run build successfully
  • 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:

  1. Configure environment variables
  2. Optimize build configuration
  3. Analyze bundle size
  4. Remove console.logs
  5. Test production build

Exercise 5.8.2: Performance Audit

Optimize application performance:

  1. Convert methods to computed where appropriate
  2. Implement lazy loading for routes
  3. Add virtual scrolling for long lists
  4. Optimize images
  5. Measure improvement with Lighthouse

Exercise 5.8.3: Error Handling

Implement comprehensive error handling:

  1. Global error handler
  2. API error handling with retry
  3. Error boundary component
  4. User-friendly error messages
  5. Error logging service integration

Exercise 5.8.4: Accessibility Audit

Improve accessibility:

  1. Add ARIA labels where needed
  2. Ensure keyboard navigation works
  3. Test with screen reader
  4. Check color contrast
  5. 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: