How We Optimized a Production Vue PWA Without Changing a Single Pixel

Share
How We Optimized a Production Vue PWA Without Changing a Single Pixel
Optimized a Production Vue PWA Without Changing a Single Pixel

A collaborative work-management Vue 3 PWA — tasks, folders, chat, offline storage — got a performance-only release: same UI, faster loads, cleaner bundles. No redesign, no new features.


Where the weight was

Two chunks dominated the production build:

Chunk Gzip (before) Problem
Entry index 862 KB Layout, Firebase, i18n, ui-kit — all eager
MainFolderView 466 KB Table, Kanban, Gantt, Calendar, Chat in one file

Route-level lazy loading existed, but three patterns broke it:

  1. PanelLayout static-imported → full app shell in entry
  2. MainFolderView sync-imported in folder routes
  3. All view modes compiled together despite v-if showing one at a time

PWA install precache mirrored this: ~6.3 MB including routes users might never open.


What we changed

Code splitting

  • Lazy PanelLayout — dynamic import in router parent
  • Lazy MainFolderViewdefineAsyncComponent in folder views
  • Split view modes — async Table, Kanban, Card, Calendar, Dashboard, Gantt, Chat, Activity
  • Deferred Firebase — dynamic import in onMounted; skip on HTTP when push is disabled

Vendor chunks (manualChunks)

Split Vue, Firebase, Dexie, ApexCharts, emoji-mart, i18n, ui-kit, and icons into separate cacheable chunks.

PWA precache

Before After
Install precache ~6.3 MB 3.3 MB (−48%)
Lazy route chunks Precached on install Excluded; runtime cache on first use
maximumFileSizeToCacheInBytes 50 MB 2 MB

Offline shell unchanged — entry, vendors, layout, CSS still precached.

IndexedDB (Dexie)

No UI changes; faster inbox on large accounts:

  • Added isInChatList and chatId indexes (schema v2, auto-migrates)
  • Fixed query builder: IN filter and count() no longer scan full tables
  • Reduced socket watcher churn on chat/notification stores

Shared libraries

  • Analytics — ~6 KB main thread plugin; Dexie/flush logic in a Web Worker; single Dexie instance via build externals
  • UI kit — icons externalized in lib build; stylesheet MD5 verified unchanged vs baseline

Dependencies

Removed unused recordrtc and video.js; patched firebase, vite, dexie, axios. npm audit: 27 → 0.

Admin console

Same manualChunks pattern — entry gzip 41 KB, Vuetify 98 KB, Vue 44 KB. No theme changes.


Results

Metric Before After Change
Entry index gzip 862 KB 177 KB −79%
MainFolderView gzip 466 KB 71 KB −85%
PWA install precache ~6.3 MB 3.3 MB −48%
npm audit 27 0 Fixed
UI kit CSS / icons MD5 baseline identical Parity ✓

User journey: login downloads ~79% less JS; opening a folder loads a thin shell first; Kanban/Gantt fetch on demand; PWA install is half the size.

Trade-off: dist/ total slightly larger (more chunk files) — intentional. Critical path bytes down; vendors cached separately.


Lessons

  1. Route lazy loading is not enough — sync-imported layout or parent SFC still creates a monolith entry.
  2. One SFC per “mode” is an anti-pattern — split with defineAsyncComponent even when only one view is visible.
  3. PWA precache is a budget — exclude lazy routes; cache them at runtime.
  4. IndexedDB needs indexes — missing indexes become silent full scans.
  5. Parity gates enable aggression — MD5 checks on CSS/icons let you refactor chunks without visual drift.

What we skipped

Tailwind 4, Vite 8, Vue Router 5, locale lazy-split, backend query tuning, mobile bundle work — all higher risk or separate tracks.


Summary

A mature Vue PWA can cut ~79% of entry JavaScript (gzip) and ~85% of its heaviest workspace chunk without touching the design. Same product, stronger load performance.

June 2026