Hook — 06 : 08 a.m., Bogotá ↔ Playa Venao deploy watch
A morning storm rattled my apartment in Colombia just as María, on Panama’s Pacific coast, pinged: “Largest bundle jumped to 1.2 MB—users on 3 G bail before first paint.” We traced it to an eager-loaded charting library lurking behind a tab. In forty minutes we shipped route-level lazy-loading, wrapped it with<Suspense>
, and split vendor chunks. Lighthouse time-to-interactive dropped 40 %, and María still caught the early waves. That surf-save frames today’s deep dive into squeezing every kilobyte out of Vue 3 apps—without sweating over arcane build configs.
Why Performance Tuning Matters Today
- Core Web Vitals (LCP, INP, CLS) drive SEO and ad revenue.
- Mobile users in Latin America juggle spotty 3 G and prepaid data caps.
- Lighthouse audits are now common in enterprise procurement checklists.
- Every extra second of load time costs conversions—and your caffeine budget.
Vue 3, Vite, and modern browsers give us three super-powers: lazy-loading, Suspense, and automatic code-splitting. Master them once, slash bundle sizes forever.
Lazy-Loading Components the Idiomatic Way
The Gist
vueCopyEdit<script setup lang="ts">
const Chart = defineAsyncComponent(() => import('@/components/BigChart.vue'));
</script>
<template>
<button @click="show = !show">Toggle Chart</button>
<Chart v-if="show" />
</template>
import()
returns a Promise; Vue renders the component only when needed. Vite splits a distinct chunk BigChart.[hash].js
.
Hands-On Example: Route-Level Splitting
tsCopyEdit// router/index.ts
const routes = [
{
path: '/reports',
component: () => import('@/views/Reports.vue'), // lazy chunk
},
];
Navigate to /reports
and Vite fetches the chunk on-demand. No extra config—just keep dynamic imports arrow-wrapped.
Suspense: Handling Pending States Like a Pro
<Suspense>
renders a fallback until all async dependencies of its subtree resolve.
vueCopyEdit<script setup>
import { defineAsyncComponent } from 'vue';
const UserCard = defineAsyncComponent({
loader: () => import('@/components/UserCard.vue'),
delay: 200, // ms before showing fallback
});
</script>
<template>
<Suspense>
<template #default>
<UserCard :id="42" />
</template>
<template #fallback>
<SkeletonCard />
</template>
</Suspense>
</template>
Perfect for dashboard skeletons or SEO-safe SSR streams.
Remote-Work Insight Box 🌎
During a sprint in Costa Rica, flaky hotel Wi-Fi caused white screens when API calls hung. Wrapping data-driven widgets inside <Suspense>
with a timeout fallback ensured users always saw spinners instead of blank gaps—saving face with investors viewing the demo from Mexico City.
Code Splitting Deep-Dive
Split Type | How to Achieve | Ideal Use |
---|---|---|
Dynamic import | () => import(...) | Route & modal components |
Vendor splitting | Vite auto-splits; inspect vite.config.ts for build.rollupOptions | Large libs like chart.js , xlsx |
CSS splitting | Vite extracts CSS per chunk | Route-scoped styles |
Prefetch / Preload | <link rel="prefetch"> via import(/* webpackPrefetch: true */) in Vite too | Next likely route |
Use npm run build -- --report
and open the bundle visualizer to hunt whales.
Performance & Accessibility Checkpoints
- LCP < 2.5 s—inline critical hero content; lazy-load below-fold images with
loading="lazy"
. - INP < 200 ms—code-split heavy interaction logic (rich editors, charts).
- CLS < 0.1—reserve space for fallbacks; don’t push layout when chunks arrive.
- Screen readers—announce Suspense fallbacks with
role="status"
.
Common Pitfalls & Quick Fixes
Pitfall | Symptom | Fix |
---|---|---|
Dynamic import inside setup() only | Chunk loads even if component never shown | Wrap in route or conditional render |
Waterfall of chunks | Multiple nested async components | Use prefetch plugin or group via /* webpackChunkName: "charts" */ comment |
Flash of unstyled content | CSS chunk loads after HTML | Enable build.cssCodeSplit (default true) & inline critical CSS |
Suspense never resolves | Promise rejected silently | Provide onErrorCaptured or return fallback UI on error |
Call-Out Reference Table
CLI / Concept | One-liner Purpose |
---|---|
defineAsyncComponent | Wrap a dynamic import with retry / delay |
<Suspense> | Await async setup + lazy children |
vite build --report | Visualize chunks |
vite-plugin-imp | Auto-import on-demand components |
import.meta.glob | Build-time code generator for lazy routes |
Diagram Snapshot (text)
Route click → Browser fetches Reports.[hash].js
+ Reports.css
in parallel → <Suspense>
shows skeleton → chunk executes, API resolves → Suspense switches to content → INP remains < 200 ms.
Wrap-Up
Vue 3’s async arsenal—dynamic import()
, <Suspense>
, and Vite’s rollup magic—lets you ship silky interactions without shipping megabyte bundles. Audit with Lighthouse, visualize chunks, and slice anything not needed at first paint. Your users on prepaid data plans from Santo Domingo to Florianópolis will feel the difference, and your Core Web Vitals will thank you.
Drop your own chunk-splitting triumphs or questions below; I’ll respond between flights and late-night arepa sessions.