What is new in Vue 3.4?

Jord
Product Engineer & Founder
The Vue JS core team announced the release of Vue 3.4 (code name "Slam Dunk") a few weeks ago. This is an exciting release. The release doesn't just contain the odd bug fix but delivers some fantastic framework features.
defineModel - Two Way Binding Simplified
With the introduction of <script setup> we have seen the Vue core team introduce macros to improve developer experience. The defineModel macro was shipped as an experimental feature in 3.3.
Well with the release of 3.4, defineModel is now stable! This macro aims to simplify how components support v-model. In the past when sending data into a component and emitting changes back to the parent, you had to do something like this:
<!-- Before: the old way --> <script setup> const props = defineProps(['modelValue']) const emit = defineEmits(['update:modelValue']) function updateValue(val) { emit('update:modelValue', val) } </script> <template> <input :value="props.modelValue" @input="updateValue($event.target.value)" /> </template>
That's a lot of ceremony for something as common as two-way binding. With defineModel, it collapses down to this:
<!-- After: with defineModel --> <script setup> const model = defineModel() </script> <template> <input v-model="model" /> </template>
That's it. No props declaration, no emit boilerplate. The macro handles the prop and the corresponding update: event under the hood. You get a ref back that you can read and write to directly.
It also supports named models and modifiers, which is great for components that need multiple v-model bindings:
<script setup> const firstName = defineModel('firstName') const lastName = defineModel('lastName') </script>
This is one of those changes that makes you wonder why it wasn't always like this.
v-bind Same-Name Shorthand
A small but welcome quality of life improvement. When the attribute name and the variable name are the same, you can now drop the value:
<!-- Before --> <img :id="id" :src="src" :alt="alt"> <!-- After --> <img :id :src :alt>
If you've used the ES6 object shorthand ({ id } instead of { id: id }), this follows the same logic. It's a small thing but it cleans up templates nicely, especially when you're passing a lot of props.
Reactivity System Overhaul
This is the one that matters most under the hood. The reactivity system got a significant refactor that makes computed properties smarter about when they trigger effects.
Before 3.4, a watcher on a computed value would fire every time its dependencies changed, even if the computed result stayed the same:
const count = ref(0) const isEven = computed(() => count.value % 2 === 0) watchEffect(() => console.log(isEven.value)) // logs true count.value = 2 // Before 3.4: logs true again (unnecessary) // After 3.4: doesn't log (value didn't change)
In 3.4, the callback only fires when the computed value actually changes. This might sound minor, but in a large application with chains of computed properties and watchers, it eliminates a lot of unnecessary re-renders.
There are two other improvements in the same vein:
- Multiple computed dependency changes now only trigger sync effects once, instead of once per dependency
- Array mutations like
shift,unshift, andsplicenow trigger effects only once, not once per element moved
These are the kind of changes you don't notice until your app suddenly feels snappier without you changing anything.
2x Faster Template Parser
The template parser was completely rewritten using a state-machine tokenizer approach. The result is a parser that is consistently twice as fast for templates of all sizes. SFC compilation performance improved by about 44% when generating source maps.
You won't interact with this directly, but you'll feel it in faster build times and HMR updates during development.
Better Hydration Mismatch Errors
If you're doing SSR with Nuxt or a custom setup, you've probably seen the dreaded hydration mismatch warning. In 3.4, these errors are actually useful now:
- Error messages clearly show what the server rendered vs what the client expected
- The actual DOM node is referenced so you can inspect it in devtools
- Class, style, and dynamic attribute mismatches are now caught properly
There's also a new compile-time flag __VUE_PROD_HYDRATION_MISMATCH_DETAILS__ if you want these detailed messages in production for debugging.
Breaking Changes
A few things were removed in this release that you should be aware of:
- Reactivity Transform has been removed from core. If you were using
$ref()and friends, you'll need the Vue Macros plugin going forward - Global JSX namespace was removed. If you're using TSX, add
jsxImportSource: 'vue'to your tsconfig @vnodeXXXevent listeners should be migrated to@vue:XXXv-isdirective is replaced by theisattribute with avue:prefix
Final Thoughts
Vue 3.4 is a solid release. The defineModel macro alone is worth the upgrade — it removes a genuinely annoying piece of boilerplate that every Vue developer has written hundreds of times. The reactivity improvements are the kind of invisible performance wins that make the framework better without asking anything of you.
If you're on 3.3, upgrading should be straightforward. Check the breaking changes above and you're good to go.
Stay in the loop.
Weekly insights on building resilient systems, scaling solo SaaS, and the engineering behind it all.