| 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
cd packages/app
ANALYZE=true bun run build
npx vite-bundle-visualizer
Code Splitting Strategy
const CoopDetails = lazy(() => import("./views/CoopDetails"));
const RoostView = lazy(() => import("./views/RoostView"));
const ArchiveExplorer = lazy(() => import("./components/ArchiveExplorer"));
<Suspense fallback={<Skeleton />}>
<CoopDetails />
</Suspense>
Dependency Impact Check
Before adding any dependency:
npx bundlephobia <package-name>
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
import { formatEther } from "viem";
import { createSmartAccountClient } from "permissionless";
import * as viem from "viem";
import format from "date-fns/format";
import { format } from "date-fns";
Part 2: Runtime Profiling
React Profiler
import { Profiler } from "react";
function onRenderCallback(
id: string,
phase: "mount" | "update",
actualDuration: number,
baseDuration: number,
) {
if (actualDuration > 16) {
logger.warn("Slow render detected", {
component: id,
phase,
actualDuration: `${actualDuration.toFixed(1)}ms`,
baseDuration: `${baseDuration.toFixed(1)}ms`,
});
}
}
<Profiler id="TabList" onRender={onRenderCallback}>
<TabList tabs={tabs} />
</Profiler>
Identifying Re-render Causes
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
performance.mark("coop-load-start");
const coops = await loadCoops();
performance.mark("coop-load-end");
performance.measure("coop-load", "coop-load-start", "coop-load-end");
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
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
<link rel="preload" href="/hero-image.webp" as="image" fetchPriority="high" />
<img
src={coop.coverImage}
width={800}
height={400}
loading="eager" // Above-fold: eager
fetchPriority="high" // LCP candidate
alt={coop.name}
/>
<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
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)
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,
overscan: 5,
});
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
const filteredTabs = useMemo(
() => tabs.filter(t => t.status === "active").sort(byDate),
[tabs]
);
const handlePublish = useCallback(
(draftId: string) => publishMutation.mutate(draftId),
[publishMutation]
);
const TabCard = React.memo(function TabCard({ tab }: Props) {
return <Card>{/* ... */}</Card>;
});
Part 6: Network Performance
Dexie Query Optimization
import { useLiveQuery } from "dexie-react-hooks";
const tabs = useLiveQuery(
() => db.tabs.where("coopId").equals(coopId).toArray(),
[coopId]
);
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
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