Skip to content

Instantly share code, notes, and snippets.

@johnleider
Created August 18, 2026 18:04
Show Gist options
  • Select an option

  • Save johnleider/0972cb33e3945d0978b55ebba5443602 to your computer and use it in GitHub Desktop.

Select an option

Save johnleider/0972cb33e3945d0978b55ebba5443602 to your computer and use it in GitHub Desktop.
Vuetify subtree re-render cascade — prod-build controlled repro
<script setup>
import { h, ref, shallowRef } from 'vue'
import Wrapper from './Wrapper.vue'
const selected = ref([])
const dummy = shallowRef(false)
const items = Array.from({ length: 50 }, (_, i) => ({ id: i, name: `Item ${i}` }))
const headers = [
{ title: 'Name', key: 'name' },
{ title: 'Row render count', key: 'renders', sortable: false },
]
const counters = { row: new Map(), child: 0, bottom: 0 }
window.__counters = counters
function track (id) {
const n = (counters.row.get(id) ?? 0) + 1
counters.row.set(id, n)
return n
}
function trackChild () {
return ++counters.child
}
function trackWrapped () {
counters.wrapped = (counters.wrapped ?? 0) + 1
return counters.wrapped
}
function trackOutside () {
counters.outside = (counters.outside ?? 0) + 1
return counters.outside
}
function trackBottom () {
return ++counters.bottom
}
function onSel (v) {
selected.value = v
}
const Child = {
setup (_, { slots }) {
return () => h('div', { class: 'pa-2' }, slots.default?.())
},
}
</script>
<template>
<Wrapper>
outside v-app, template wrapper: <span id="outside-count">{{ trackOutside() }}</span>
</Wrapper>
<v-app>
<v-container>
<Wrapper>
inside v-app, template wrapper: <span id="wrapped-count">{{ trackWrapped() }}</span>
</Wrapper>
<label class="d-block mb-2">
<input id="dummy" v-model="dummy" type="checkbox"> unrelated v-model (native)
</label>
<v-switch v-model="dummy" color="primary" hide-details label="unrelated v-model (VSwitch)" />
<Child>
plain child slot renders: <span id="child-count">{{ trackChild() }}</span>
</Child>
<v-data-table
v-model="selected"
:headers="headers"
:items="items"
:items-per-page="50"
item-value="id"
show-select
height="350"
fixed-header
>
<template #item.renders="{ item }">
{{ track(item.id) }}
</template>
<template #bottom>
<div id="bottom-count" class="pa-1 text-caption">bottom: {{ trackBottom() }}</div>
</template>
</v-data-table>
</v-container>
</v-app>
</template>
import { createApp } from 'vue'
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
import 'vuetify/styles'
import App from './App.vue'
createApp(App).use(createVuetify({ components, directives })).mount('#app')

Vuetify subtree re-render cascade — controlled repro

Vuetify 4.1.10, Vue 3.5.41, Vite production build (vite build && vite preview).

Two identical Wrapper.vue (<div><slot/></div>, compiled template SFC) instances with render counters in their slot content: one OUTSIDE <v-app> (direct child of App's template), one INSIDE <v-app><v-container>. Plus counters in a plain h() child, the table's #bottom slot, and every row's #item.renders slot.

Steps: 3 clicks on an unrelated native <input v-model="dummy">, then 1 row-checkbox click.

counter baseline after 3 unrelated toggles after 1 selection click
Wrapper OUTSIDE v-app 1 1 1
Wrapper INSIDE v-app 1 4 5
plain h() child (inside) 1 4 5
v-data-table #bottom slot 1 4 5
every row #item.renders 1 4 5

Conclusions:

  1. Vue's stable-slot gating works: the outside wrapper never re-renders even though App itself re-renders on every toggle (the v-model input is in App's template).
  2. The identical wrapper inside the Vuetify tree re-renders on every unrelated root update — the cascade is introduced by the Vuetify layer, not by Vue.
  3. The selection click also re-renders the inside wrapper: with v-model on the table in the root component, selection triggers the app-wide cascade in addition to the selection-Set row fan-out.
<template>
<div class="pa-2">
<slot />
</div>
</template>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment