# Frontend Performance Profiling — Vue 3 + Vite + PWA ## Stack Context | Layer | Tool | Version | |---|---|---| | Framework | Vue 3 (SFC + Composition API) | ^3.5 | | State | Pinia | ^3.0 | | Router | Vue Router | ^5.0 | | Bundler | Vite | ^7.3 | | CSS | Tailwind CSS (Vite plugin) | ^4.2 | | PWA | vite-plugin-pwa (Workbox) | ^1.2 | | Backend client | Supabase JS | ^2.99 | | Build | vue-tsc + vite build | — | | Tests | Vitest + jsdom | ^4.1 | --- ## Profiling Toolchain ### Build-Time Analysis | Tool | Purpose | Command | |---|---|---| | Vite bundle visualizer | Chunk sizes, tree-shaking effectiveness | `npx vite-bundle-visualizer` (generates `stats.html`) | | `vite build --report` | Build timing and chunk summary | `npx vite build 2>&1` | | `source-map-explorer` | Trace bytes to source files | `npx source-map-explorer dist/assets/*.js` | | `bundlephobia` | Check dependency size before adding | `npx bundlephobia ` | **Quick bundle size check (no extra dependency needed):** ```bash # Build and report chunk sizes cd apps/frontend && npx vite build 2>&1 | grep -E "(dist/|\.js|\.css)" | sort -t'│' -k2 -rn ``` **Detailed analysis with rollup-plugin-visualizer:** ```bash # Install temporarily and run cd apps/frontend && npx vite-bundle-visualizer # Opens stats.html in browser — shows treemap of every module ``` ### Runtime Analysis (Chrome DevTools) | Tab | What it measures | When to use | |---|---|---| | **Performance** | Frame rate, scripting, rendering, painting | Janky animations, slow interactions | | **Lighthouse** | LCP, FID/INP, CLS, TTI, bundle size | Overall page health score | | **Network** | Waterfall, asset sizes, cache status | Slow loads, unnecessary requests | | **Memory** | Heap snapshots, allocation timeline | Memory leaks, growing detached DOM | | **Coverage** | Used vs unused JS/CSS bytes | Dead code, over-imported libraries | ### Core Web Vitals | Metric | Target | What it measures | |---|---|---| | **LCP** (Largest Contentful Paint) | < 2.5s | How fast the main content appears | | **INP** (Interaction to Next Paint) | < 200ms | How fast interactions respond | | **CLS** (Cumulative Layout Shift) | < 0.1 | How much the layout shifts during load | | **TTFB** (Time to First Byte) | < 800ms | Server response time | | **FCP** (First Contentful Paint) | < 1.8s | How fast anything renders | ### Lighthouse CLI (Headless — for Scripted Analysis) ```bash # Run Lighthouse from CLI (no browser GUI needed) npx lighthouse http://localhost:4173 \ --output=json \ --output-path=./lighthouse-report.json \ --chrome-flags="--headless --no-sandbox" \ --only-categories=performance \ --preset=desktop # For mobile (default): npx lighthouse http://localhost:4173 \ --output=json \ --output-path=./lighthouse-report.json \ --chrome-flags="--headless --no-sandbox" \ --only-categories=performance ``` **Agent-readable extraction:** ```bash # Extract key metrics from Lighthouse JSON cat lighthouse-report.json | node -e " const r = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')); const a = r.audits; console.log('| Metric | Score | Value |'); console.log('|---|---|---|'); ['first-contentful-paint','largest-contentful-paint','total-blocking-time', 'cumulative-layout-shift','speed-index','interactive'].forEach(k => { if(a[k]) console.log('| ' + a[k].title + ' | ' + (a[k].score*100) + ' | ' + a[k].displayValue + ' |'); }); " ``` --- ## Vue/Vite-Specific Optimization Patterns ### Pattern: Vendor Chunk Splitting (Vite) **Symptom:** Every deploy invalidates the entire JS bundle cache, including stable vendor libs (Vue, Supabase JS). See agnostic principle: *Artifact Partitioning by Change Frequency* in SKILL.md. **Fix:** Add `manualChunks` to `vite.config.ts`: ```typescript build: { rollupOptions: { output: { manualChunks: { 'vendor-vue': ['vue', 'vue-router', 'pinia'], 'vendor-supabase': ['@supabase/supabase-js'], }, }, }, }, ``` **Group by change frequency:** - `vendor-vue`: framework runtime — changes only on major upgrades - `vendor-supabase`: SDK/client — changes infrequently - App code (auto): changes every deploy **Verification:** After `vite build`, check `dist/assets/` for `vendor-vue-*.js` and `vendor-supabase-*.js` chunks. ### Pattern: Resource Hint Pipeline (HTML) **Symptom:** Fonts load late because browser discovers them only after parsing CSS. See agnostic principle: *Dependency Discovery Parallelization* in SKILL.md. **Fix:** Replace CSS `@import` with HTML `` tags and add `preconnect` hints: ```html ``` Then remove the `@import url(...)` from your CSS file. **Safety:** Only `preconnect` to domains used on every page load. For runtime-determined domains (e.g., Supabase URL from env vars), skip the hint — hardcoding would break across environments. ### Pattern: Route-Level Code Splitting **Symptom:** Lighthouse reports a large initial JS bundle with low Coverage score. All routes are in a single chunk. **Fix:** Use dynamic imports for route components in Vue Router: ```typescript // ❌ Eager import — everything in one chunk import SiraatView from '@/views/SiraatView.vue' // ✅ Lazy route — separate chunk, loaded on navigation const SiraatView = () => import('@/views/SiraatView.vue') ``` **Verification:** After build, check `dist/assets/` — each lazy route should produce a separate `.js` chunk. ### Pattern: Component Lazy Loading **Symptom:** Heavy components (charts, editors, modals) are loaded even when not visible. **Fix:** Use `defineAsyncComponent` for below-the-fold or conditionally-rendered components: ```typescript import { defineAsyncComponent } from 'vue' const HeavyChart = defineAsyncComponent(() => import('@/features/analytics/HeavyChart.vue') ) ``` ### Pattern: Pinia Store Hydration **Symptom:** Multiple API calls for the same data on page load. Network waterfall shows redundant fetches. **Fix:** Deduplicate fetches in Pinia stores with loading-state guards: ```typescript const store = defineStore('lesson', () => { const data = ref(null) const loading = ref(false) async function fetch(id: string) { if (loading.value) return // ← prevent double-fetch if (data.value?.id === id) return // ← already have it loading.value = true try { data.value = await api.getLesson(id) } finally { loading.value = false } } return { data, loading, fetch } }) ``` ### Pattern: Image Optimization **Symptom:** Lighthouse flags large images (LCP degradation). Network shows uncompressed PNGs. **Fix checklist:** 1. Use `` for below-the-fold images 2. Use WebP/AVIF formats (Vite handles this with plugins) 3. Specify explicit `width` and `height` attributes (prevents CLS) 4. Use `` with `srcset` for responsive images ### Pattern: Watchers / Computed Overuse **Symptom:** DevTools Performance tab shows repeated "Scripting" blocks during reactivity updates. **Fix:** - Prefer `computed` over `watch` — computed values are lazy and cached - Use `watchEffect` only when side effects are truly needed - Use `shallowRef` / `shallowReactive` for large objects that don't need deep reactivity ```typescript // ❌ Deep reactive on a 1000-item array — Vue tracks every nested property const items = ref([]) // ✅ Shallow — Vue only tracks the array reference, not individual items const items = shallowRef([]) ``` ### Pattern: Virtual Scrolling for Long Lists **Symptom:** Rendering > 100 DOM nodes causes jank. DevTools shows long "Rendering" and "Painting" phases. **Fix:** Use virtual scrolling (only render visible items): ```bash npm install @tanstack/vue-virtual ``` ### Pattern: PWA Cache Strategy Audit **Symptom:** Repeat visits are slow despite service worker. Network tab shows cache misses. **Audit checklist:** 1. Verify `navigator.serviceWorker.controller` is active (DevTools → Application → Service Workers) 2. Check `Cache Storage` in DevTools — are the expected caches populated? 3. Verify `runtimeCaching` patterns in `vite.config.ts` match actual API URL patterns 4. Check `networkTimeoutSeconds` — too high means slow fallback to cache ### Pattern: Tailwind CSS Purging **Symptom:** Large CSS bundle despite using Tailwind. Build output shows > 50KB CSS. **Fix:** Tailwind v4 (your version) uses automatic content detection. Verify: 1. No `@import` of the full Tailwind CSS build in production 2. The `@tailwindcss/vite` plugin is used (it is — confirmed in your config) 3. No dynamic class names that prevent tree-shaking (e.g., `class={`text-${color}-500`}`) --- ## Vue/Vite-Specific Anti-Patterns 1. **Don't eagerly import all routes.** This defeats Vite's code splitting. Always use dynamic `() => import()` for route components. 2. **Don't use `v-if` + `v-for` on the same element.** `v-if` has higher priority in Vue 3 — use a `