Vue / Advanced: architecture, hydration, and production evidence / Lesson 14
Performance, rendering cost, and measured optimization
Locate the user-visible bottleneck before changing reactivity, code loading, or list rendering.
On this page
Lesson goals
- Separate startup, update, layout, and data-processing costs.
- Use stable inputs and bounded rendering without hiding state changes.
Define a reproducible interaction
A slow first page, delayed search result, janky list, and growing memory use need different evidence. Define a representative task, dataset, device class, build mode, and observation method. Development instrumentation can help diagnosis while distorting final timings, so compare equivalent release-like conditions where appropriate. Measure before caching or marking data shallow. A performance improvement must preserve correct state, accessibility, and error behavior. This course does not run a server, browser benchmark, or load test; it explains the measurement design and validates only harmless local computations where available.
Avoid unnecessary source mutation during derivation
An expensive derivation belongs outside repeated ad hoc template expressions, but its algorithm and mutation behavior still matter. The standalone example sorts a copy of synthetic records and verifies the source order remains unchanged. This correctness check is not a benchmark or proof that sorting is the actual bottleneck. In Vue, a computed derivation can cache according to reactive dependencies; it cannot make an inefficient algorithm free. Avoid sorting a shared source array accidentally inside a getter. Review stable ids and actual consumer needs before adding another layer of cached state.
const notes = [{ id: "n2", title: "Zebra" }, { id: "n1", title: "Alpha" }]
const sorted = [...notes].sort((a, b) => a.title.localeCompare(b.title))
if (sorted[0].id !== "n1" || notes[0].id !== "n2") {
throw new Error("SORT_OWNERSHIP_MISMATCH")
}
Runs as standalone JavaScript under Node. The sorted view begins with n1 while the source still begins with n2. Locale-specific ordering needs its own product policy; this example proves ownership behavior for the supplied simple data.
Reduce work at the boundary that actually changes
Stable props can prevent unnecessary child updates when only one itemβs active state changes. Keep the value passed to each child as specific as the child needs instead of making every child depend on a broad changing object. Stable keys preserve identity but do not automatically make every render cheap. Large lists may need virtualization so only relevant items are materialized, with accessibility and focus behavior checked. v-once and v-memo are specialized controls whose dependency assumptions can create stale content if used incorrectly. Adopt them only after measurement and explicit correctness review.
Load a heavy optional component on demand
Code splitting can reduce initial work when an optional feature is not needed immediately. defineAsyncComponent wraps a component loader, and a bundler can turn the dynamic import into a separate chunk. The example renders the optional panel only after a user action. This does not automatically reduce server data cost or make a slow component fast once loaded. Provide meaningful loading and error behavior in the real feature. Router route-component lazy loading has its own documented pattern and should not be confused with wrapping every route in defineAsyncComponent. Keep chunk and network failure scenarios in the user contract.
<script setup>
import { ref, defineAsyncComponent } from "vue"
const showDetails = ref(false)
const NoteAnalytics = defineAsyncComponent(() => import("./NoteAnalytics.vue"))
</script>
<template>
<button type="button" @click="showDetails = true">Show note details</button>
<NoteAnalytics v-if="showDetails" />
</template>
Requires the app-defined NoteAnalytics.vue and a compatible bundler. The conditional controls first rendering and loader demand. No build, chunk download, or browser performance measurement was executed for this fragment.
Keep optimization promises truthful
shallowRef can reduce deep conversion overhead for large immutable structures or external systems, but nested changes do not trigger the same behavior as ordinary deep state. Replace the outer value according to the documented contract or use the appropriate explicit trigger only when justified. markRaw and other escape hatches affect proxy identity and should not become blanket fixes. Profile image decoding, layout, network, and JavaScript separately because low reactive-update counts can coexist with slow frames. Compare the original task after each focused change and retain bounded evidence. A faster result that loses updates or keyboard access is not an acceptable optimization.
Common errors and fixes
A computed sort changes the shared list
- Why it happens
- A mutating array operation is used during derivation.
- How to fix it
- Derive from a copy or a supported nonmutating method.
- Prevent it next time
- Test source identity and ordering contracts.
A memoized row displays stale content
- Why it happens
- The dependency list omits a value that affects rendering.
- How to fix it
- Restore complete dependencies or remove the premature optimization.
- Prevent it next time
- Pair performance work with correctness cases.
A shallow structure is mutated deeply with no update
- Why it happens
- Deep-reactivity behavior was assumed for a shallow API.
- How to fix it
- Follow the immutable replacement or explicit trigger contract.
- Prevent it next time
- Document shallow state to every consumer.
Check your understanding
1. A list remains slow even with few component updates. What should be investigated?
Show worked solution
Inspect layout, drawing, image decoding, JavaScript work, memory pressure, and data loading under a representative trace. Update counts are one signal, not the complete frame cost. Optimize the measured dominant phase and recheck the same task.
2. Should every route component be wrapped in defineAsyncComponent?
Show worked solution
No. Vue Router documents route lazy loading through dynamic component imports with its own integration. Use the API suited to the boundary and verify loading, errors, navigation, and bundle behavior instead of applying one wrapper universally.
Key takeaways
- Measure the user task before choosing an optimization.
- Stable inputs and bounded rendering need truthful state contracts.
- Code splitting, reactivity, and browser rendering solve different costs.