Skip to content
ExponaBLOG
VUE.JS & NUXT · DAY 064 / 100

Vite configuration for Vue: a practical Vue & Nuxt guide

Tune your build.

Expona Admin6 min read

Introduction

A production Vue or Nuxt application lives and dies by its build configuration. Out of the box, Vite is fast and sensible, but every team eventually hits a point where the defaults need tuning—mysterious bundle bloat, slow dev server reloads, or imports that span five directory levels. This guide covers Vite configuration for Vue: a practical Vue & Nuxt guide, focusing on the four levers we pull most often in production: import aliases, environment variables, route-based code splitting, and bundle visualization. We'll show concrete patterns that work across both vanilla Vue setups and Nuxt.

Aliases for clean imports

Import paths that look like ../../../components/Button.vue are a maintenance nightmare. Aliases solve this by mapping a short prefix to a directory, so you write @/components/Button.vue instead. Vite supports this natively and it's one of the first things we configure on every project.

In a standard Vue app with vite.config.ts:

typescript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      '@components': path.resolve(__dirname, './src/components'),
      '@stores': path.resolve(__dirname, './src/stores'),
      '@utils': path.resolve(__dirname, './src/utils'),
    },
  },
})

Now throughout your codebase, you can write clean, predictable imports:

typescript
import Button from '@/components/Button.vue'
import { useCart } from '@stores/cart'
import { formatPrice } from '@utils/currency'

In Nuxt, aliases are configured in nuxt.config.ts and @ is already mapped to the src directory by default. You can add custom ones:

typescript
export default defineNuxtConfig({
  alias: {
    '@components': './components',
    '@layouts': './layouts',
    '@stores': './stores',
  },
})

Aliases also help when restructuring. If you move a components folder, you only update the config, not hundreds of import statements. TypeScript also respects these paths if you add them to tsconfig.json:

typescript
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
      "@components/*": ["./src/components/*"],
      "@stores/*": ["./src/stores/*"]
    }
  }
}

Environment variables with VITE_ prefix

Vite only exposes environment variables that start with VITE_ to your client bundle. This is a security feature—you don't accidentally leak a database URL. We use this pattern for API endpoints, feature flags, and third-party keys.

Create a .env file and environment-specific overrides:

typescript
# .env
VITE_API_BASE=https://api.example.com
VITE_FEATURE_NEW_CHECKOUT=true

# .env.development
VITE_API_BASE=http://localhost:3000
VITE_LOG_LEVEL=debug

# .env.production
VITE_ANALYTICS_KEY=pk_prod_abc123
VITE_FEATURE_NEW_CHECKOUT=false

Access them in your code via import.meta.env:

typescript
// api/client.ts
const client = axios.create({
  baseURL: import.meta.env.VITE_API_BASE,
})

// composables/useFeatures.ts
export function useFeatures() {
  return {
    newCheckoutEnabled: import.meta.env.VITE_FEATURE_NEW_CHECKOUT === 'true',
  }
}

TypeScript won't know about these keys by default, so add types to your environment. Create src/vite-env.d.ts:

typescript
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_API_BASE: string
  readonly VITE_FEATURE_NEW_CHECKOUT: string
  readonly VITE_LOG_LEVEL: string
  readonly VITE_ANALYTICS_KEY?: string
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}

In your build pipeline, load the right .env file. When you run npm run build, Vite automatically loads .env.production. For staging, pass the mode explicitly: vite build --mode staging loads .env.staging.

Code splitting by route

A large single-page app bundled as one file means users download unused code. Splitting by route lets each page load only what it needs. Vite and modern bundlers handle this well when you use dynamic imports.

In vanilla Vue with vue-router, mark route components as dynamic:

typescript
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/',
    component: () => import('@/pages/Home.vue'),
  },
  {
    path: '/products',
    component: () => import('@/pages/Products.vue'),
  },
  {
    path: '/cart',
    component: () => import('@/pages/Cart.vue'),
  },
  {
    path: '/checkout',
    component: () => import('@/pages/Checkout.vue'),
  },
]

export const router = createRouter({
  history: createWebHistory(),
  routes,
})

Each route gets its own chunk, loaded only when visited. Vite automatically names these intelligently in the build output.

In Nuxt, this is built-in. Every page file in the pages/ directory becomes a separate route chunk automatically. No configuration needed. Similarly, components registered globally via the components/ directory are code-split by default in recent Nuxt versions.

For shared dependencies (e.g., a heavy library used by multiple routes), configure explicit chunk splitting in vite.config.ts:

typescript
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vendor-utils': ['lodash-es', 'date-fns'],
          'vendor-ui': ['@headlessui/vue', '@heroicons/vue'],
        },
      },
    },
  },
})

This keeps heavy dependencies in separate chunks that browser caches between page loads, reducing duplication and improving cache hit rates.

Analyze bundles with visualizer plugins

You can't optimize what you don't measure. The rollup-plugin-visualizer generates an interactive treemap of your bundle, showing which files and dependencies consume the most space.

Install it:

typescript
npm install -D rollup-plugin-visualizer

Add to vite.config.ts:

typescript
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    vue(),
    visualizer({
      open: true,
      gzipSize: true,
      brotliSize: true,
      filename: 'dist/stats.html',
    }),
  ],
})

Run a production build and the plugin opens an interactive HTML file showing the size breakdown. Common surprises we find: unused dependencies pulled in by sub-packages, multiple versions of the same library, or components that should be lazy-loaded but aren't.

Another useful tool is the built-in Vite preview. After building, run npm run preview and measure real page load times in your staging environment.

We also recommend integrating bundle size checks into CI. Add a script to fail the build if the main chunk exceeds a threshold:

typescript
// scripts/check-bundle-size.js
import fs from 'fs'
import path from 'path'

const distPath = 'dist'
const maxSize = 250 * 1024 // 250 KB

const files = fs.readdirSync(distPath)
const jsFiles = files.filter(f => f.endsWith('.js'))

jsFiles.forEach(file => {
  const size = fs.statSync(path.join(distPath, file)).size
  if (size > maxSize) {
    console.error(`${file} exceeds ${maxSize} bytes: ${size}`)
    process.exit(1)
  }
})

Common mistakes

  • Forgetting the VITE_ prefix. Environment variables without it won't be exposed to the client. Server-side .env values stay private by design.
  • Hardcoding API URLs. Use environment variables even for development. It's the only way to swap endpoints cleanly between environments without code changes.
  • Not splitting shared vendor chunks. If two routes both import a 50 KB library, you get 100 KB of duplicated code unless you configure manualChunks.
  • Ignoring bundle analysis until launch. Check your bundle size early and often. A 5 MB app is much harder to trim than catching bloat at 2 MB.
  • Over-aliasing. Too many aliases make code harder to navigate. Stick to top-level directories: @/components, @/stores, @/utils.

Key takeaways

  • Use path aliases to keep imports clean and maintainable across your codebase.
  • Prefix environment variables with VITE_ so they're exposed to the client; define types in TypeScript for IDE support.
  • Leverage dynamic imports and code splitting by route to reduce initial load size.
  • Run bundle visualization regularly to catch unexpected size increases before they hit production.
  • Integrate bundle size checks into CI to prevent regressions.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles