Vue 3.4: faster compilation and clearer component models

Trace a component model from input to parent, reproduce missing-update failures, and see which computed consumers rerun in Vue 3.4.
Separate compiler speed from application behavior
Vue 3.4 stabilized defineModel and changed how computed results trigger effects. This explainer uses a display-name input to show both mechanisms. It assumes you already use script setup and component props. The example is synthetic. It is not a report about an Ulties client application.
The release announcement, dated 28 December 2023, also reported a parser running roughly twice as fast in its benchmarks. That measures one compilation stage. It does not establish that your application builds twice as fast. The reproduction below checks component behavior, not build performance. Use your own locked application and comparable cache conditions for a build-time comparison.
A model is still a prop and an event
defineModel creates a ref backed by the modelValue prop and update:modelValue event. When the input writes to that ref, the parent listener receives the new value. When the parent changes its value, the prop carries it back to the child. The macro removes repeated wiring. The parent still owns the state.
NameField requires a string model and an input ID. The parent supplies the initial empty string. Two instances use different IDs so clicking either label focuses its own input. The fixture verifies their values remain separate when one is edited.
<script setup>
defineProps({
inputId: { type: String, required: true }
});
const name = defineModel({ type: String, required: true });
</script>
<template>
<label :for="inputId">Display name</label>
<input :id="inputId" v-model="name" maxlength="80" />
</template>
Keep validation with the owner of the value
ProfileEditor considers a trimmed value valid when it is non-empty. This is a demonstration rule, not a policy for real people’s names. It accepts a single letter or emoji and rejects whitespace-only input. There is no save operation or server-side validation in this example.
The previous example required trim().length to be at least two and described that as two non-space characters. The browser reproduction exposed the mismatch: one emoji occupied two UTF-16 code units and passed. Trimming also leaves interior spaces in place. A minimum string length was an unjustified name requirement, so this revision asks only for a value.
Typing Ada updates the parent preview. Reset assigns an empty string in the parent and clears the child input. A value supplied through a later promise also reaches the input. These checks exercise both directions of the binding.
<script setup>
import { computed, ref } from 'vue';
import NameField from './NameField.vue';
const name = ref('');
const valid = computed(() => name.value.trim().length > 0);
</script>
<template>
<NameField input-id="profile-name" v-model="name" />
<p>Preview: {{ name }}</p>
<p v-if="!valid">Enter a display name.</p>
<button type="button" @click="name = ''">Reset</button>
</template>
Watch the result, not every source change
The fixture attaches one watchEffect consumer to valid and another to name. For the sequence empty string, A, Ada, Alan, the validity consumer records false then true. The name consumer records every value. Changing Ada to Alan still updates the preview even though validity stays true.
This distinction follows the computed result. The tagged Vue 3.4.0 implementation compares the newly evaluated result with the previous value before notifying dependents. A separate fixture comparison returns a fresh object containing the same boolean. Its consumer runs for every name change because each object has a different identity.
These are effect observations, not timings or a count of every render in an application. The harness reads the component’s setup state through a version-pinned internal observation point. That access is test instrumentation, not an API to use in your components. Avoid adding a watcher that copies derived validity into another ref: the computed getter already describes the dependency.
Break the wiring before trusting the test
Replacing the parent’s v-model with a modelValue prop alone removes the update listener. In the deliberate failure case, typing changes the visible child input while the parent remains empty. A screenshot of the input alone would miss the broken binding.
Replacing computed with a ref containing the initial validity makes the preview update but leaves validity false after Ada is entered. A third variant gives the child a default while the parent starts undefined. The child displays Child default while the parent is still undefined. Initialize the owner’s value instead of relying on a child-only default to synchronize it.
Download the source and tests below. With Docker running and Python 3 installed, extract the archive and run python3 frontend/scripts/verify-vue-34.py from its root. Image construction needs network access. The browser run has no external network, host directory mounts or credentials. It compiles the published SFCs with Vue and its SFC compiler pinned to 3.4.0, then tests them in Chromium.
The expected summary reports twelve checks, including three deliberate failure cases. The bundle includes its lockfile and captured evidence. This historical reproduction uses Node 24.20.0 and Chromium 153.0.8010.12. It is not a recommended dependency set for a new application. It does not verify TSX, SSR hydration, persistence or performance.
Reproduce the examples
Exact SFCs, pinned compiler and browser, dependency lock, twelve checks and three deliberate failure cases.
Download the source and tests (.zip)
