A Vue 3 results panel re-renders on every keystroke in a search box it never displays; how do you find which reactive change triggers it?
answer
- confirm it re-renders first
- debug hook on the suspect
- read the type and key
- props object means the parent
basics
~20 sConfirm the extra updates with the Vue devtools, then add onRenderTriggered to the panel and log the event's type, key and target on each keystroke. The key names the write; then find which read in the panel's render depends on it.
solid answer
~40 sFirst **confirm** it: in a development build, the Vue devtools show the panel updating on each keystroke and how long each update takes. Then **ask the component**: add `onRenderTriggered((e) => console.log(e.type, e.key, e.target))` to the panel (a `debugger` works too) and type once. The event names the write: `set` on key `query` of a shared reactive `filters` object means the panel's render reads something that depends on `query` — often `iterate` from looping or printing the whole object. If `target` is the panel's own props object, a changed prop came from the parent; repeat the check one level up, usually finding an object literal rebuilt on each render. An event with no `target` came through a `computed`: add `onTrigger` to it. Fix the read, then re-measure the same keystroke.
code
vue · 20 lines<script setup lang="ts">
import { onRenderTracked, onRenderTriggered } from 'vue'
const props = defineProps<{ rows: { id: number; name: string }[] }>()
if (import.meta.env.DEV) {
onRenderTracked((e) => {
if (e.type === 'iterate') console.log('[ResultsPanel] iterates', e.target)
})
onRenderTriggered((e) => {
console.log('[ResultsPanel] triggered by', e.type, String(e.key), e.target)
})
}
</script>
<template>
<table>
<tr v-for="row in props.rows" :key="row.id"><td>{{ row.name }}</td></tr>
</table>
</template>go deeper
Recall that Vue offers development-only hooks to see what made a component re-render, and that you measure before fixing.
Explain how to read the triggered event's type, key and target, and what an iterate dependency means.
Walk the full diagnosis: confirm with the devtools, attribute with onRenderTriggered, follow props to the parent or a computed's onTrigger, fix, re-measure.
Turn recurring re-render findings into team conventions and review checks, rather than one-off fixes per component.
## The symptom A page has a search `<input>` bound with `v-model` to `filters.query` and a heavy `<ResultsPanel>` that renders a table. The panel never shows the query, yet typing feels sluggish and every keystroke seems to re-render the table. The job is to find **which reactive write** reaches the panel's render effect, and **through which read**. ## Step 1 — confirm and size it 1. Run a **development build** with the Vue devtools open. 2. Type one character and check which components report an update. If `ResultsPanel` updates on each key, the suspicion is confirmed. 3. Note its render and patch durations; they are the baseline to beat. If the panel does not update, the lag is elsewhere — the input's own handler, a watcher doing heavy work, or layout — and the Vue-side hunt stops here. ## Step 2 — ask the component why Add a development-only debug hook to the panel: ```vue <script setup lang="ts"> import { onRenderTriggered } from 'vue' if (import.meta.env.DEV) { onRenderTriggered((e) => { console.log('[ResultsPanel]', e.type, String(e.key), e.target) }) } </script> ``` Type one character and read the log. `onRenderTriggered` fires for each notification of the panel's render effect, with the operation `type`, the `key` and the reactive `target`. ## Step 3 — interpret the event | What the event shows | What it means | Where to look | |---|---|---| | `set` on `query`, target is the shared `filters` object | The panel's render reads `filters.query`, directly or indirectly | Template bindings such as `v-bind="filters"`, a debug `{{ filters }}`, a method called from the template | | `set` on a prop name, target is the panel's props object | The parent passed a changed prop | The parent's template: an inline object or array built on each render | | Only `effect`, no `target` or `key` | The change came through a `computed` the panel reads | Add `onTrigger` to that computed's debug options | | Nothing logged, yet the panel updates | The update did not come through a reactive read of its own | The parent's slot content, or a directive or transition on the panel's vnode | To see **which read** created the dependency, pair it with `onRenderTracked` and filter for the key in question; a `type` of `iterate` shows the render walks a whole object, so any key change re-renders it. ## Step 4 — typical causes it uncovers - The panel receives the whole `filters` object and reads it wholesale — `v-bind="filters"` onto a child, or printing it — so `query` became a dependency. - The parent passes `:options="{ pageSize, sort }"` inline; the parent re-renders on each keystroke because **it** reads `query`, and the new object fails the child's props check every time. - A `computed` in the panel filters rows using `filters` in full, so it re-evaluates on `query` changes and returns a new array even when rows are unchanged. - A `v-model` on the search box writes to the same reactive object the panel iterates. ## Step 5 — fix and verify The fix belongs to the cause: narrow what the panel reads, pass only primitives it needs, or keep the input's state local and hand the panel a debounced value. Then repeat Step 1 with the same keystroke and confirm the panel no longer updates, or that its duration dropped. Remove the debug hook afterwards — it does nothing in production, but it is noise in development. ## Pitfalls during the hunt - **Logging the whole event object** in a busy component can freeze the console; log `type`, `key` and a short label instead. - **Forgetting that tracked fires on every read**: an unfiltered `onRenderTracked` log for a large table is thousands of lines per keystroke. - **Reading one event as the whole story**: several triggers can arrive for a single keystroke — for example the input's value and a derived flag — and each has its own cause. - **Measuring after the fix in different conditions**: repeat the same interaction, on the same build, with the same data size. - **Leaving the instrumentation in**: it is harmless in production but clutters development logs for everyone. ## What makes this a senior answer - Measuring before changing code. - Using `onRenderTriggered` to turn "it re-renders" into a specific write and key. - Knowing that a props-object target sends you to the parent, and a missing target sends you to a computed.
- The Vue 3 panel's trigger log shows `set` on its own prop `options` on every keystroke. What is the likely cause?The parent re-renders on each keystroke because its template reads the query, and it passes `options` as an object literal built during that render. The new reference fails the child's props comparison every time. Hoisting the object into state or a `computed` in the parent makes the reference stable.
- Why run this investigation in a development build rather than production?`onRenderTriggered`, `onRenderTracked` and the devtools' per-component timings are all development-only in Vue 3. A production build would show the lag in a browser trace but give no component-level answer about which reactive write caused it.
saying these in an interview costs you the question
- Wraps the panel in v-memo before finding out what triggers it.
- Assumes the panel re-renders just because its parent re-rendered.
- Expects onRenderTriggered to work on the production build.
- Reads only the key and ignores whether the target is the props object.
- Adds a deep watcher to detect changes instead of using the debug hooks.