Performance and Deployment
|
This section documents the current Vue 3.x release line as published at
the official Vue.js documentation, which is the reference these pages are written and
verified against. No specific patch version is pinned. The Composition API with This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since Vue and its ecosystem iterate quickly. This section’s bibliography lists the reference material consulted while preparing these pages. |
Vue is fast by default, but a large app still benefits from measuring before optimising and from a build and deployment setup tuned for production. This page pairs the Performance and Production Deployment guides with worked deployment recipes.
Profiling
Measure first — the guide’s Profiling Tools section lists these:
-
Vue DevTools timeline — the Performance tab records component render durations and event timings in a development build.
-
Browser performance panel — Chrome/Firefox DevTools show real frame cost, long tasks, and layout thrashing that DevTools alone cannot.
-
app.config.performance— set ittrue(dev only) and Vue emitsperformance.mark/performance.measureentries for componentinit,compile,render, andpatch, visible in the browser’s performance timeline.
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.config.performance = true
app.mount('#app')
Page-load optimisation
-
Bundle size — keep dependencies lean, prefer ES-module builds so bundlers tree-shake, and audit with
npx vite-bundle-visualizer. Import only what you use from utility libraries. -
Code splitting — Vite splits at every dynamic
import(); combine it with router-level lazy loading (below) so a route’s code arrives only when visited. -
SSR / SSG — server-rendering the first paint, or pre-rendering static routes at build time, cuts time-to-content for content-heavy pages. See Page Load Optimizations and Server-Side Rendering.
-
Assets and CDN — serve hashed static assets from a CDN with long-lived cache headers; preload the critical JS/CSS; use
<link rel="modulepreload">for the entry chunk.
Update-time optimisation
Once mounted, the cost is re-rendering. The Update Optimizations section:
-
v-memo— skip re-rendering av-forrow whose dependencies are unchanged (see Rendering, Render Functions, and Web Components). -
shallowRef/shallowReactive— for large immutable payloads (API responses, editor documents), a shallow container tracks only the.value/ top-level reassignment, not every nested property. -
<KeepAlive>— cache component instances across toggles so a heavy tab or route is not rebuilt each time it reappears. -
Virtualised lists — render only the visible window of a long list (e.g.
vue-virtual-scroller). -
Stable
:key`s— key `v-for by a persistent id, never the array index, so patching reuses DOM instead of recreating it. -
Avoid accidental reactivity — do not wrap large static config or class instances in
reactive; usemarkRawor a plain module constant. -
Computed stability — since Vue 3.4 a
computedwhose recalculated result is===(or deep-equal for primitives) to the previous value does not trigger downstream effects. Return stable references from computeds and keep them side-effect free.
import { shallowRef, triggerRef, markRaw } from 'vue'
const rows = shallowRef([]) // 10k-row grid -- track the array identity only
async function load() {
rows.value = await fetchRows() // reassign triggers; deep mutation would not
}
const chart = markRaw(new ChartLibrary()) // third-party instance, never made reactive
Async components and route splitting
defineAsyncComponent defers loading a component until it
renders, with loading and error states:
import { defineAsyncComponent } from 'vue'
const CommentPanel = defineAsyncComponent({
loader: () => import('./CommentPanel.vue'),
loadingComponent: Spinner,
errorComponent: LoadError,
delay: 200,
timeout: 8000,
})
Route-level splitting is the highest-value case — give the router a dynamic import and each route becomes its own chunk:
const routes = [
{ path: '/', component: () => import('./views/Home.vue') },
{ path: '/reports', component: () => import('./views/Reports.vue') },
]
Production feature flags
Bundlers replace these compile-time globals; setting the unused ones to false lets the minifier drop dead
code. The Compile-Time Flags reference is authoritative.
| Flag | Effect when false |
|---|---|
|
drop Options API support from the bundle (only if the app and its deps are Composition-only) |
|
keep DevTools disabled in production (the default; set |
|
omit verbose hydration-mismatch messages from the prod SSR build |
With Vite these are pre-configured; override in vite.config.js:
export default {
define: {
__VUE_OPTIONS_API__: false,
__VUE_PROD_DEVTOOLS__: false,
},
}
Without a bundler that sets process.env.NODE_ENV, define these yourself or you ship the dev build.
Dev vs. prod builds
The development build includes the template compiler, warnings, dev-only invariant checks, and the DevTools bridge — several times the size and slower. The production path, per Production Deployment:
-
NODE_ENV=production— gates everyif (DEV)warning block.vite buildsets it automatically. -
Ahead-of-time template compilation — SFCs are compiled to render functions at build time, so the runtime-only Vue build (no compiler) ships. Avoid runtime string templates in production.
-
vite buildoutput — minified, hashed, tree-shaken JS/CSS plus anindex.htmlindist/. Inspect it withvite preview.
npm run build # -> dist/ (vue-tsc type-check + vite build)
npm run preview # serve dist/ locally to sanity-check the real bundle
Runtime error tracking
Wire a reporter so production errors reach your monitoring service:
const app = createApp(App)
app.config.errorHandler = (err, instance, info) => {
// uncaught errors from render, watchers, lifecycle hooks, event handlers
reportToService(err, { info })
}
app.config.warnHandler = (msg, instance, trace) => {
// dev-only; silence known-noisy warnings or forward them
}
Component-local recovery uses the errorCaptured
hook (or onErrorCaptured in setup): return false to stop propagation after showing a fallback UI. See
app.config.errorHandler.
Deploying a static SPA
A built Vue SPA is static files. Two server-side requirements:
-
History-mode fallback — with
createWebHistory, deep links like/reports/42must serveindex.htmlso the router can take over. Every host below needs a rewrite rule; see Vue Router — HTML5 History Mode. -
Cache headers — hashed assets under
/assets/getCache-Control: public, max-age=31536000, immutable;index.htmlgetsno-cacheso a deploy is picked up immediately.
Netlify
# netlify.toml
[build]
command = "npm run build"
publish = "dist"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
[[headers]]
for = "/assets/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
AWS S3 + CloudFront
Upload dist/ to a private bucket, serve it through CloudFront with Origin Access Control, and map error
responses back to the app:
aws s3 sync dist/ s3://my-app-bucket --delete \
--cache-control "public, max-age=31536000, immutable" \
--exclude index.html
aws s3 cp dist/index.html s3://my-app-bucket/index.html \
--cache-control "no-cache"
# CloudFront: custom error responses 403 and 404 -> /index.html, response code 200
aws cloudfront create-invalidation --distribution-id ABCD1234 --paths "/index.html"
CI/CD pipeline
Lint, test, build, deploy. GitHub Actions:
name: deploy
on:
push:
branches: [main]
jobs:
build-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run lint
- run: npm run test:unit -- --run
- run: npm run build
- run: |
aws s3 sync dist/ s3://my-app-bucket --delete
aws cloudfront create-invalidation --distribution-id ABCD1234 --paths "/*"
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
The equivalent GitLab CI/CD:
stages: [test, build, deploy]
test:
stage: test
image: node:24
script:
- npm ci
- npm run lint
- npm run test:unit -- --run
build:
stage: build
image: node:24
script:
- npm ci
- npm run build
artifacts:
paths: [dist/]
deploy:
stage: deploy
image: registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest
only: [main]
script:
- aws s3 sync dist/ s3://my-app-bucket --delete
- aws cloudfront create-invalidation --distribution-id "$CF_DIST_ID" --paths "/*"
See also
-
Rendering, Render Functions, and Web Components — the compiler hints that make renders cheap.
-
Security and Accessibility — hardening and a11y checks to add to the CI job above.