Performance, rendering and SSR basics

Where render cost actually comes from, the escape hatches that avoid deep reactivity, and what server rendering and hydration change.

Where the cost comes from

import { ref, computed, shallowRef } from 'vue'

// every property of a reactive object is wrapped in a proxy: fine for a small form,
// expensive for a large table you replace wholesale
const rows = shallowRef([])             // only the .value assignment is tracked

const sorted = computed(() => [...rows.value].sort(byDate))

// a plain function in the template re-runs on every render
const cheap = computed(() => sorted.value.filter(r => r.visible))
  • The template compiler hoists static markup and patches only what is dynamic. The wins come from making fewer things dynamic, not from micro-optimising directives.
  • A v-for over a long list is the usual cost: the whole list re-renders when a dependency changes. Filter, paginate or virtualise before reaching for a trick.
  • computed caches on its dependencies; a method called from the template does not, so it runs on every render.
  • Avoid creating objects, arrays or arrow functions inside a hot loop in the template — each render allocates new ones and defeats the child's prop comparison.
  • Measure first. The browser profiler and Vue devtools show which component re-renders and how often, which is the only way to know a change helped.

Escape hatches and long lists

import { shallowRef, triggerRef, markRaw } from 'vue'

const chart = shallowRef(null)
chart.value = createChart()      // replaces the whole value, so one re-render
triggerRef(chart)                // force an update after an in-place mutation

const handlers = markRaw({ onSave, onDelete })   // opted out of reactivity for good

const thirdParty = shallowRef(new Editor())      // a class instance you must not proxy
<!-- skip this subtree unless one of the listed values changed -->
<div v-memo="[user.id, user.updatedAt]">
  <ExpensiveRow :user="user" />
</div>

<!-- render only the window the user can see -->
<VirtualList :items="rows" :item-height="48" v-slot="{ item }">
  <Row :row="item" />
</VirtualList>
  • shallowRef tracks only .value. Use it for API responses, chart instances and large lists you replace as a whole.
  • markRaw excludes a value from the reactive system permanently — the right choice for third-party class instances that must not be proxied.
  • v-memo skips a subtree's update until one of the listed values changes. It is a targeted tool for a long list where only one row changes, so confirm it helps.
  • Virtual scrolling renders only the visible slice. For thousands of rows it is the only real fix, and it changes scrolling and search behaviour, so plan for it early.
  • Route-level code splitting usually beats render optimisation: the network is the larger cost for a first visit.
💡
Tie every performance change to a measurement. A list of 200 rows is not slow; a list of 200 rows that re-renders on every keystroke in an unrelated filter is.

Server rendering and hydration

// a component that cannot produce identical output on both sides
import { ref, onMounted } from 'vue'

const now = ref('')          // empty on the server, filled in the browser
const theme = ref('light')

onMounted(() => {
  now.value = new Date().toLocaleTimeString()
  theme.value = localStorage.getItem('theme') ?? 'light'
})
  • Server rendering sends HTML the user can see before any JavaScript runs, which helps first paint and crawlers. It does not reduce the JavaScript shipped — hydration still runs on the client.
  • Hydration mismatches come from output that differs between the two sides: Date.now(), random ids, window checks, a theme read from storage. Render a stable placeholder and fill it in on mount.
  • A browser-only widget can be excluded from the server pass by rendering it inside a client-only wrapper or mounting it in onMounted.
  • Module-scope state is a hazard on the server: one instance is shared by every request, so a user could see another user's data. State must be created per request.
  • Reach for a framework with server routes, file-based routing and per-page data loading when the app needs this everywhere; hand-rolled server rendering is more work than most content sites need.

FAQ

Do I need server rendering?
For public content that must be indexed or appear quickly on a slow connection, yes. For an internal dashboard behind a login, no: client rendering is simpler, and the first paint after authentication is not a conversion event.
Does v-memo make everything faster?
No. It adds a comparison on every render, and it only pays off when the subtree is expensive and its inputs rarely change. Applied broadly it adds overhead without changing the render.

Production builds, environments and deployment Slots, dynamic and async components

Last refreshed 2026-09-18.