| |

Vue.js 42 🟢 Async Components and Lazy Loading via defineAsyncComponent()

A Vue application ships as JavaScript. The larger the application, the larger the bundle, and the longer the initial load. A user who lands on the login page does not need the code for the settings panel, the admin dashboard, or the rich text editor. Loading all of it upfront wastes bandwidth and delays the first paint. Async components are the mechanism that splits the bundle: the component’s code is fetched only when it is needed. Vue 3 provides defineAsyncComponent() for this, and the build tooling (Vite, webpack) turns each dynamically imported component into a separate chunk.

Key point: defineAsyncComponent() wraps a dynamic import() and returns a component that Vue renders when the chunk arrives. The component is fetched on first render, cached after that, and can be configured with loading, error, delay, and timeout behavior. Lazy loading is a build-time optimization expressed at the component level.


Why async components exist

The bundle-size problem. A single bundle that contains the entire application is downloaded by every user on every visit. The login page pays for the admin dashboard. The landing page pays for the video player. Bundle size grows with the application, and the initial load time grows with it.

The code-splitting problem. Build tools can split a bundle into chunks, but only if the code is structured to allow it. A static import statement tells the bundler that the module is needed at startup. A dynamic import() tells the bundler that the module can be loaded later. defineAsyncComponent() is the wrapper that makes the dynamic import usable as a component.

The route-level problem. Route-based code splitting is the most common pattern. Each route’s component is loaded only when the user navigates to that route. Vue Router supports this directly with dynamic imports, and defineAsyncComponent() provides the same behavior at the component level for cases where the split is finer-grained than a route.

The heavy-component problem. Some components are large and rarely used: a rich text editor, a charting library, a PDF viewer, a code editor. Loading them upfront inflates the bundle for users who never open them. Wrapping them in defineAsyncComponent() defers the cost to the moment they are needed.

The conditional-render problem. A component that renders only when a condition is true, such as a modal that opens on a button click, can be lazy-loaded. The chunk is fetched when the modal first appears, not when the page loads.

The performance-budget problem. Performance budgets are measured in kilobytes of JavaScript. Code splitting is the primary tool for staying within a budget. Every component that is not needed for the initial render is a candidate for defineAsyncComponent().


a. The basics of defineAsyncComponent

defineAsyncComponent() accepts a loader function and returns a component. The loader must return a Promise that resolves to the component definition.

The minimal usage

<script setup>
import { defineAsyncComponent } from 'vue';

const AsyncEditor = defineAsyncComponent(() =>
  import('./components/Editor.vue')
);
</script>

<template>
  <AsyncEditor />
</template>

The import('./components/Editor.vue') is a dynamic import. The bundler splits Editor.vue into a separate chunk. The chunk is fetched when AsyncEditor first renders.

The parent’s perspective

The async component is used like any other component. The parent does not need to know that it is loaded on demand.

<template>
  <AsyncEditor v-model="content" @save="handleSave" />
</template>

Props, events, slots, and refs work the same way. The difference is the timing of the code fetch.

The loading state

The default behavior shows nothing while the chunk is loading. To show a loading indicator, pass an options object with a loadingComponent.

<script setup>
import { defineAsyncComponent } from 'vue';
import LoadingSpinner from './LoadingSpinner.vue';

const AsyncEditor = defineAsyncComponent({
  loader: () => import('./components/Editor.vue'),
  loadingComponent: LoadingSpinner,
  delay: 200,
});
</script>

The delay option postpones the loading component for 200 milliseconds. If the chunk arrives faster, the loading indicator never appears, which avoids a flash of content that immediately disappears.

The error state

If the chunk fails to load, the component renders an errorComponent instead.

<script setup>
import { defineAsyncComponent } from 'vue';
import LoadingSpinner from './LoadingSpinner.vue';
import ErrorDisplay from './ErrorDisplay.vue';

const AsyncEditor = defineAsyncComponent({
  loader: () => import('./components/Editor.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200,
  timeout: 30000,
});
</script>

The timeout option fails the component if the chunk does not load within the specified milliseconds.

The onError callback

The onError option receives the error, the retry function, the fail function, and the number of attempts. It decides whether to retry or fail.

<script setup>
import { defineAsyncComponent } from 'vue';

const AsyncEditor = defineAsyncComponent({
  loader: () => import('./components/Editor.vue'),
  onError(error, retry, fail, attempts) {
    if (attempts <= 3) {
      retry();
    } else {
      fail();
    }
  },
});
</script>

The retry loop is useful for flaky networks. The attempts argument limits the number of retries.

The default timeout

The default timeout is Infinity, which means the component waits forever. In practice, a finite timeout is almost always better, because a chunk that never loads leaves the user with a blank area and no error.


b. Lazy loading patterns

The placement of defineAsyncComponent() determines what is loaded when. The patterns below cover the common cases.

Route-level lazy loading

Vue Router accepts a dynamic import directly as the component for a route. This is the most common form of code splitting.

const routes = [
  {
    path: '/',
    component: () => import('./views/Home.vue'),
  },
  {
    path: '/settings',
    component: () => import('./views/Settings.vue'),
  },
  {
    path: '/admin',
    component: () => import('./views/Admin.vue'),
  },
];

Each route’s component is a separate chunk. The chunk is fetched when the user navigates to the route. The Home chunk is fetched on the first visit, and the Settings and Admin chunks are fetched only when their routes are visited.

Conditional lazy loading

A component that renders only when a condition is true can be lazy-loaded without a route.

<script setup>
import { ref, defineAsyncComponent } from 'vue';

const showEditor = ref(false);
const AsyncEditor = defineAsyncComponent(() => import('./Editor.vue'));
</script>

<template>
  <button @click="showEditor = true">Open editor</button>
  <AsyncEditor v-if="showEditor" />
</template>

The Editor chunk is fetched the first time the button is clicked. Subsequent toggles use the cached chunk.

Interaction-triggered lazy loading

A component can be lazy-loaded before it is rendered, using an event handler that triggers the import.

<script setup>
import { ref } from 'vue';

const AsyncEditor = ref(null);

async function preloadEditor() {
  AsyncEditor.value = (await import('./Editor.vue')).default;
}
</script>

<template>
  <button @mouseenter="preloadEditor" @click="showEditor = true">
    Open editor
  </button>
  <component :is="AsyncEditor" v-if="showEditor" />
</template>

The chunk is fetched when the user hovers the button, so it is ready when they click. This is a common pattern for prefetching on the interaction that is likely to lead to the render.

The Suspense integration

<Suspense> wraps async components and shows a fallback while they load. It replaces the per-component loading component with a single boundary.

<template>
  <Suspense>
    <template #default>
      <AsyncEditor />
    </template>
    <template #fallback>
      <LoadingSpinner />
    </template>
  </Suspense>
</template>

The fallback shows until all async components inside the boundary are resolved. Nested Suspense boundaries allow finer-grained control.

The error boundary

Vue 3 does not have a built-in error boundary component, but the onErrorCaptured hook serves the same purpose. It catches errors from child components, including failed async loads.

<script setup>
import { onErrorCaptured, ref } from 'vue';

const hasError = ref(false);

onErrorCaptured((err) => {
  hasError.value = true;
  return false;
});
</script>

<template>
  <div v-if="hasError">Failed to load component.</div>
  <slot v-else />
</template>

The onErrorCaptured hook is called when a child throws. Returning false stops the propagation.


c. defineAsyncComponent versus other loading patterns

defineAsyncComponent() is one tool among several. The alternatives cover different cases.

Static import

A static import at the top of the script loads the module at build time and includes it in the main bundle. This is the default and is correct for components that are always rendered.

<script setup>
import Editor from './Editor.vue';
</script>

No lazy loading. The component is available immediately, and the bundler does not split it.

Dynamic import without the wrapper

A dynamic import() in a route or in an event handler returns a Promise. The router accepts the Promise directly, and the component is loaded when the route is resolved. This is the same mechanism as defineAsyncComponent(), without the loading, error, and timeout options.

const routes = [
  { path: '/editor', component: () => import('./Editor.vue') },
];

For routes, the router’s own loading behavior is often sufficient, and defineAsyncComponent() is unnecessary.

The import() in a template ref

A component can be imported dynamically and assigned to a ref. The ref is then used with <component :is>. This is more manual than defineAsyncComponent() and is rarely the right choice.

<script setup>
import { ref } from 'vue';

const AsyncEditor = ref(null);

async function load() {
  AsyncEditor.value = (await import('./Editor.vue')).default;
}
</script>

<template>
  <component :is="AsyncEditor" />
</template>

The defineAsyncComponent() wrapper handles the loading and error states, so the manual version is not needed except for unusual cases.

The comparison

PatternLoading UIError UITimeoutUse Case
Static importN/AN/AN/AAlways-rendered components
Router dynamic importRouter defaultRouter defaultNoRoutes
defineAsyncComponentConfigurableConfigurableYesComponents with custom behavior
Manual import() + refNoneNoneNoVery rare

The bundle inspector

The effect of code splitting is visible in the build output. Vite and webpack report each chunk and its size.

npm run build
# dist/assets/Home-abc123.js    12.4 kB
# dist/assets/Settings-def456.js 8.2 kB
# dist/assets/Editor-ghi789.js  142.7 kB

The Editor chunk is the largest and is the best candidate for lazy loading. The Home chunk is small and can be in the main bundle.

The prefetch

Vite generates prefetch hints for chunks that are dynamically imported. The browser fetches them during idle time, so they are ready before the user navigates. The /* @vite-ignore */ comment disables the hint when it is not wanted.

const AsyncEditor = defineAsyncComponent(() =>
  import(/* webpackPrefetch: true */ './Editor.vue')
);

Webpack uses the magic comment. Vite uses import.meta.glob for more control.


Complete Example Session

<!-- ============================================
     PART 1: BASIC ASYNC COMPONENT
     ============================================ -->

<script setup>
import { defineAsyncComponent } from 'vue';

const AsyncEditor = defineAsyncComponent(() =>
  import('./components/Editor.vue')
);
</script>

<template>
  <AsyncEditor />
</template>

<!-- ============================================
     PART 2: WITH LOADING COMPONENT
     ============================================ -->

<script setup>
import { defineAsyncComponent } from 'vue';
import LoadingSpinner from './LoadingSpinner.vue';

const AsyncEditor = defineAsyncComponent({
  loader: () => import('./components/Editor.vue'),
  loadingComponent: LoadingSpinner,
  delay: 200,
});
</script>

<template>
  <AsyncEditor />
</template>

<!-- ============================================
     PART 3: WITH ERROR AND TIMEOUT
     ============================================ -->

<script setup>
import { defineAsyncComponent } from 'vue';
import LoadingSpinner from './LoadingSpinner.vue';
import ErrorDisplay from './ErrorDisplay.vue';

const AsyncEditor = defineAsyncComponent({
  loader: () => import('./components/Editor.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200,
  timeout: 30000,
});
</script>

<template>
  <AsyncEditor />
</template>

<!-- ============================================
     PART 4: WITH RETRY
     ============================================ -->

<script setup>
import { defineAsyncComponent } from 'vue';

const AsyncEditor = defineAsyncComponent({
  loader: () => import('./components/Editor.vue'),
  onError(error, retry, fail, attempts) {
    if (attempts <= 3) retry();
    else fail();
  },
});
</script>

<!-- ============================================
     PART 5: ROUTE-LEVEL LAZY LOADING
     ============================================ -->

<script>
import { createRouter, createWebHistory } from 'vue-router';

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: () => import('./views/Home.vue') },
    { path: '/settings', component: () => import('./views/Settings.vue') },
    { path: '/admin', component: () => import('./views/Admin.vue') },
  ],
});

export default router;
</script>

<!-- ============================================
     PART 6: CONDITIONAL LAZY LOADING
     ============================================ -->

<script setup>
import { ref, defineAsyncComponent } from 'vue';

const showEditor = ref(false);
const AsyncEditor = defineAsyncComponent(() => import('./Editor.vue'));
</script>

<template>
  <button @click="showEditor = true">Open editor</button>
  <AsyncEditor v-if="showEditor" />
</template>

<!-- ============================================
     PART 7: PREFETCH ON HOVER
     ============================================ -->

<script setup>
import { ref } from 'vue';

const AsyncEditor = ref(null);

async function preload() {
  if (!AsyncEditor.value) {
    AsyncEditor.value = (await import('./Editor.vue')).default;
  }
}
</script>

<template>
  <button @mouseenter="preload" @click="showEditor = true">
    Open editor
  </button>
  <component :is="AsyncEditor" v-if="showEditor" />
</template>

<!-- ============================================
     PART 8: SUSPENSE INTEGRATION
     ============================================ -->

<template>
  <Suspense>
    <template #default>
      <AsyncEditor />
    </template>
    <template #fallback>
      <LoadingSpinner />
    </template>
  </Suspense>
</template>

<!-- ============================================
     PART 9: ERROR BOUNDARY WITH onErrorCaptured
     ============================================ -->

<script setup>
import { onErrorCaptured, ref } from 'vue';

const hasError = ref(false);

onErrorCaptured((err) => {
  hasError.value = true;
  return false;
});
</script>

<template>
  <div v-if="hasError">Failed to load component.</div>
  <slot v-else />
</template>

<!-- ============================================
     PART 10: BUILD OUTPUT
     ============================================ -->

<!--
npm run build

dist/assets/index-abc123.js      52.3 kB
dist/assets/Home-def456.js       12.4 kB
dist/assets/Settings-ghi789.js    8.2 kB
dist/assets/Editor-jkl012.js    142.7 kB

The Editor chunk is the largest.
It is loaded only when the Editor component is rendered.
The initial bundle does not include it.
-->

The ten parts showed basic async components, loading and error states, retry, route-level lazy loading, conditional lazy loading, prefetch on hover, Suspense integration, error boundaries, and the build output.


Quick Reference

defineAsyncComponent Options

OptionPurposeDefault
loaderFunction returning a PromiseRequired
loadingComponentShown while loadingNone
errorComponentShown on errorNone
delayMilliseconds before loading component200
timeoutMilliseconds before failingInfinity
suspensibleUse with Suspensetrue
onErrorCustom error handlerNone

Loading Patterns

PatternWhereWhen Loaded
Route-levelRouter configOn navigation
Conditionalv-if in templateWhen condition is true
Prefetch on hoverEvent handlerOn hover
SuspenseBoundary componentWhen rendered
AlwaysStatic importAt startup

Lazy Loading Decision

ComponentLazy?Reason
Home pageNoNeeded for first paint
Login formNoFirst interaction
Admin dashboardYesRarely visited
Rich text editorYesLarge, conditional
Settings panelYesNot on critical path
ModalYesRendered on demand

Build Tool Comments

ToolCommentEffect
webpack/* webpackChunkName: "editor" */Names the chunk
webpack/* webpackPrefetch: true */Prefetches during idle
Viteimport.meta.globBatch imports
Vite/* @vite-ignore */Disables processing

Best Practices

✅ Do This:

<!-- Lazy-load heavy components. -->
const AsyncEditor = defineAsyncComponent(() => import('./Editor.vue'));  // ✅

<!-- Provide a loading component. -->
loadingComponent: LoadingSpinner  // ✅

<!-- Set a finite timeout. -->
timeout: 30000  // ✅

<!-- Use route-level lazy loading. -->
{ path: '/admin', component: () => import('./Admin.vue') }  // ✅

<!-- Retry on transient failures. -->
onError(error, retry, fail, attempts) { if (attempts <= 3) retry(); }  // ✅

<!-- Use Suspense for nested async components. -->
<Suspense><AsyncEditor /></Suspense>  // ✅

<!-- Prefetch on hover for likely interactions. -->
@mouseenter="preload"  // ✅

❌ Don’t Do This:

<!-- Don't lazy-load the initial route. -->
{ path: '/', component: () => import('./Home.vue') }  // ⚠️ Only if needed

<!-- Don't lazy-load tiny components. -->
const AsyncButton = defineAsyncComponent(() => import('./Button.vue'));  // ❌ Overhead

<!-- Don't use an infinite timeout in production. -->
timeout: Infinity  // ❌ Default; set a finite value

<!-- Don't skip the error component. -->
// No errorComponent → silent failure  // ❌

<!-- Don't lazy-load everything. -->
// Each chunk adds a request.  // ❌

<!-- Don't forget the retry logic on flaky networks. -->
// Default behavior fails on first error.  // ❌

Common Pitfalls

PitfallWhy It HappensFix
Loading flashChunk loads faster than delaySet delay to 200ms
Blank area on failureNo error componentProvide errorComponent
Infinite waitDefault timeout is InfinitySet a finite timeout
Chunk not splitStatic import usedUse dynamic import()
Retry loops foreverNo attempt limitCheck attempts
Suspense fallback never showsComponent loads fastExpected; fallback only on slow loads
Type error in parentAsync component not typedUse Component type or infer
Prefetch on every componentPrefetch hints for all chunksOnly prefetch likely next views

Real-World Examples

1. Basic Async

const AsyncEditor = defineAsyncComponent(() => import('./Editor.vue'))

2. With Loading

const Async = defineAsyncComponent({ loader: () => import('./Editor.vue'), loadingComponent: Spinner })

3. With Error

const Async = defineAsyncComponent({ loader: () => import('./Editor.vue'), errorComponent: ErrorDisplay })

4. With Timeout

const Async = defineAsyncComponent({ loader: () => import('./Editor.vue'), timeout: 30000 })

5. With Retry

onError(error, retry, fail, attempts) { if (attempts <= 3) retry(); else fail(); }

6. Route-Level

{ path: '/admin', component: () => import('./Admin.vue') }

7. Conditional

<AsyncEditor v-if="showEditor" />

8. Prefetch on Hover

@mouseenter="preload"

9. Suspense

<Suspense><template #fallback><Spinner /></template><AsyncEditor /></Suspense>

10. Error Boundary

onErrorCaptured((err) => { hasError.value = true; return false; })

Visual

Code Splitting

┌────────────────────────────────────────────────────────────┐
│  WITHOUT LAZY LOADING                                      │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  main.js (entire application)                     │     │
│  │  ┌────────────────────────────────────────────┐   │     │
│  │  │ Home + Settings + Admin + Editor + ...     │   │     │
│  │  │ Size: 850 kB                                │   │     │
│  │  └────────────────────────────────────────────┘   │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  Downloaded on first visit.                                │
│                                                            │
│  ──────────────────────────────────────────────────────    │
│                                                            │
│  WITH LAZY LOADING                                         │
│                                                            │
│  ┌──────────────┐                                          │
│  │ main.js      │ 52 kB   ← Loaded on startup             │
│  └──────────────┘                                          │
│  ┌──────────────┐                                          │
│  │ Home.js      │ 12 kB   ← Loaded when / visited         │
│  └──────────────┘                                          │
│  ┌──────────────┐                                          │
│  │ Editor.js    │ 142 kB  ← Loaded when editor opens      │
│  └──────────────┘                                          │
│  ┌──────────────┐                                          │
│  │ Admin.js     │ 98 kB   ← Loaded when /admin visited    │
│  └──────────────┘                                          │
│                                                            │
│  Initial load: 52 kB instead of 850 kB.                    │
└────────────────────────────────────────────────────────────┘

Async Component Lifecycle

┌────────────────────────────────────────────────────────────┐
│  ASYNC COMPONENT LIFECYCLE                                 │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  1. RENDER TRIGGERED                              │     │
│  │     Component reaches the render phase            │     │
│  └────────────────────────┬─────────────────────────┘     │
│                           │                                │
│                           v                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  2. LOADER CALLED                                 │     │
│  │     import() starts the chunk fetch               │     │
│  └────────────────────────┬─────────────────────────┘     │
│                           │                                │
│                           v                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  3. DELAY PERIOD (default 200ms)                  │     │
│  │     Nothing shown yet                             │     │
│  └────────────────────────┬─────────────────────────┘     │
│                           │                                │
│                           v                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  4. LOADING COMPONENT SHOWN (if still loading)    │     │
│  └────────────────────────┬─────────────────────────┘     │
│                           │                                │
│              ┌────────────┴────────────┐                  │
│              │                         │                  │
│              v                         v                  │
│  ┌──────────────────┐      ┌──────────────────┐          │
│  │ CHUNK LOADS      │      │ TIMEOUT/ERROR    │          │
│  │ Show component   │      │ Show error       │          │
│  └──────────────────┘      └──────────────────┘          │
│                                                            │
└────────────────────────────────────────────────────────────┘

The Interface Decision

┌────────────────────────────────────────────────────────────┐
│  WHEN TO LAZY LOAD                                         │
│                                                            │
│  Component always rendered?                                │
│    → Static import                                         │
│                                                            │
│  Route-level?                                              │
│    → Router dynamic import                                 │
│                                                            │
│  Large and conditional?                                    │
│    → defineAsyncComponent                                  │
│                                                            │
│  Small and conditional?                                    │
│    → Static import (chunk overhead > benefit)              │
│                                                            │
│  Part of a group of related views?                         │
│    → Group into a single chunk                             │
│                                                            │
│  On the critical rendering path?                           │
│    → Static import (do not delay first paint)              │
└────────────────────────────────────────────────────────────┘

Suspense Boundary

┌────────────────────────────────────────────────────────────┐
│  SUSPENSE BOUNDARY                                         │
│                                                            │
│  <Suspense>                                                │
│    ┌────────────────────────────────────────────────┐     │
│    │  #default                                       │     │
│    │    <AsyncEditor />                              │     │
│    │    <AsyncChart />                               │     │
│    │    <AsyncPanel />                               │     │
│    └────────────────────────────────────────────────┘     │
│                                                            │
│    ┌────────────────────────────────────────────────┐     │
│    │  #fallback                                      │     │
│    │    <LoadingSpinner />                           │     │
│    └────────────────────────────────────────────────┘     │
│                                                            │
│  The fallback shows until ALL async components inside      │
│  the boundary have resolved.                               │
│                                                            │
│  Nested Suspense allows finer control:                     │
│  <Suspense>                                                │
│    <Suspense><AsyncA /></Suspense>                         │
│    <Suspense><AsyncB /></Suspense>                         │
│  </Suspense>                                               │
│                                                            │
│  Each inner boundary shows its own fallback.               │
└────────────────────────────────────────────────────────────┘

Summary

ItemValue
defineAsyncComponent()Wraps a dynamic import into a component
LoaderFunction returning a Promise
loadingComponentShown while loading
errorComponentShown on failure
delayMilliseconds before showing the loading component
timeoutMilliseconds before failing
onErrorCustom retry/fail handler
Route-level splittingDynamic import in the router
SuspenseBoundary for async components
onErrorCapturedError boundary hook

Key takeaways:

  • defineAsyncComponent() wraps a dynamic import. The loader function returns a Promise that resolves to the component. The bundler splits the component into a separate chunk, and Vue fetches it on first render.
  • Lazy loading is a build-time optimization. The dynamic import is what signals the bundler. Without it, the component stays in the main bundle.
  • Route-level splitting is the most common pattern. Vue Router accepts dynamic imports directly. Each route’s component is a chunk loaded on navigation.
  • Conditional rendering can also be lazy. A component that renders only when a condition is true is fetched the first time it appears.
  • The delay option prevents loading flashes. Setting delay: 200 postpones the loading component for 200ms. Fast chunks never show it.
  • The default timeout is Infinity. A finite timeout is almost always better, because a chunk that never loads leaves a blank area with no error.
  • onError enables retry logic. The handler receives the retry and fail functions and the attempt count. It decides whether to retry or give up.
  • Suspense replaces per-component loading. The boundary shows a single fallback until all nested async components resolve.
  • The build output shows the effect. Each chunk’s size is reported, and the largest chunks are the best candidates for lazy loading.

Remember: defineAsyncComponent() is the Vue 3 mechanism for loading a component’s code on demand. It wraps a dynamic import, returns a component, and configures the loading, error, delay, and timeout behavior. Route-level lazy loading uses the same dynamic import directly in the router config. The result is a smaller initial bundle, faster first paint, and code that is fetched only when it is needed. The choice of what to lazy-load depends on the rendering path: components that render on first paint should be static, and components that render conditionally or on navigation are candidates for splitting. The build output reports each chunk, and the largest chunks are the best candidates. Use loadingComponent and errorComponent to make the loading experience clean, set a finite timeout, and add retry logic for transient failures. Lazy loading is a performance optimization that costs nothing at runtime once the chunk is cached and pays for itself on every visit.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!