- name
- performance
- user-invocable
- false
- description
- Performance profiling and optimization - bundle analysis, Lighthouse CI, React Profiler, memory leaks, service worker cache efficiency. Use for performance investigations, optimization, and budget enforcement.
- version
- 1.0.0
- status
- active
- packages
- ["shared","app","extension"]
- dependencies
- []
- last_updated
- 2026-03-12
- last_verified
- 2026-03-12
# Performance Skill
Performance profiling and optimization guide: bundle analysis, runtime profiling, memory management, and monitoring.
---
## Activation
When invoked:
- Identify the performance layer (bundle size, runtime, memory, network).
- Measure before optimizing — never optimize without data.
- Check bundle budgets before adding dependencies.
## Part 1: Bundle Budgets
### Coop Limits
| Target | Budget | Check |
|--------|--------|-------|
| Main bundle (app) | < 150KB gzipped | `bun --filter app build` |
| Extension popup | < 100KB gzipped | `bun --filter extension build` |
| Per-route chunk | < 50KB gzipped | Vite code splitting |
| Total JS (app) | < 400KB gzipped | `bun run build` output |
### Bundle Analysis
```bash
# Analyze bundle composition
cd packages/app
ANALYZE=true bun run build
# Or use vite-bundle-visualizer
npx vite-bundle-visualizer
```
### Code Splitting Strategy
```typescript
// Route-level splitting (automatic with React Router lazy)
const CoopDetails = lazy(() => import("./views/CoopDetails"));
const RoostView = lazy(() => import("./views/RoostView"));
// Component-level splitting (for heavy components)
const ArchiveExplorer = lazy(() => import("./components/ArchiveExplorer"));
// Always wrap lazy components in Suspense
<Suspense fallback={<Skeleton />}>
<CoopDetails />
</Suspense>
```
### Dependency Impact Check
Before adding any dependency:
```bash
# Check package size
npx bundlephobia <package-name>
# Or check locally after adding
bun run build 2>&1 | grep "chunk size"
```
| Category | Max Size | Examples |
|----------|----------|---------|
| UI library | < 20KB | Radix primitives (~3KB each) |
| Utility | < 10KB | date-fns (tree-shakeable) |
| Web3 | < 50KB | viem + permissionless (tree-shakeable) |
| State | < 5KB | Zustand (~1KB) |
| CRDT | < 30KB | Yjs (~15KB) |
### Tree Shaking
```typescript
// GOOD: Named imports allow tree shaking
import { formatEther } from "viem";
import { createSmartAccountClient } from "permissionless";
// BAD: Barrel re-export may prevent tree shaking for large libs
import * as viem from "viem";
// GOOD: Direct path imports for large libraries
import format from "date-fns/format";
// BAD: Top-level import pulls entire library
import { format } from "date-fns";
```
## Part 2: Runtime Profiling
### React Profiler
```typescript
import { Profiler } from "react";
function onRenderCallback(
id: string,
phase: "mount" | "update",
actualDuration: number,
baseDuration: number,
) {
if (actualDuration > 16) {
// Longer than one frame (60fps)
logger.warn("Slow render detected", {
component: id,
phase,
actualDuration: `${actualDuration.toFixed(1)}ms`,
baseDuration: `${baseDuration.toFixed(1)}ms`,
});
}
}
// Wrap suspect components
<Profiler id="TabList" onRender={onRenderCallback}>
<TabList tabs={tabs} />
</Profiler>
```
### Identifying Re-render Causes
```typescript
// Debug hook: log what caused a re-render
function useWhyDidYouRender(componentName: string, props: Record<string, unknown>) {
const previousProps = useRef(props);
useEffect(() => {
const changedProps = Object.entries(props).reduce<Record<string, { from: unknown; to: unknown }>>(
(acc, [key, value]) => {
if (previousProps.current[key] !== value) {
acc[key] = { from: previousProps.current[key], to: value };
}
return acc;
},
{}
);
if (Object.keys(changedProps).length > 0) {
logger.debug(`${componentName} re-rendered`, { changedProps });
}
previousProps.current = props;
});
}
```
### Performance Marks
```typescript
// Mark critical operations
performance.mark("coop-load-start");
const coops = await loadCoops();
performance.mark("coop-load-end");
performance.measure("coop-load", "coop-load-start", "coop-load-end");
// Read measurements
const entries = performance.getEntriesByName("coop-load");
logger.info("Coop load time", { duration: entries[0]?.duration });
```
## Part 3: Web Vitals
### Core Metrics
| Metric | Good | Needs Work | Poor |
|--------|------|------------|------|
| **LCP** (Largest Contentful Paint) | < 2.5s | 2.5-4s | > 4s |
| **FID** (First Input Delay) | < 100ms | 100-300ms | > 300ms |
| **CLS** (Cumulative Layout Shift) | < 0.1 | 0.1-0.25 | > 0.25 |
| **INP** (Interaction to Next Paint) | < 200ms | 200-500ms | > 500ms |
| **TTFB** (Time to First Byte) | < 800ms | 800ms-1.8s | > 1.8s |
### Collecting Web Vitals
```typescript
// Use web-vitals library
import { onLCP, onFID, onCLS, onINP, onTTFB } from "web-vitals";
function reportWebVital(metric: { name: string; value: number; rating: string }) {
logger.info("Web Vital", {
name: metric.name,
value: metric.value.toFixed(1),
rating: metric.rating,
});
}
onLCP(reportWebVital);
onFID(reportWebVital);
onCLS(reportWebVital);
onINP(reportWebVital);
onTTFB(reportWebVital);
```
### LCP Optimization
```typescript
// Preload critical images
<link rel="preload" href="/hero-image.webp" as="image" fetchPriority="high" />
// Explicit dimensions prevent layout shift
<img
src={coop.coverImage}
width={800}
height={400}
loading="eager" // Above-fold: eager
fetchPriority="high" // LCP candidate
alt={coop.name}
/>
// Below-fold images: lazy load
<img
src={tab.screenshot}
loading="lazy" // Below-fold: lazy
width={400}
height={300}
alt={tab.title}
/>
```
## Part 4: Memory Management
### Detecting Memory Leaks
Common leak sources in Coop:
| Source | Symptom | Fix |
|--------|---------|-----|
| Blob URLs not revoked | Memory grows with each screenshot | `URL.revokeObjectURL()` |
| Event listeners not removed | Memory grows on navigation | Cleanup in useEffect or `{ once: true }` |
| Timers not cleared | Callbacks fire after unmount | Cleanup in useEffect |
| Yjs docs not destroyed | Memory grows per coop opened | `yDoc.destroy()` on unmount |
| y-webrtc providers not disconnected | WebRTC connections leak | `provider.disconnect()` + `provider.destroy()` |
| Large IndexedDB cursors | Memory spike during iteration | Use pagination/limits |
### AbortController Cleanup
```typescript
function useCoopSearch(query: string) {
const [results, setResults] = useState<Coop[]>([]);
useEffect(() => {
const controller = new AbortController();
searchCoops(query, { signal: controller.signal })
.then(setResults)
.catch((error) => {
if (error.name !== "AbortError") {
logger.error("Search failed", { error });
}
});
return () => controller.abort();
}, [query]);
return results;
}
```
## Part 5: List Performance
### Virtualization (50+ Items)
```typescript
import { useVirtualizer } from "@tanstack/react-virtual";
function TabList({ tabs }: { tabs: Tab[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: tabs.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 120, // Estimated row height
overscan: 5, // Render 5 extra items above/below
});
return (
<div ref={parentRef} className="h-[600px] overflow-auto">
<div
style={{ height: `${virtualizer.getTotalSize()}px`, position: "relative" }}
>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: "absolute",
top: 0,
transform: `translateY(${virtualItem.start}px)`,
width: "100%",
}}
>
<TabCard tab={tabs[virtualItem.index]} />
</div>
))}
</div>
</div>
);
}
```
### Memoization Guidelines
```typescript
// USE useMemo for:
// - Expensive computations (filtering, sorting large arrays)
const filteredTabs = useMemo(
() => tabs.filter(t => t.status === "active").sort(byDate),
[tabs]
);
// USE useCallback for:
// - Callbacks passed to memoized children
const handlePublish = useCallback(
(draftId: string) => publishMutation.mutate(draftId),
[publishMutation]
);
// USE React.memo for:
// - List item components rendered many times
const TabCard = React.memo(function TabCard({ tab }: Props) {
return <Card>{/* ... */}</Card>;
});
// DON'T memoize:
// - Primitives (strings, numbers, booleans)
// - Components that always re-render anyway
// - Cheap computations
```
## Part 6: Network Performance
### Dexie Query Optimization
```typescript
// Dexie live queries are reactive and efficient
import { useLiveQuery } from "dexie-react-hooks";
// Good: indexed field query
const tabs = useLiveQuery(
() => db.tabs.where("coopId").equals(coopId).toArray(),
[coopId]
);
// Bad: full table scan with JS filter
const tabs = useLiveQuery(
() => db.tabs.filter(t => t.coopId === coopId).toArray(),
[coopId]
);
```
## Anti-Patterns
- **Never optimize without measuring first** — profile before changing code
- **Never add large dependencies without checking size** — use bundlephobia
- **Never forget to clean up Yjs docs and providers** — destroy on unmount
- **Never render 50+ items without virtualization** — use @tanstack/react-virtual
- **Never memoize primitives** — only expensive computations
- **Never use `import *`** — prevents tree shaking
- **Never set images without explicit dimensions** — causes CLS
## Quick Reference Checklist
### Performance Investigation
- [ ] Bundle size within budgets? (`bun run build` output)
- [ ] Web Vitals in "good" range? (LCP < 2.5s, CLS < 0.1)
- [ ] No memory leaks? (Yjs docs, blob URLs, listeners cleaned up)
- [ ] Lists virtualized if 50+ items?
- [ ] Images have explicit width/height?
- [ ] Heavy components code-split with `lazy()`?
- [ ] Dexie queries use indexed fields?
- [ ] Memoization only where justified?
## Related Skills
- `react` — React-specific optimization (useMemo, useCallback, React.memo)
- `vite` — Bundle splitting and build optimization
- `data-layer` — Dexie query efficiency and Yjs sync performance
- `monitoring` — Runtime performance tracking
GitHubで見る