skip to content

A Vue 3 parent measures its children in onMounted, but one child created with defineAsyncComponent measures as empty: why, and how do you fix it?

level: seniorimportance: nice to knowfreq 30%

answer

  1. mounted means synchronous children
  2. the wrapper mounts, the inner comes later
  3. let the child announce itself
  4. observe size instead of sampling once

basics

~20 s

A parent's onMounted waits only for synchronous children. An async component mounts a lightweight wrapper immediately and the real component later, after its code loads, so at that moment there is only a placeholder. Measure when the child reports it has mounted, or observe its size.

solid answer

~40 s

Vue considers a component mounted once its **synchronous** children are mounted; the API reference explicitly excludes async components and components inside `<Suspense>`. A `defineAsyncComponent` child is a wrapper component: it mounts immediately, rendering a placeholder (or nothing) until its loader resolves, then re-renders with the real component, whose own `onMounted` fires later. The parent's `onMounted` has already run, so the measurement sees the placeholder. Fixes: have the child emit an event from its own `onMounted` and measure in the handler; use a `ResizeObserver`, which also catches later size changes from images or fonts; or wrap the async part in `<Suspense>` and act when it resolves. Do not fix it with a `setTimeout` guess.

go deeper

for a junior

Remember that a parent's onMounted guarantees only its synchronous children are mounted.

for a middle

Explain that an async component is a wrapper that mounts immediately and renders the real component later, so its own onMounted fires after the parent's.

for a senior

Diagnose the race with logging and network throttling, and fix it with an event-driven approach such as a ResizeObserver or a child readiness event instead of a timer.

for a principal

Treat layout that samples sizes once at mount as a design risk, and standardise on observed measurements for components whose content arrives asynchronously.

## The symptom A `DashboardGrid` lays out its panels in `onMounted` by reading each panel's height. Most panels are ordinary components; the heavy chart panel is loaded with `defineAsyncComponent(() => import('./ChartPanel.vue'))` so that its code is split into a separate chunk. The chart panel's height comes back as `0`, and the layout is wrong until something else triggers a relayout. ## What onMounted actually promises The Vue API reference defines mounted precisely. A component is considered mounted after: - **all of its synchronous child components have been mounted**, which explicitly does not include async components or components inside `<Suspense>` trees; - its own DOM tree has been created and inserted into the parent container, and it is only in the document if the application's root container is. So the parent's guarantee is narrower than "everything in my template is on screen". ## What an async component does at mount time `defineAsyncComponent` returns a **wrapper component**. Without a `<Suspense>` ancestor, the wrapper is a normal synchronous child when the parent renders: 1. The wrapper's setup starts loading the real component. 2. Until it loads, the wrapper renders a loading component if one is configured and its delay has passed, and otherwise nothing, which leaves an empty placeholder. 3. The wrapper is mounted, and the parent's `onMounted` runs. 4. When the loader resolves, the wrapper re-renders with the real component, which then mounts and runs its own `onMounted`. Step 3 happens before step 4, so the parent measures a placeholder. The same gap exists for content inside a `<Suspense>` boundary in the parent's template: while it is pending, that content is rendered off-document, and the parent's `onMounted` does not wait for it. ## Fixes, from most to least robust | Approach | How | Trade-off | |---|---|---| | Observe the size | `ResizeObserver` on the panel element, created in `onMounted`, disconnected in `onUnmounted` | also handles images, fonts and data arriving later | | Child announces readiness | child calls `emit('ready')` in its own `onMounted`; parent measures in `@ready` | explicit, but only covers the first mount | | Suspense boundary | wrap the async panels in `<Suspense>` and act on its resolve event | groups loading; Suspense is still experimental | | Timer | `setTimeout(measure, 500)` | a guess that fails on slow networks; avoid | The deeper point is that **one-shot measurement in the parent's `onMounted` is fragile** even without async components: images, web fonts and late data all change sizes after mount. Observing size changes turns a timing problem into an event. ## A sketch of the observer approach ```ts import { onMounted, onUnmounted, useTemplateRef } from 'vue' const grid = useTemplateRef<HTMLElement>('grid') let observer: ResizeObserver | undefined onMounted(() => { observer = new ResizeObserver(() => layout()) for (const panel of grid.value!.children) observer.observe(panel) }) onUnmounted(() => observer?.disconnect()) ``` This observes the panels present at mount; if the set of panels can change later, re-observe when it does. ## Why Vue does not simply wait It would be possible in principle to delay a parent's `onMounted` until every async descendant has loaded, but that would tie the parent's mount to the network: a slow chunk would hold back every piece of code that runs in `onMounted` across the whole ancestor chain, and splitting code into lazy chunks would quietly slow down unrelated components. Vue's definition keeps `onMounted` fast and predictable, and leaves the "wait for everything async" behaviour to an explicit opt-in, the `<Suspense>` boundary, where the waiting is visible in the template. ## How to diagnose it quickly 1. Log in the parent's `onMounted` and in the async child's `onMounted`: the parent's line appears first. 2. Inspect the DOM at the parent's `onMounted`: the async slot holds a placeholder or loading component. 3. Throttle the network in dev tools: the gap grows, confirming it is load timing rather than rendering. ## What interviewers listen for - The precise definition: mounted means synchronous children only. - The wrapper-then-inner mechanism of async components. - A fix that reacts to an event (a child's own mount, a size change) rather than a delay.

  • Can a Vue 3 component's onMounted run while its DOM is not in the document at all?
    Yes. The API reference says onMounted only guarantees the component's DOM is in the document if the application's root container is. Mounting an app into a detached element, or rendering content that is kept off-document until later, satisfies mounted without being visible, so measurements return zeros.
  • Why is a setTimeout before measuring a poor fix?
    It guesses how long loading takes. On a fast machine it wastes time; on a slow network or a cold cache the chunk arrives after the timer and the measurement is still wrong. Reacting to the child's own `onMounted` or to a `ResizeObserver` callback measures exactly when the size is real.

saying these in an interview costs you the question

  • onMounted waits for every descendant, async ones included
  • The parent's onMounted waits until the async component's code has loaded
  • A longer setTimeout reliably fixes the measurement
  • Mounted always means the DOM is visible in the document
  • The async child's onMounted runs before the parent's