[autoresearch] baseline: system prompt quality tests (5/5 passing)
This commit is contained in:
1 parent
05cc036f6d
commit
60b0101717
142 files changed
+18589
-9947
No files matched your search
@@ -0,0 +1,227 @@
|
||||
---
|
||||
name: perf-optimization
|
||||
description: >
|
||||
Profile-driven performance optimization protocol. Use when profiling data (CPU, heap, trace)
|
||||
is available or when the user requests performance analysis. Covers methodology, pattern catalog,
|
||||
safety invariants, and when-to-stop heuristics. Language-specific tooling is in languages/*.md.
|
||||
---
|
||||
|
||||
# Performance Optimization Skill
|
||||
|
||||
## When to Use
|
||||
|
||||
- User provides profiling data (pprof, flamegraph, py-spy, Chrome DevTools, Dart DevTools)
|
||||
- User asks to analyze or optimize performance of a specific component
|
||||
- A benchmark regression is detected
|
||||
- After deploying a new feature that touches a hot path
|
||||
|
||||
## Core Methodology
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
P1[Profile] --> P2[Analyze]
|
||||
P2 --> P3[Prioritize]
|
||||
P3 --> P4[Optimize]
|
||||
P4 --> P5[Benchmark]
|
||||
P5 --> P6{Improvement?}
|
||||
P6 -->|Yes| P7[Verify & Ship]
|
||||
P6 -->|No| P3
|
||||
```
|
||||
|
||||
### Step 1: Profile
|
||||
|
||||
Collect profiling data using the language-appropriate tool. Load the relevant `languages/*.md` module for exact commands.
|
||||
|
||||
**Output:** Raw profiling data (CPU profile, heap profile, or trace).
|
||||
|
||||
### Step 2: Analyze
|
||||
|
||||
Read the profile. Focus on these principles (universal across all runtimes):
|
||||
|
||||
1. **Focus on `cum` (cumulative):** The total resources consumed by a function AND everything it called. This finds the expensive architectural flows.
|
||||
2. **Contextualize `flat`:** Resources consumed by the function itself only. If a runtime function (GC, malloc, syscall) has high flat time, trace it UP the call chain to find the user-land code that triggered it.
|
||||
3. **Ignore runtime noise:** Scheduler overhead (`runtime.mcall`, `runtime.systemstack`, GC workers) will always appear. Note if GC pressure is high, but don't try to "fix" the scheduler.
|
||||
4. **Separate benchmark artifacts from production cost:** Test harness allocations (e.g., `httptest.NewRequest`, `ResponseRecorder`) inflate heap profiles but don't exist in production.
|
||||
|
||||
**Output:** Structured analysis document in `docs/research_logs/{component}-perf-analysis.md`.
|
||||
|
||||
### Step 3: Prioritize
|
||||
|
||||
Rank fixes by **impact/risk ratio**:
|
||||
|
||||
| Priority | Criteria |
|
||||
|---|---|
|
||||
| Do first | Low risk, high impact (caching, pre-allocation, fast-reject) |
|
||||
| Do second | Medium risk, high impact (library swap, algorithm change) |
|
||||
| Do last | High risk, high impact (major refactor, custom implementation) |
|
||||
| Skip | Any risk, low impact (micro-optimization below noise floor) |
|
||||
|
||||
**Rule:** If a fix requires more than 1 day AND saves < 20% on the hot path, defer it.
|
||||
|
||||
### Step 4: Optimize
|
||||
|
||||
Implement one fix at a time. For each fix:
|
||||
1. Write tests FIRST (TDD — Red → Green → Refactor)
|
||||
2. Implement the fix
|
||||
3. Run all existing tests to verify no regression
|
||||
4. Benchmark immediately
|
||||
|
||||
**Never batch multiple optimizations into one commit.** Each fix must be independently verifiable and revertable.
|
||||
|
||||
### Step 5: Benchmark
|
||||
|
||||
Compare before/after with the **exact same benchmark configuration** (same `-benchtime`, same `-count`, same machine load). Report:
|
||||
- `ns/op` (latency)
|
||||
- `B/op` (memory per operation)
|
||||
- `allocs/op` (heap allocations per operation)
|
||||
|
||||
### Step 6: When to Stop
|
||||
|
||||
**Stop optimizing when any of these are true:**
|
||||
- Remaining CPU is in hardware-optimized assembly (AES-NI, P-256, SIMD) — you cannot beat the hardware
|
||||
- Remaining allocations are from the language runtime itself (GC, goroutine stacks, HTTP server internals)
|
||||
- The fix requires a custom implementation of a well-audited library — the security/maintenance risk outweighs the perf gain
|
||||
- The measured improvement is < 5% and within benchmark noise
|
||||
|
||||
---
|
||||
|
||||
## Optimization Pattern Catalog
|
||||
|
||||
These are generic, language-agnostic patterns. Apply them when the profiling data shows the corresponding symptom.
|
||||
|
||||
### Pattern: Result Caching
|
||||
|
||||
**Symptom:** Same expensive computation repeated with identical inputs (crypto verification, JSON parsing, regex compilation).
|
||||
|
||||
**Fix:** Cache results keyed by input hash. Use bounded LRU with TTL to prevent memory exhaustion.
|
||||
|
||||
**Safety invariant:** When caching security-sensitive results (auth tokens, permission checks):
|
||||
- ALWAYS re-validate expiry/revocation on cache hit
|
||||
- ALWAYS bound cache size (DoS protection)
|
||||
- ALWAYS set TTL shorter than the security credential's validity period
|
||||
|
||||
### Pattern: Pre-allocation
|
||||
|
||||
**Symptom:** High `allocs/op` from repeatedly constructing the same objects (option structs, config slices, header maps).
|
||||
|
||||
**Fix:** Build the object once at init time, share it read-only across requests. Safe for concurrent use if the object is immutable after construction.
|
||||
|
||||
### Pattern: Fast-Reject / Short-Circuit
|
||||
|
||||
**Symptom:** Expensive validation path runs even for clearly invalid inputs.
|
||||
|
||||
**Fix:** Add a cheap structural pre-check before the expensive path. Examples: check string length before regex, count delimiters before parsing, check content-type before deserialization.
|
||||
|
||||
### Pattern: Library Swap
|
||||
|
||||
**Symptom:** High allocation count or CPU in a third-party library's internal parsing/serialization.
|
||||
|
||||
**Fix:** Replace with a library that uses lower-allocation strategies (manual scanners vs `encoding/json.Decoder`, zero-copy parsing, arena allocation).
|
||||
|
||||
**Safety invariant:** When swapping security-critical libraries (JWT, TLS, crypto):
|
||||
- Explicitly restrict accepted algorithms (prevent algorithm confusion attacks)
|
||||
- Verify the replacement library is well-audited and actively maintained
|
||||
- Run the full existing test suite — no behavioral change allowed
|
||||
|
||||
### Pattern: Pooling
|
||||
|
||||
**Symptom:** High GC pressure from many short-lived objects of the same type being allocated and discarded rapidly.
|
||||
|
||||
**Fix:** Use an object pool (sync.Pool in Go, object pool in Java, arena in Rust) to reuse allocations.
|
||||
|
||||
**Caveat:** Only effective when objects are uniform in size and have a clear acquire/release lifecycle. Misuse creates subtle bugs.
|
||||
|
||||
### Pattern: Batching
|
||||
|
||||
**Symptom:** Many small I/O operations (DB queries, HTTP calls, file writes) dominating wall-clock time.
|
||||
|
||||
**Fix:** Batch operations into fewer, larger calls. Examples: batch INSERT, pipeline Redis commands, buffer writes.
|
||||
|
||||
### Pattern: Artifact Partitioning by Change Frequency
|
||||
|
||||
**Symptom:** Deploying a small change invalidates a large cached artifact (JS bundle, Docker image, compiled binary), forcing consumers to re-download/rebuild the entire thing.
|
||||
|
||||
**Fix:** Partition build artifacts by change frequency so that stable layers survive volatile deploys:
|
||||
- **Stable layer**: dependencies, vendor libraries, base images — changes rarely
|
||||
- **Volatile layer**: application code — changes on every deploy
|
||||
|
||||
**Examples across stacks:**
|
||||
- **JS/Bundler**: Vite `manualChunks` / Webpack `splitChunks` to isolate vendor libraries into separate chunks
|
||||
- **Docker**: multi-stage builds with `COPY go.mod` + `RUN go mod download` BEFORE `COPY . .` — dependency layer caches across builds
|
||||
- **Monorepo**: separate packages by change frequency so CI only rebuilds what changed
|
||||
|
||||
**Safety invariant:** Total artifact size stays the same or slightly increases (chunk overhead). The benefit is on **repeat consumption** — stable layers serve from cache.
|
||||
|
||||
**When NOT to apply:** One-shot artifacts with no caching benefit (single-use CI, ephemeral environments).
|
||||
|
||||
### Pattern: Dependency Discovery Parallelization
|
||||
|
||||
**Symptom:** Sequential resource discovery creates waterfalls — each resource is discovered only after the previous one completes (download → parse → discover next → download → ...).
|
||||
|
||||
**Fix:** Declare dependencies as early as possible so the system can fetch them in parallel:
|
||||
- Move resource declarations upstream (earlier in the boot/parse sequence)
|
||||
- Use explicit hints to bypass sequential discovery chains
|
||||
|
||||
**Examples across stacks:**
|
||||
- **Browser**: `<link rel="preconnect">` to establish connections before CSS/JS requests them; move CSS `@import` to HTML `<link>` for parallel discovery
|
||||
- **Go**: `go mod download` before build to prefetch modules
|
||||
- **DB**: connection pool warm-up at startup instead of on first query
|
||||
- **DNS**: `dns-prefetch` hints for domains the app will contact
|
||||
|
||||
**Safety invariant:** Only pre-declare resources you WILL use. Unused preconnects/prefetches waste resources (TCP connections, DNS queries, module downloads).
|
||||
|
||||
### Pattern: Concurrent-Fetch Dedup
|
||||
|
||||
**Symptom:** Network tab shows two identical API calls fired at the same time. Multiple UI components mount simultaneously and each independently calls the same fetch function.
|
||||
|
||||
**Fix:** Add a loading-state guard (semaphore) at the store/service layer:
|
||||
|
||||
```
|
||||
async function fetchData() {
|
||||
if (isLoading) return // ← drop duplicate in-flight request
|
||||
isLoading = true
|
||||
try { data = await api.getData() }
|
||||
finally { isLoading = false }
|
||||
}
|
||||
```
|
||||
|
||||
**When to apply:** When the same data store is used by multiple co-mounted components (e.g., a navigation bar and a page view both calling `fetchProfile()` on mount).
|
||||
|
||||
**Caveat:** This is a simple semaphore, not request dedup. If the data needs refreshing after the in-flight call completes, the caller should retry. For advanced use cases, consider a proper request dedup cache (e.g., TanStack Query's `staleTime`).
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns (Things NOT to Do)
|
||||
|
||||
1. **Don't optimize runtime internals.** If `runtime.mallocgc` or `runtime.gcBgMarkWorker` is high, fix the USER CODE that triggers allocations — don't try to tune the GC directly.
|
||||
2. **Don't replace battle-tested crypto with custom implementations.** The performance ceiling of ECDSA/RSA is in the math. Accept it.
|
||||
3. **Don't optimize based on gut feeling.** Always profile first. Premature optimization is the root of all evil.
|
||||
4. **Don't combine multiple optimizations into one commit.** If a combined commit causes a regression, you can't isolate which fix is at fault.
|
||||
5. **Don't disable security features for performance.** Algorithm restriction, input validation, and expiry checks are non-negotiable.
|
||||
6. **Don't profile without a stable baseline.** Run benchmarks with fixed parameters (`-benchtime`, `-count`, same machine load). Without a reproducible baseline, before/after comparisons are meaningless noise.
|
||||
|
||||
---
|
||||
|
||||
## Language Modules
|
||||
|
||||
Load the relevant language module when working with a specific runtime:
|
||||
|
||||
| Module | Use when |
|
||||
|---|---|
|
||||
| [Go](languages/go.md) | Go services, APIs, CLI tools |
|
||||
| [Rust](languages/rust.md) | Rust binaries, libraries |
|
||||
| [Python](languages/python.md) | Python services, CLI, data pipelines |
|
||||
| [Frontend](languages/frontend.md) | Web frontends (JS/TS bundle, rendering, network) |
|
||||
|
||||
> **Contributing:** After completing a perf optimization session, extract generalizable patterns
|
||||
> from your `docs/research_logs/` findings into this catalog. Project-specific details stay in
|
||||
> the research log; reusable patterns belong here.
|
||||
|
||||
## Profiling Scripts
|
||||
|
||||
Language-specific data extraction scripts live in `scripts/`:
|
||||
|
||||
| Script | Purpose |
|
||||
|---|---|
|
||||
| [go-pprof.sh](scripts/go-pprof.sh) | Extract Go pprof CPU/heap profiles into agent-readable markdown |
|
||||
| [frontend-lighthouse.sh](scripts/frontend-lighthouse.sh) | Two modes: `lighthouse` (Core Web Vitals, needs Chrome) or `bundle` (Vite chunk analysis, always works) |
|
||||
@@ -0,0 +1,347 @@
|
||||
# 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 <package-name>` |
|
||||
|
||||
**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 `<link>` tags and add `preconnect` hints:
|
||||
|
||||
```html
|
||||
<!-- In index.html <head> — BEFORE the CSS link -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=...&display=swap">
|
||||
```
|
||||
|
||||
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<Lesson | null>(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 `<img loading="lazy">` 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 `<picture>` 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<Item[]>([])
|
||||
|
||||
// ✅ Shallow — Vue only tracks the array reference, not individual items
|
||||
const items = shallowRef<Item[]>([])
|
||||
```
|
||||
|
||||
### 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 `<template>` wrapper.
|
||||
3. **Don't deep-watch large arrays.** Use `shallowRef` or `watch(() => arr.length)` to avoid O(n) reactivity tracking.
|
||||
4. **Don't ignore `key` in `v-for`.** Missing keys force Vue to re-render entire lists instead of patching individual items.
|
||||
5. **Don't inline large SVGs when they could be `<img>` references.** Inline SVGs increase HTML parse time and bundle size.
|
||||
6. **Don't load Supabase JS in every component.** Import the singleton client from `supabaseClient.ts` — avoid creating multiple GoTrue instances.
|
||||
7. **Don't use plain functions for reactive template expressions.** If `v-if="isVisible()"` calls a function, Vue re-executes it on every render cycle. Use `computed` instead — Vue caches the result and only re-runs when dependencies change.
|
||||
|
||||
---
|
||||
|
||||
## Irreducible Floors in Vue/Vite/Browser
|
||||
|
||||
| Cost | What it is | Why it's irreducible |
|
||||
|---|---|---|
|
||||
| Vue runtime | Reactivity system, virtual DOM diffing | Framework overhead — ~30KB min gzipped |
|
||||
| Supabase JS | GoTrue + PostgREST client | Auth + API client — ~40KB min gzipped |
|
||||
| TLS handshake | HTTPS connection setup | Network latency — mitigated by HTTP/2, preconnect |
|
||||
| Font loading | WOFF2 download + layout shift | Use `font-display: swap` and preload critical fonts |
|
||||
| Service Worker registration | SW parse + install on first visit | One-time cost per SW update |
|
||||
| DNS resolution | Supabase API domain lookup | Use `<link rel="dns-prefetch">` |
|
||||
|
||||
---
|
||||
|
||||
## Data Extraction Scripts
|
||||
|
||||
Use the scripts in `scripts/` for automated profiling:
|
||||
|
||||
```bash
|
||||
# Always-works bundle analysis (no Chrome needed) — start here
|
||||
scripts/frontend-lighthouse.sh bundle ./apps/frontend
|
||||
|
||||
# Lighthouse Core Web Vitals (needs Chrome/Chromium)
|
||||
scripts/frontend-lighthouse.sh lighthouse http://localhost:4173 desktop
|
||||
|
||||
# With custom timeout (default: 120s)
|
||||
LIGHTHOUSE_TIMEOUT=60 scripts/frontend-lighthouse.sh lighthouse http://localhost:4173
|
||||
```
|
||||
|
||||
> **Agent tip:** If Lighthouse hangs (common in headless/CI environments), the script
|
||||
> auto-kills after the timeout and suggests bundle mode. Always start with `bundle` mode
|
||||
> since it works everywhere and provides the most actionable data (chunk sizes, vendor
|
||||
> splitting status, oversized chunk flags).
|
||||
|
||||
## Recommended Profiling Workflow
|
||||
|
||||
1. **Bundle analysis first:** Run `scripts/frontend-lighthouse.sh bundle ./apps/frontend` to check chunk sizes
|
||||
2. **Lighthouse desktop + mobile:** Run `scripts/frontend-lighthouse.sh lighthouse <url> desktop` and default (mobile) — compare scores
|
||||
3. **Runtime profiling:** Use Chrome DevTools Performance tab to record user flows (navigation, exercise completion)
|
||||
4. **Memory:** Take heap snapshots before and after a user flow — diff for leaks (detached DOM, event listeners)
|
||||
5. **Coverage:** Run Coverage tool — identify dead JS/CSS shipped to the client
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Post-Optimization Clean Sweep Checklist
|
||||
|
||||
After applying the main fixes, run this checklist to catch leftover issues.
|
||||
This was codified from the FATH frontend session (2026-03-23).
|
||||
|
||||
### Template Reactivity
|
||||
- [ ] All `v-if` / `v-show` conditions use `computed` refs, not plain function calls
|
||||
- [ ] `v-for` lists use stable `:key` values (not array index)
|
||||
- [ ] No plain functions in template expressions that could be `computed`
|
||||
|
||||
### Component Loading
|
||||
- [ ] All routes use `() => import()` (lazy routes)
|
||||
- [ ] Modals, drawers, and sheets use `defineAsyncComponent` (they are never visible on initial render)
|
||||
- [ ] Heavy sub-components within lazy routes use `defineAsyncComponent` if they are conditionally rendered
|
||||
|
||||
### Store Health
|
||||
- [ ] Stores used by multiple co-mounted views have a concurrent-fetch guard (`if (isLoading) return`)
|
||||
- [ ] No `deep: true` watchers on large arrays — use `shallowRef` + explicit trigger
|
||||
- [ ] Computed getters are used instead of methods for derived store state
|
||||
|
||||
### Bundle Health
|
||||
- [ ] Vendor libraries are isolated into stable chunks (`manualChunks`)
|
||||
- [ ] CSS `@import` statements for external stylesheets (fonts, etc.) are replaced with HTML `<link>` tags
|
||||
- [ ] `<link rel="preconnect">` hints exist for all statically-known third-party origins
|
||||
- [ ] Supabase JS is imported only once from a singleton module, not in individual components
|
||||
|
||||
### Build Verification
|
||||
- [ ] `npx vite build` — clean output, no warnings
|
||||
- [ ] `npx vitest run` — all tests pass
|
||||
- [ ] `npx vue-tsc -b` — no type errors
|
||||
- [ ] Build output diff — no unexpected chunk size regressions
|
||||
@@ -0,0 +1,133 @@
|
||||
# Go Performance Profiling
|
||||
|
||||
## Toolchain
|
||||
|
||||
| Tool | Purpose | Command |
|
||||
|---|---|---|
|
||||
| `go tool pprof` | CPU / heap / block / mutex profiles | `go tool pprof -text -cum -nodecount=30 profile.pb.gz` |
|
||||
| `go test -bench` | Micro-benchmarks | `go test -bench=BenchmarkX -benchtime=3s -benchmem -count=3` |
|
||||
| `go test -cpuprofile` | Capture CPU profile during benchmarks | `go test -bench=BenchmarkX -cpuprofile=cpu.prof` |
|
||||
| `go test -memprofile` | Capture heap profile during benchmarks | `go test -bench=BenchmarkX -memprofile=mem.prof` |
|
||||
| `go build -gcflags="-m"` | Escape analysis (what allocates on heap) | `go build -gcflags="-m" ./path/to/pkg 2>&1 \| grep "escapes to heap"` |
|
||||
| `go test -trace` | Execution trace (goroutine scheduling) | `go test -bench=BenchmarkX -trace=trace.out` |
|
||||
| `benchstat` | Statistical comparison of benchmarks | `benchstat old.txt new.txt` |
|
||||
|
||||
## Data Extraction Script
|
||||
|
||||
Use `scripts/go-pprof.sh` to extract profiles into agent-readable markdown.
|
||||
|
||||
```bash
|
||||
# CPU profile from a .prof file
|
||||
.agents/skills/perf-optimization/scripts/go-pprof.sh cpu path/to/cpu.prof
|
||||
|
||||
# Heap profile from a live endpoint
|
||||
.agents/skills/perf-optimization/scripts/go-pprof.sh heap http://localhost:6060/debug/pprof/heap
|
||||
|
||||
# CPU profile from a benchmark (generates and analyzes in one step)
|
||||
.agents/skills/perf-optimization/scripts/go-pprof.sh bench ./internal/platform/server/... BenchmarkRequireAuth
|
||||
```
|
||||
|
||||
## Go-Specific Optimization Patterns
|
||||
|
||||
### Escape Analysis
|
||||
|
||||
Before optimizing allocations, run escape analysis to understand WHY values escape to heap:
|
||||
|
||||
```bash
|
||||
go build -gcflags="-m" ./internal/platform/server/ 2>&1 | grep "escapes to heap"
|
||||
```
|
||||
|
||||
Common escape reasons:
|
||||
- Returning a pointer to a local variable
|
||||
- Storing a value in an interface (boxing)
|
||||
- Closure captures
|
||||
- Slice/map growth beyond initial capacity
|
||||
|
||||
### `sync.Pool` for Hot-Path Allocations
|
||||
|
||||
**When to use:** The profiler shows a specific type allocated/freed thousands of times per second on the same hot path.
|
||||
|
||||
**When NOT to use:** Objects that are long-lived, vary wildly in size, or have cleanup requirements.
|
||||
|
||||
```go
|
||||
var bufPool = sync.Pool{
|
||||
New: func() interface{} { return new(bytes.Buffer) },
|
||||
}
|
||||
|
||||
func process() {
|
||||
buf := bufPool.Get().(*bytes.Buffer)
|
||||
defer func() { buf.Reset(); bufPool.Put(buf) }()
|
||||
// use buf...
|
||||
}
|
||||
```
|
||||
|
||||
### Pre-sized Slices and Maps
|
||||
|
||||
**Symptom:** `runtime.growslice` or `runtime.mapassign` in the profile — the runtime is repeatedly growing backing arrays.
|
||||
|
||||
```go
|
||||
// ❌ Grows 3 times: cap 0 → 1 → 2 → 4
|
||||
items := []Item{}
|
||||
for _, raw := range data { items = append(items, parse(raw)) }
|
||||
|
||||
// ✅ One allocation
|
||||
items := make([]Item, 0, len(data))
|
||||
for _, raw := range data { items = append(items, parse(raw)) }
|
||||
```
|
||||
|
||||
### `strings.Builder` Over `fmt.Sprintf` in Loops
|
||||
|
||||
**Symptom:** `fmt.Sprintf` or `runtime.convTstring` in allocation-heavy loops.
|
||||
|
||||
```go
|
||||
var b strings.Builder
|
||||
b.Grow(estimatedLen)
|
||||
for _, part := range parts { b.WriteString(part) }
|
||||
result := b.String()
|
||||
```
|
||||
|
||||
### Benchmark Best Practices
|
||||
|
||||
```go
|
||||
func BenchmarkX(b *testing.B) {
|
||||
// Setup OUTSIDE the loop — don't measure init cost
|
||||
fixture := buildFixture(b)
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := doWork(fixture)
|
||||
// Prevent compiler from eliminating the call
|
||||
_ = result
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Statistical validity:** Always run with `-count=5` minimum and compare with `benchstat`:
|
||||
|
||||
```bash
|
||||
go test -bench=BenchmarkX -benchmem -count=5 > old.txt
|
||||
# ... make changes ...
|
||||
go test -bench=BenchmarkX -benchmem -count=5 > new.txt
|
||||
benchstat old.txt new.txt
|
||||
```
|
||||
|
||||
## Go-Specific Anti-Patterns
|
||||
|
||||
1. **Don't use `sync.Pool` for everything.** It's only effective for truly hot paths with uniform objects. Misuse adds complexity without measurable gain.
|
||||
2. **Don't fight the GC.** Go's GC is tuned for latency. If throughput matters more, tune `GOGC` before rewriting code.
|
||||
3. **Don't use `unsafe` for performance.** The compiler's escape analysis and inlining are usually sufficient. `unsafe` breaks memory safety guarantees.
|
||||
4. **Don't ignore `-race` when benchmarking.** Always verify correctness with `-race` before measuring performance without it.
|
||||
|
||||
## Irreducible Floors in Go
|
||||
|
||||
These are performance costs you cannot eliminate — recognize them and stop:
|
||||
|
||||
| Function | What it is | Why it's irreducible |
|
||||
|---|---|---|
|
||||
| `p256MulInternal` | P-256 scalar multiplication (ECDSA) | Hardware-optimized assembly |
|
||||
| `runtime.mallocgc` | Go memory allocator | Symptom, not cause — fix the caller |
|
||||
| `runtime.gcBgMarkWorker` | GC mark phase | Proportional to allocation rate — fix allocs |
|
||||
| `runtime.futex` / `runtime.usleep` | OS synchronization | Scheduling overhead |
|
||||
| `syscall.Syscall` | Kernel calls (I/O) | Irreducible for actual I/O ops |
|
||||
| `crypto/aes` | AES encryption | Hardware AES-NI, cannot improve |
|
||||
@@ -0,0 +1,12 @@
|
||||
# Python Performance Profiling
|
||||
|
||||
> **Stub:** This module will be populated when Python profiling is needed in a project.
|
||||
> Follow the pattern established in [go.md](go.md).
|
||||
|
||||
## Expected Content
|
||||
|
||||
- Toolchain: `py-spy`, `cProfile`, `memory_profiler`, `line_profiler`, `scalene`
|
||||
- Patterns: `__slots__`, generator expressions, `collections.deque`, `numpy` vectorization
|
||||
- Benchmarking: `timeit`, `pytest-benchmark`
|
||||
- Irreducible floors: GIL contention, interpreter overhead, C extension boundaries
|
||||
- Script: `scripts/python-pyspy.sh`
|
||||
@@ -0,0 +1,12 @@
|
||||
# Rust Performance Profiling
|
||||
|
||||
> **Stub:** This module will be populated when Rust profiling is needed in a project.
|
||||
> Follow the pattern established in [go.md](go.md).
|
||||
|
||||
## Expected Content
|
||||
|
||||
- Toolchain: `perf`, `flamegraph`, `criterion`, `cargo-flamegraph`, `dhat`
|
||||
- Patterns: Arena allocation, `#[inline]`, zero-copy parsing, `Box` vs stack
|
||||
- Benchmarking: `criterion` setup, statistical comparison
|
||||
- Irreducible floors: syscall overhead, TLS handshake, memory-mapped I/O
|
||||
- Script: `scripts/rust-flamegraph.sh`
|
||||
@@ -0,0 +1,374 @@
|
||||
#!/usr/bin/env bash
|
||||
# frontend-lighthouse.sh — Robust frontend performance profiling for AI agents.
|
||||
#
|
||||
# Two modes:
|
||||
# 1. `lighthouse` — Run headless Lighthouse with timeout and binary discovery
|
||||
# 2. `bundle` — Analyze Vite build output (always works, no Chrome needed)
|
||||
#
|
||||
# Usage:
|
||||
# frontend-lighthouse.sh lighthouse <url> [preset]
|
||||
# frontend-lighthouse.sh bundle <frontend-dir>
|
||||
#
|
||||
# Arguments:
|
||||
# lighthouse mode:
|
||||
# url — The URL to audit (e.g., http://localhost:4173)
|
||||
# preset — "desktop" or "mobile" (default: mobile)
|
||||
#
|
||||
# bundle mode:
|
||||
# frontend-dir — Path to the frontend project root (must have vite.config.*)
|
||||
#
|
||||
# Examples:
|
||||
# frontend-lighthouse.sh lighthouse http://localhost:4173 desktop
|
||||
# frontend-lighthouse.sh bundle ./apps/frontend
|
||||
# frontend-lighthouse.sh bundle /home/user/project/apps/frontend
|
||||
#
|
||||
# Prerequisites:
|
||||
# lighthouse mode: Node.js + Chrome/Chromium (or globally installed lighthouse)
|
||||
# bundle mode: Node.js + Vite project (no Chrome needed)
|
||||
#
|
||||
# Robustness features (learned from real-world agent sessions):
|
||||
# - Binary discovery: checks PATH, npx, ~/.npm-global/bin, common install locations
|
||||
# - Timeout: kills Lighthouse after LIGHTHOUSE_TIMEOUT seconds (default: 120)
|
||||
# - Fallback: if Lighthouse fails/hangs, suggests running bundle mode instead
|
||||
# - Error reporting disabled: prevents interactive consent prompts
|
||||
# - Bundle mode: always works, extracts chunk sizes from vite build output
|
||||
#
|
||||
# Output: Structured markdown tables suitable for LLM consumption.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
MODE="${1:-}"
|
||||
LIGHTHOUSE_TIMEOUT="${LIGHTHOUSE_TIMEOUT:-120}"
|
||||
|
||||
# ─── Binary Discovery ──────────────────────────────────────────────────────────
|
||||
# Lighthouse can be installed globally via npm, in ~/.npm-global, or via npx.
|
||||
# Try multiple locations to find a working binary.
|
||||
find_lighthouse() {
|
||||
# 1. Check PATH
|
||||
if command -v lighthouse &>/dev/null; then
|
||||
echo "lighthouse"
|
||||
return
|
||||
fi
|
||||
|
||||
# 2. Check common global install locations
|
||||
local candidates=(
|
||||
"$HOME/.npm-global/bin/lighthouse"
|
||||
"/usr/local/bin/lighthouse"
|
||||
"/usr/bin/lighthouse"
|
||||
)
|
||||
for candidate in "${candidates[@]}"; do
|
||||
if [[ -x "$candidate" ]]; then
|
||||
echo "$candidate"
|
||||
return
|
||||
fi
|
||||
done
|
||||
|
||||
# 3. Fall back to npx (will auto-install if needed)
|
||||
echo "npx --yes lighthouse"
|
||||
}
|
||||
|
||||
# ─── Lighthouse Mode ───────────────────────────────────────────────────────────
|
||||
run_lighthouse() {
|
||||
local url="$1"
|
||||
local preset="${2:-mobile}"
|
||||
|
||||
local lh_cmd
|
||||
lh_cmd=$(find_lighthouse)
|
||||
|
||||
local preset_flag=""
|
||||
if [[ "$preset" == "desktop" ]]; then
|
||||
preset_flag="--preset=desktop"
|
||||
fi
|
||||
|
||||
local tmpfile
|
||||
tmpfile=$(mktemp /tmp/lighthouse-XXXXXX.json)
|
||||
trap 'rm -f "$tmpfile"' EXIT
|
||||
|
||||
echo "# Lighthouse Report: ${url} (${preset})"
|
||||
echo ""
|
||||
echo "Binary: ${lh_cmd}"
|
||||
echo "Timeout: ${LIGHTHOUSE_TIMEOUT}s"
|
||||
echo ""
|
||||
|
||||
# Run Lighthouse with timeout to prevent infinite hangs in headless environments.
|
||||
# --enable-error-reporting=false prevents interactive consent prompts.
|
||||
# --no-sandbox is required in containerized/CI environments.
|
||||
local lh_pid
|
||||
local lh_exit=0
|
||||
|
||||
$lh_cmd "$url" \
|
||||
--output=json \
|
||||
--output-path="$tmpfile" \
|
||||
--chrome-flags="--headless --no-sandbox --disable-gpu --disable-dev-shm-usage" \
|
||||
--only-categories=performance \
|
||||
$preset_flag \
|
||||
--enable-error-reporting=false \
|
||||
--quiet 2>/dev/null &
|
||||
lh_pid=$!
|
||||
|
||||
# Wait with timeout — kill if Lighthouse hangs (common in headless/CI)
|
||||
local elapsed=0
|
||||
while kill -0 "$lh_pid" 2>/dev/null; do
|
||||
if (( elapsed >= LIGHTHOUSE_TIMEOUT )); then
|
||||
kill -9 "$lh_pid" 2>/dev/null || true
|
||||
wait "$lh_pid" 2>/dev/null || true
|
||||
echo "⚠️ Lighthouse timed out after ${LIGHTHOUSE_TIMEOUT}s."
|
||||
echo ""
|
||||
echo "This usually means Chrome/Chromium is not available or the headless"
|
||||
echo "environment doesn't support it. Try bundle mode instead:"
|
||||
echo ""
|
||||
echo " $0 bundle <frontend-dir>"
|
||||
echo ""
|
||||
return 1
|
||||
fi
|
||||
sleep 2
|
||||
elapsed=$((elapsed + 2))
|
||||
done
|
||||
|
||||
wait "$lh_pid" || lh_exit=$?
|
||||
|
||||
if [[ $lh_exit -ne 0 ]]; then
|
||||
echo "⚠️ Lighthouse exited with code ${lh_exit}."
|
||||
echo "Try bundle mode instead: $0 bundle <frontend-dir>"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -s "$tmpfile" ]]; then
|
||||
echo "⚠️ Lighthouse produced no output."
|
||||
echo "Try bundle mode instead: $0 bundle <frontend-dir>"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Extract metrics into structured markdown
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const r = JSON.parse(fs.readFileSync('$tmpfile', 'utf8'));
|
||||
const a = r.audits;
|
||||
const score = Math.round((r.categories.performance.score || 0) * 100);
|
||||
|
||||
console.log('## Performance Score: ' + score + '/100');
|
||||
console.log('');
|
||||
console.log('## Core Web Vitals');
|
||||
console.log('');
|
||||
console.log('| Metric | Value | Score |');
|
||||
console.log('|---|---|---|');
|
||||
|
||||
const vitals = [
|
||||
'first-contentful-paint',
|
||||
'largest-contentful-paint',
|
||||
'total-blocking-time',
|
||||
'cumulative-layout-shift',
|
||||
'speed-index',
|
||||
'interactive',
|
||||
];
|
||||
|
||||
vitals.forEach(k => {
|
||||
if (a[k]) {
|
||||
const s = a[k].score !== null ? Math.round(a[k].score * 100) : 'N/A';
|
||||
console.log('| ' + a[k].title + ' | ' + (a[k].displayValue || 'N/A') + ' | ' + s + ' |');
|
||||
}
|
||||
});
|
||||
|
||||
console.log('');
|
||||
console.log('## Opportunities');
|
||||
console.log('');
|
||||
|
||||
const opportunities = Object.values(a)
|
||||
.filter(audit => audit.details && audit.details.type === 'opportunity' && audit.details.overallSavingsMs > 0)
|
||||
.sort((x, y) => (y.details.overallSavingsMs || 0) - (x.details.overallSavingsMs || 0));
|
||||
|
||||
if (opportunities.length > 0) {
|
||||
console.log('| Opportunity | Potential Savings |');
|
||||
console.log('|---|---|');
|
||||
opportunities.forEach(o => {
|
||||
console.log('| ' + o.title + ' | ' + Math.round(o.details.overallSavingsMs) + ' ms |');
|
||||
});
|
||||
} else {
|
||||
console.log('No significant opportunities found.');
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log('## Diagnostics');
|
||||
console.log('');
|
||||
|
||||
const diagnostics = Object.values(a)
|
||||
.filter(audit => audit.details && audit.details.type === 'table' && audit.score !== null && audit.score < 0.9)
|
||||
.slice(0, 10);
|
||||
|
||||
if (diagnostics.length > 0) {
|
||||
console.log('| Diagnostic | Score |');
|
||||
console.log('|---|---|');
|
||||
diagnostics.forEach(d => {
|
||||
console.log('| ' + d.title + ' | ' + Math.round(d.score * 100) + ' |');
|
||||
});
|
||||
} else {
|
||||
console.log('All diagnostics passed.');
|
||||
}
|
||||
"
|
||||
}
|
||||
|
||||
# ─── Bundle Analysis Mode ──────────────────────────────────────────────────────
|
||||
# Always works — no Chrome needed. Extracts chunk sizes from vite build output.
|
||||
# This was the most useful data source during the FATH frontend session: it
|
||||
# revealed the 297KB monolithic index chunk and guided all 6 fixes.
|
||||
run_bundle() {
|
||||
local frontend_dir="$1"
|
||||
|
||||
if [[ ! -d "$frontend_dir" ]]; then
|
||||
echo "Error: directory '$frontend_dir' does not exist."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify it's a Vite project
|
||||
if ! ls "$frontend_dir"/vite.config.* &>/dev/null; then
|
||||
echo "Error: no vite.config.* found in '$frontend_dir'. Is this a Vite project?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "# Bundle Analysis: ${frontend_dir}"
|
||||
echo ""
|
||||
echo "Running \`vite build\`..."
|
||||
echo ""
|
||||
|
||||
local build_output
|
||||
build_output=$(cd "$frontend_dir" && npx vite build 2>&1)
|
||||
local build_exit=$?
|
||||
|
||||
if [[ $build_exit -ne 0 ]]; then
|
||||
echo "⚠️ Build failed (exit code $build_exit):"
|
||||
echo ""
|
||||
echo '```'
|
||||
echo "$build_output"
|
||||
echo '```'
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Parse build output into structured tables
|
||||
node -e "
|
||||
const output = \`$build_output\`;
|
||||
const lines = output.split('\n');
|
||||
|
||||
const jsChunks = [];
|
||||
const cssChunks = [];
|
||||
|
||||
// Parse Vite build output lines like:
|
||||
// dist/assets/index-BExof8y3.js 20.04 kB │ gzip: 6.90 kB
|
||||
for (const line of lines) {
|
||||
const match = line.match(/dist\/assets\/(.+?)\s+([\d.]+)\s*kB\s*│\s*gzip:\s*([\d.]+)\s*kB/);
|
||||
if (match) {
|
||||
const [, name, rawKB, gzipKB] = match;
|
||||
const entry = { name, raw: parseFloat(rawKB), gzip: parseFloat(gzipKB) };
|
||||
if (name.endsWith('.js')) jsChunks.push(entry);
|
||||
else if (name.endsWith('.css')) cssChunks.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by gzip size descending
|
||||
jsChunks.sort((a, b) => b.gzip - a.gzip);
|
||||
cssChunks.sort((a, b) => b.gzip - a.gzip);
|
||||
|
||||
const totalJsRaw = jsChunks.reduce((s, c) => s + c.raw, 0);
|
||||
const totalJsGzip = jsChunks.reduce((s, c) => s + c.gzip, 0);
|
||||
const totalCssRaw = cssChunks.reduce((s, c) => s + c.raw, 0);
|
||||
const totalCssGzip = cssChunks.reduce((s, c) => s + c.gzip, 0);
|
||||
|
||||
console.log('## JS Chunks (sorted by gzip size)');
|
||||
console.log('');
|
||||
console.log('| Chunk | Raw (KB) | Gzip (KB) |');
|
||||
console.log('|---|---|---|');
|
||||
jsChunks.forEach(c => {
|
||||
// Clean chunk name: remove hash suffix for readability
|
||||
const clean = c.name.replace(/-[A-Za-z0-9_-]{8}\\.js$/, '.js');
|
||||
console.log('| ' + clean + ' | ' + c.raw.toFixed(2) + ' | ' + c.gzip.toFixed(2) + ' |');
|
||||
});
|
||||
console.log('| **Total JS** | **' + totalJsRaw.toFixed(2) + '** | **' + totalJsGzip.toFixed(2) + '** |');
|
||||
|
||||
if (cssChunks.length > 0) {
|
||||
console.log('');
|
||||
console.log('## CSS Chunks');
|
||||
console.log('');
|
||||
console.log('| Chunk | Raw (KB) | Gzip (KB) |');
|
||||
console.log('|---|---|---|');
|
||||
cssChunks.forEach(c => {
|
||||
const clean = c.name.replace(/-[A-Za-z0-9_-]{8}\\.css$/, '.css');
|
||||
console.log('| ' + clean + ' | ' + c.raw.toFixed(2) + ' | ' + c.gzip.toFixed(2) + ' |');
|
||||
});
|
||||
console.log('| **Total CSS** | **' + totalCssRaw.toFixed(2) + '** | **' + totalCssGzip.toFixed(2) + '** |');
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log('## Summary');
|
||||
console.log('');
|
||||
console.log('| Metric | Value |');
|
||||
console.log('|---|---|');
|
||||
console.log('| JS chunks | ' + jsChunks.length + ' |');
|
||||
console.log('| CSS chunks | ' + cssChunks.length + ' |');
|
||||
console.log('| Total JS (gzip) | ' + totalJsGzip.toFixed(2) + ' KB |');
|
||||
console.log('| Total CSS (gzip) | ' + totalCssGzip.toFixed(2) + ' KB |');
|
||||
console.log('| Largest JS chunk | ' + (jsChunks[0]?.name || 'N/A') + ' (' + (jsChunks[0]?.gzip.toFixed(2) || '0') + ' KB gzip) |');
|
||||
|
||||
// Flag potential issues
|
||||
console.log('');
|
||||
console.log('## Flags');
|
||||
console.log('');
|
||||
const bigChunks = jsChunks.filter(c => c.gzip > 50);
|
||||
if (bigChunks.length > 0) {
|
||||
console.log('⚠️ **Large chunks (>50 KB gzip):**');
|
||||
bigChunks.forEach(c => {
|
||||
console.log(' - ' + c.name + ': ' + c.gzip.toFixed(2) + ' KB gzip');
|
||||
});
|
||||
console.log('');
|
||||
console.log('Consider splitting vendor libraries into separate chunks (see: Artifact Partitioning pattern in SKILL.md).');
|
||||
} else {
|
||||
console.log('✅ No oversized chunks detected.');
|
||||
}
|
||||
|
||||
const vendorChunks = jsChunks.filter(c => c.name.startsWith('vendor-'));
|
||||
if (vendorChunks.length > 0) {
|
||||
console.log('');
|
||||
console.log('✅ Vendor chunk splitting detected: ' + vendorChunks.map(c => c.name).join(', '));
|
||||
} else if (jsChunks.some(c => c.gzip > 40)) {
|
||||
console.log('');
|
||||
console.log('⚠️ No vendor chunk splitting detected. The largest chunk may contain framework + app code bundled together.');
|
||||
}
|
||||
"
|
||||
}
|
||||
|
||||
# ─── Main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
case "${MODE}" in
|
||||
lighthouse|lh)
|
||||
URL="${2:-}"
|
||||
PRESET="${3:-mobile}"
|
||||
if [[ -z "$URL" ]]; then
|
||||
echo "Usage: $0 lighthouse <url> [desktop|mobile]"
|
||||
echo " $0 bundle <frontend-dir>"
|
||||
exit 1
|
||||
fi
|
||||
run_lighthouse "$URL" "$PRESET"
|
||||
;;
|
||||
bundle|build)
|
||||
DIR="${2:-}"
|
||||
if [[ -z "$DIR" ]]; then
|
||||
echo "Usage: $0 bundle <frontend-dir>"
|
||||
echo " $0 lighthouse <url> [desktop|mobile]"
|
||||
exit 1
|
||||
fi
|
||||
run_bundle "$DIR"
|
||||
;;
|
||||
*)
|
||||
echo "Frontend Performance Profiling Script"
|
||||
echo ""
|
||||
echo "Usage:"
|
||||
echo " $0 lighthouse <url> [desktop|mobile] — Run Lighthouse (needs Chrome)"
|
||||
echo " $0 bundle <frontend-dir> — Analyze Vite build chunks (no Chrome)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 lighthouse http://localhost:4173 desktop"
|
||||
echo " $0 bundle ./apps/frontend"
|
||||
echo ""
|
||||
echo "Environment:"
|
||||
echo " LIGHTHOUSE_TIMEOUT=120 — Seconds before killing a hung Lighthouse (default: 120)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env bash
|
||||
# go-pprof.sh — Extract Go pprof profiles into agent-readable markdown.
|
||||
#
|
||||
# Usage:
|
||||
# go-pprof.sh cpu <profile-path-or-url> [top-n]
|
||||
# go-pprof.sh heap <profile-path-or-url> [top-n]
|
||||
# go-pprof.sh bench <package-path> <bench-regex> [top-n]
|
||||
#
|
||||
# Examples:
|
||||
# go-pprof.sh cpu cpu.prof
|
||||
# go-pprof.sh heap http://localhost:6060/debug/pprof/heap
|
||||
# go-pprof.sh bench ./internal/platform/server/... BenchmarkRequireAuth 40
|
||||
#
|
||||
# Output: Structured markdown suitable for LLM consumption.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PROFILE_TYPE="${1:-}"
|
||||
SOURCE="${2:-}"
|
||||
TOP_N="${3:-30}"
|
||||
|
||||
usage() {
|
||||
echo "Usage:"
|
||||
echo " $0 cpu <profile-path-or-url> [top-n]"
|
||||
echo " $0 heap <profile-path-or-url> [top-n]"
|
||||
echo " $0 bench <package-path> <bench-regex> [top-n]"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [[ -z "$PROFILE_TYPE" || -z "$SOURCE" ]]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
extract_pprof() {
|
||||
local profile_file="$1"
|
||||
local label="$2"
|
||||
local top="$3"
|
||||
|
||||
echo ""
|
||||
echo "--- PPROF ${label} PROFILE (Top ${top} by Cumulative) ---"
|
||||
echo ""
|
||||
echo '```'
|
||||
go tool pprof -text -cum -nodecount="$top" "$profile_file" 2>/dev/null \
|
||||
| grep -v "^Showing nodes" \
|
||||
| sed '/^$/d'
|
||||
echo '```'
|
||||
echo ""
|
||||
}
|
||||
|
||||
case "$PROFILE_TYPE" in
|
||||
cpu|heap|block|mutex)
|
||||
extract_pprof "$SOURCE" "$(echo "$PROFILE_TYPE" | tr '[:lower:]' '[:upper:]')" "$TOP_N"
|
||||
;;
|
||||
bench)
|
||||
BENCH_REGEX="${3:-}"
|
||||
TOP_N="${4:-30}"
|
||||
|
||||
if [[ -z "$BENCH_REGEX" ]]; then
|
||||
echo "Error: bench mode requires <package-path> and <bench-regex>"
|
||||
usage
|
||||
fi
|
||||
|
||||
TMPDIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TMPDIR"' EXIT
|
||||
|
||||
echo "# Benchmark Profile: ${BENCH_REGEX}"
|
||||
echo ""
|
||||
|
||||
# Run benchmark and capture results + profiles
|
||||
echo "## Benchmark Results"
|
||||
echo ""
|
||||
echo '```'
|
||||
go test "$SOURCE" \
|
||||
-bench="$BENCH_REGEX" \
|
||||
-benchtime=2s \
|
||||
-benchmem \
|
||||
-count=1 \
|
||||
-run='^$' \
|
||||
-cpuprofile="$TMPDIR/cpu.prof" \
|
||||
-memprofile="$TMPDIR/mem.prof" \
|
||||
-timeout=120s 2>&1
|
||||
echo '```'
|
||||
|
||||
# Extract CPU profile
|
||||
if [[ -f "$TMPDIR/cpu.prof" ]]; then
|
||||
# Find the test binary (go test leaves it next to the profile)
|
||||
TEST_BIN=$(find "$TMPDIR" -name '*.test' 2>/dev/null | head -1 || true)
|
||||
if [[ -n "$TEST_BIN" ]]; then
|
||||
extract_pprof "$TMPDIR/cpu.prof" "CPU" "$TOP_N"
|
||||
else
|
||||
extract_pprof "$TMPDIR/cpu.prof" "CPU" "$TOP_N"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Extract heap profile
|
||||
if [[ -f "$TMPDIR/mem.prof" ]]; then
|
||||
echo ""
|
||||
echo "--- PPROF HEAP PROFILE (Top ${TOP_N} by Cumulative, alloc_space) ---"
|
||||
echo ""
|
||||
echo '```'
|
||||
go tool pprof -text -cum -nodecount="$TOP_N" -alloc_space "$TMPDIR/mem.prof" 2>/dev/null \
|
||||
| grep -v "^Showing nodes" \
|
||||
| sed '/^$/d'
|
||||
echo '```'
|
||||
echo ""
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Error: unknown profile type '$PROFILE_TYPE'"
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
Reference in new issue
Block a user