What is new in Vue 3.5?

Jord
Product Engineer & Founder
Vue 3.5 (code name "Tengen Toppa Gurren Lagann") landed in September 2024. There are no breaking changes in this release, which is always nice. What you do get is a faster, leaner reactivity system and a set of new features that solve real pain points.
Reactive Props Destructure
This was experimental in 3.4 and is now stable and enabled by default. You can destructure defineProps and the variables stay reactive.
Before, setting default values on typed props was awkward:
const props = withDefaults(
defineProps<{
count?: number
msg?: string
}>(),
{
count: 0,
msg: 'hello'
}
)
Now you just write it like normal JavaScript:
const { count = 0, msg = 'hello' } = defineProps<{
count?: number
msg?: string
}>()
The compiler is smart enough to convert destructured variable access into props.count under the hood, so reactivity is preserved. One thing to note — if you want to watch a destructured prop, you need a getter:
watch(() => count, (newVal) => {
console.log('count changed:', newVal)
})
This is a small change that removes a lot of friction. No more withDefaults wrapper, no more separate defaults object.
useTemplateRef()
Template refs have always been a bit magic. You declare a ref with the same name as the ref attribute in the template and Vue connects them. It works, but it's implicit and doesn't support dynamic refs.
Vue 3.5 adds useTemplateRef to make this explicit:
<script setup>
import { useTemplateRef } from 'vue'
const inputRef = useTemplateRef('input')
</script>
<template>
<input ref="input">
</template>
The string you pass to useTemplateRef matches the ref attribute in the template. The advantage over the old approach is that it works with dynamic ref bindings and makes the connection between script and template obvious.
onWatcherCleanup()
If you've ever set up a fetch call inside a watcher and needed to abort it when the watcher re-runs, you'll appreciate this. Previously you had to manage cleanup manually. Now there's a dedicated API:
import { watch, onWatcherCleanup } from 'vue'
watch(id, (newId) => {
const controller = new AbortController()
fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
// handle response
})
onWatcherCleanup(() => {
controller.abort()
})
})
The cleanup callback runs before the watcher triggers again or when the component unmounts. It's similar to the cleanup function in React's useEffect, but scoped specifically to watchers.
Lazy Hydration
This is a big one for SSR and Nuxt users. Async components can now specify a hydration strategy, meaning the component's JavaScript isn't loaded and executed until it's actually needed:
import { defineAsyncComponent, hydrateOnVisible } from 'vue'
const AsyncComp = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
hydrate: hydrateOnVisible()
})
With hydrateOnVisible, the component renders as HTML on the server but only hydrates on the client when it scrolls into view. This is great for below-the-fold content, comment sections, footers — anything the user might not interact with immediately.
This directly translates to better page load performance without sacrificing SEO, since the server-rendered HTML is still there.
useId()
Generating unique IDs that are stable across server and client renders has always been a pain in SSR. If the server generates one ID and the client generates another, you get a hydration mismatch.
useId solves this:
<script setup>
import { useId } from 'vue'
const id = useId()
</script>
<template>
<form>
<label :for="id">Name:</label>
<input :id="id" type="text" />
</form>
</template>
The generated ID is guaranteed to be the same on server and client. Simple, but it fills a gap that previously required third-party solutions or manual workarounds.
Deferred Teleport
Teleport now supports a defer prop that lets you teleport to a target element that's rendered later in the same template:
<Teleport defer target="#container">
<p>This content gets teleported</p>
</Teleport>
<div id="container"></div>
Without defer, the teleport target needs to exist before the Teleport component renders. This removes that ordering constraint, which is useful when you don't control the render order of your layout.
data-allow-mismatch
Another SSR quality of life improvement. Some hydration mismatches are expected — like a formatted date that differs between server and client timezones. You can now suppress the warning:
<span data-allow-mismatch>{{ date.toLocaleString() }}</span>
You can also scope it to specific mismatch types: text, children, class, style, or attribute. This keeps your console clean without hiding real issues.
Reactivity System Performance
The internals got a major refactor. The headline numbers:
- 56% reduction in memory usage for the reactivity system
- Up to 10x faster tracking for large, deeply reactive arrays
There are no API changes here — it's purely internal. But if your app works with large reactive datasets, you should see a noticeable improvement after upgrading.
Custom Elements Improvements
If you're building web components with Vue, 3.5 adds several capabilities:
configureAppoption for setting up app-level config (error handlers, plugins)useHost()anduseShadowRoot()composables for accessing the custom element contextshadowRoot: falseoption to render without Shadow DOMnonceoption for CSP-compliant style injection
This is a niche feature set, but it brings Vue's custom element support much closer to production-ready.
Final Thoughts
Vue 3.5 is a quality release. There's no single headline feature, but the sum of the parts is significant. Reactive props destructure removes daily boilerplate. Lazy hydration is a genuine performance win for SSR apps. The reactivity system using 56% less memory is the kind of thing that makes your infrastructure team happy.
No breaking changes means upgrading is low risk. If you're on 3.4, it's worth the bump.
Stay in the loop.
Weekly insights on building resilient systems, scaling solo SaaS, and the engineering behind it all.