skip to content

Initial Load Cost

Shrinking first-load code: tree-shaken built-ins, async components and lazy routes as split points, the __VUE_OPTIONS_API__ flag, and SSR or SSG for first paint. Interviewers probe the trade-offs.

part ofVue.jsoverview, primer and where to startread it →
on this pageshow

explore

questions

4

In Vue 3, how do you keep a heavy component out of the initial bundle, and what can silently undo the split?

level: juniorimportance: must knowfreq 58%

answer

  1. dynamic import as the split point
  2. wrap the loader, not the module
  3. fetched when first rendered
  4. one static import undoes it

basics

~10 s

Wrap a dynamic import in defineAsyncComponent, e.g. defineAsyncComponent(() => import('./Chart.vue')), and render it only when needed. A static import of the same file elsewhere, or rendering it on first paint, undoes the gain.

solid answer

~40 s

In Vue 3 you turn a component into a split point with `defineAsyncComponent(() => import('./Chart.vue'))`. The bundler sees the dynamic `import()` and puts `Chart.vue` and its dependencies in a separate chunk; Vue calls the loader the first time the component is **rendered**, caches the result, and renders it once it resolves. It pays off for things not needed at first paint — a chart behind a tab, a dialog behind `v-if`. The split is undone silently if **any other module still imports `Chart.vue` statically**, because the code then also lives in an eager chunk, and it gains little if the component renders on first paint anyway, since you have added a round trip. For routes, Vue Router's own lazy loading is the tool: the route's `component` is a plain `() => import(...)`, not `defineAsyncComponent`.

code

ts · 8 lines
ts
import { defineAsyncComponent } from 'vue'

// Split point: SalesChart.vue and what only it imports become a separate chunk,
// fetched the first time <SalesChart> is rendered, then cached.
export const SalesChart = defineAsyncComponent(() => import('./SalesChart.vue'))

// Undoes the split if it exists in any other module:
// import SalesChartEager from './SalesChart.vue'

go deeper

for a junior

Recall defineAsyncComponent with a dynamic import, and that the chunk loads the first time the component renders.

for a middle

Explain why a leftover static import or a shared import of the heavy library defeats the split, and how route-level lazy loading differs.

for a senior

Decide which components to split from what first paint needs, confirm in the build output, and avoid waterfalls from over-splitting.

for a principal

Set rules for where split points belong across an app and how they are checked in review, rather than splitting ad hoc.

## The mechanism A **split point** is a place where the bundler cuts the dependency graph and emits a separate file (a chunk) that is downloaded later. Bundlers create one wherever they see an ECMAScript dynamic `import()`. Vue 3's `defineAsyncComponent` is the adapter that turns that promise into something a template can render: ```vue <script setup lang="ts"> import { defineAsyncComponent, ref } from 'vue' const SalesChart = defineAsyncComponent(() => import('./SalesChart.vue')) const showChart = ref(false) </script> <template> <button @click="showChart = true">Show chart</button> <SalesChart v-if="showChart" /> </template> ``` 1. At build time, `SalesChart.vue` and everything only it imports (say, a charting library) go into their own chunk. 2. At run time, nothing is fetched until the `v-if` becomes true and Vue renders the async wrapper. 3. The wrapper calls the loader once, keeps the pending promise, and reuses the resolved component for every later instance — one fetch, however many times it is rendered. 4. When the chunk arrives, the wrapper renders the real component in place. `defineAsyncComponent` also accepts an options object — loading and error components, delays, timeouts — which is a topic of its own; the split itself only needs the loader. ## Where it pays off - Components behind interaction: dialogs, drawers, tabs other than the first, editors that open on click. - Heavy dependencies used by one component: charts, maps, rich-text editors, syntax highlighters. - Admin or rarely used panels inside an otherwise light page. ## What silently undoes the split | Pitfall | Why it defeats the split | |---|---| | Another file still has `import SalesChart from './SalesChart.vue'` | The module is reachable statically, so it also lands in an eager chunk | | The heavy library is imported in `main.ts` or a shared util | The dependency moves back to the entry chunk even if the component is lazy | | The async component renders on first paint | You pay an extra round trip before it can appear, often making first paint later | | Registering the component globally with a static import | Global registration keeps it in the bundle whether it is used or not | | Wrapping a route component in `defineAsyncComponent` | Route lazy loading is a separate router feature; the router docs say not to use async components as route components | The first two are the most common: a split that exists in one file and is erased by a convenience import somewhere else. Checking the build output for which chunk contains the heavy dependency is the only reliable confirmation. ## Components versus routes - **Component-level splits** use `defineAsyncComponent`, rendered like any component. - **Route-level splits** give the route's `component` a function returning `import('./views/Reports.vue')`; the router resolves it during navigation. The router's own guide describes the two as distinct features and asks you not to mix them for route components. Route-level splitting is usually the first and biggest win; component-level splitting refines what remains inside a route. ## Trade-offs to state in an interview - Every split adds a network request and a moment where the UI must show *something* — a placeholder, a loading component, or nothing. - Too many tiny chunks cost more in request overhead and waterfalls than they save. - Splitting is about the **first** load; a user who opens every panel still downloads everything eventually. ## What the user sees while it loads Until the chunk arrives, the async wrapper has nothing to render. By default it renders nothing in that slot, so layout can jump when the real component appears. The usual remedies are: - Reserve space with CSS so the arrival does not shift the page. - Provide a loading placeholder through the async component's options, which is a separate topic. - Trigger the render slightly before it is needed — for example when a tab is hovered — so the fetch overlaps with the user's intent. An async component inside a `<Suspense>` boundary is also treated as a dependency of that boundary by default, so the boundary's fallback shows until it resolves. That is useful when several async pieces should appear together rather than one by one. ## A quick checklist - Is the component needed for first paint? If yes, do not split it. - Is every static import of it gone? - Does its heavy dependency appear only in its chunk? - Is there a reasonable placeholder while it loads?

  • In Vue 3, if an async component is rendered in ten places, how many times is its loader called?
    Once per `defineAsyncComponent` call. The wrapper keeps the pending promise and the resolved component, so later instances reuse them. That only holds if the wrapper is created once — for example at module level — rather than recreated inside a function that re-runs.
  • Why can making an above-the-fold component async make the page slower?
    The browser cannot request the chunk until the app has run and rendered the async wrapper, so the component appears one network round trip later than if it had been in the entry chunk. Splitting helps only for code that is not needed for the first paint.

An async component is like a restaurant that keeps the dessert trolley in the back: it only comes out when someone orders dessert, but if a waiter also parks one copy at the entrance, the space saving is gone.

saying these in an interview costs you the question

  • Thinks defineAsyncComponent alone guarantees the code leaves the main bundle.
  • Wraps route components in defineAsyncComponent instead of a plain import function.
  • Makes above-the-fold components async to shrink the bundle.
  • Believes the loader runs again for every instance rendered.
  • Splits every small component into its own chunk by default.
open as a page

Why doesn't a Vue 3 production bundle include Transition or KeepAlive code when the app never uses them, unlike Vue 2's global API?

level: middleimportance: should knowfreq 40%

basics

~20 s

Vue 3 exposes its APIs and built-ins as named ES module exports, and the template compiler imports only the helpers a template uses. A bundler then drops unreferenced exports such as Transition, whereas Vue 2 hung everything on one global Vue object.

open as a page

A Vue 3 SPA's marketing landing page downloads the whole app bundle and paints late; how would you cut its initial load?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Serve the landing page as pre-rendered HTML — ideally SSG, shipped separately from the SPA — then shrink what JavaScript remains: lazy routes and async components, local instead of global registration, precompiled templates and feature flags.

open as a page

In a Vue 3 app, what does setting the __VUE_OPTIONS_API__ compile-time flag to false remove, and what can break?

level: middleimportance: nice to knowfreq 25%

basics

~20 s

VUE_OPTIONS_API set to false lets the bundler drop Options API support: data, computed, methods, watch, lifecycle options, mixins and extends. Components written with setup or script setup keep working; any dependency written with the Options API breaks.

open as a page