@github-ui/storybook-addon-performance-panel
v0.1.2
Published
a performance panel addon for storybook
Downloads
2,936
Maintainers
Keywords
Readme
@github-ui/storybook-addon-performance-panel
A Storybook addon that provides real-time performance monitoring for stories. It displays comprehensive metrics including frame timing, input responsiveness, memory usage, React profiling, and more.
Installation
The addon is included in the @github-ui/storybook package and is already configured globally.
To add the addon to your Storybook configuration, add it to the addons array in .storybook/main.ts:
// .storybook/main.ts
const config = {
addons: [
// ... other addons
'@github-ui/storybook-addon-performance-panel',
],
}This follows Storybook addon best practices by using a preset that automatically:
- Registers the panel in Storybook's manager UI
- Applies the performance monitoring decorator to all stories
Usage
The performance monitor decorator is applied globally when the addon is added to .storybook/main.ts, so all stories automatically have performance monitoring enabled. The metrics panel appears as a "⚡ Performance" tab at the bottom of Storybook.
Manual Integration
If you need to add the decorator to a specific story or configure it manually:
import { withPerformanceMonitor } from '@github-ui/storybook-addon-performance-panel/decorator'
export default {
title: 'MyComponent',
decorators: [withPerformanceMonitor],
}View Mode Behavior
The performance panel automatically detects the Storybook view mode:
- Story/Canvas view: Full performance metrics are collected and displayed
- Docs view: A message is shown indicating that metrics are optimized for Canvas view, since docs mode renders stories in iframes which affects timing accuracy
Architecture
The addon consists of two main parts:
┌─────────────────────────────────────────────────────────────────┐
│ Preview Iframe (Decorator) │
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ PerformanceProvider │───▶│ Metrics Collection │ │
│ │ └─ProfiledComponent│ │ • RAF loop (frame timing) │ │
│ │ └─Story │ │ • PerformanceObservers │ │
│ └─────────────────────┘ │ • MutationObservers │ │
│ │ • Event listeners │ │
│ │ • React Profiler API │ │
│ └──────────────┬──────────────┘ │
│ │ │
│ channel.emit(METRICS_UPDATE) │
└──────────────────────────────────────────────┼──────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Manager (Panel) │
│ ┌─────────────────────┐ │
│ │ PerformancePanel │◀── useChannel(METRICS_UPDATE) │
│ │ └─MetricsSections │ │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘Metrics Collected
Frame Timing
- FPS: Frames per second (target: 60fps)
- Frame Time: Average milliseconds per frame (target: ≤16.67ms)
- Dropped Frames: Frames exceeding 2× the expected frame time
- Frame Jitter: Sudden spikes in frame time vs baseline
- Frame Stability: Percentage indicating frame time consistency
Input Responsiveness
- Input Latency: Time from pointer event to next animation frame
- Paint Time: Browser rendering time via double-RAF technique
- INP: Interaction to Next Paint via Event Timing API (Core Web Vital)
- Uses
PerformanceObserverwithevententry type for accurate measurement - Calculated as p98 of worst interactions per Web Vitals spec
- Includes breakdown: input delay, processing time, presentation delay
- Uses
- FID: First Input Delay - latency of the very first interaction
- Last Interaction: Real-time details of most recent interaction (with Inspect button)
- Slowest Interaction: Details of worst interaction for debugging (with Inspect button)
- Shows timing breakdown:
[wait Xms] → [js Xms] → [paint Xms] - Click "Inspect" to highlight and scroll to the target element
- Shows timing breakdown:
Main Thread Health
- Long Tasks: Tasks blocking main thread >50ms (via PerformanceObserver)
- Total Blocking Time (TBT): Sum of (duration - 50ms) for all long tasks
- Thrashing: Style writes followed by long frames (forced sync layout)
- DOM Churn: Rate of DOM mutations per measurement period
Long Animation Frames (Chrome 123+)
- LoAF Count: Number of animation frames exceeding 50ms
- Blocking Duration: Total and longest blocking time from LoAFs
- P95 Duration: 95th percentile LoAF duration
- Script Attribution: Which scripts contributed to long frames
- Source URL, function name, invoker type (event-listener, user-callback, etc.)
- Helps identify exactly which code caused slow frames
Element Timing
- Element Count: Number of elements with
elementtimingattribute tracked - Largest Render Time: Slowest element to render (similar to LCP concept)
- Individual Elements: Render time for each tracked element
- Add
elementtiming="identifier"attribute to elements you want to track - Useful for measuring when hero images, key content, or specific UI elements render
- Add
Layout Stability
- CLS: Cumulative Layout Shift score (Core Web Vital)
- Forced Reflows: Layout property reads after style writes
- Style Writes: Inline style mutations observed via MutationObserver
React Performance
- Mount Count/Duration: Initial render metrics from React Profiler
- Slow Updates: React updates exceeding 16ms frame budget
- P95 Duration: 95th percentile React update time
- Render Cascades: Nested updates during commit phase (setState in useLayoutEffect)
Memory & Resources (Chrome only)
- Heap Usage: Current JS heap size
- Memory Delta: Change from baseline since last reset
- GC Pressure: Memory allocation rate (MB/s)
- Compositor Layers: Elements promoted to GPU layers
Metric Thresholds
Metrics are color-coded based on Web Vitals standards:
| Status | Meaning | |--------|---------| | 🟢 Green | Good performance (meets targets) | | 🟡 Yellow | Needs improvement (may cause issues) | | 🔴 Red | Poor performance (likely causing issues) |
Key Thresholds
| Metric | Good | Needs Work | Poor | |--------|------|------------|------| | FPS | ≥55 | 30-55 | <30 | | Frame Time | ≤16.67ms | 16.67-32ms | >32ms | | Input Latency | ≤16ms | 16-50ms | >50ms | | INP | ≤200ms | 200-500ms | >500ms | | CLS | <0.1 | 0.1-0.25 | >0.25 | | TBT | <200ms | 200-600ms | >600ms |
Package Exports
The addon follows Storybook addon best practices with the following entry points:
// Main entry point - re-exports types and constants
import { ADDON_ID, PANEL_ID, PERF_EVENTS, THRESHOLDS, DEFAULT_METRICS } from '@github-ui/storybook-addon-performance-panel'
// Preset for automatic addon registration (used by main.ts)
import '@github-ui/storybook-addon-performance-panel/preset'
// Manager entry - registers the panel in Storybook UI
import '@github-ui/storybook-addon-performance-panel/manager'
// Preview entry - exports decorator for stories
import '@github-ui/storybook-addon-performance-panel/preview'
// Types and constants
import {
ADDON_ID,
PANEL_ID,
PERF_EVENTS,
THRESHOLDS,
DEFAULT_METRICS
} from '@github-ui/storybook-addon-performance-panel/types'Collectors
The addon uses modular collector classes for metrics gathering. Each collector uses the most accurate available API for its metrics.
See collectors/README.md for detailed documentation on:
- Collection methods (optimal vs heuristic)
- Browser APIs used
- Accuracy and limitations
- Browser compatibility
| Collector | Method | Type |
|-----------|--------|------|
| FrameTimingCollector | requestAnimationFrame loop | Heuristic |
| InputCollector | Event Timing API (PerformanceObserver) | Optimal |
| MainThreadCollector | Long Tasks API (PerformanceObserver) | Optimal |
| LongAnimationFrameCollector | LoAF API (PerformanceObserver) | Optimal |
| LayoutShiftCollector | Layout Instability API (PerformanceObserver) | Optimal |
| MemoryCollector | performance.memory | Optimal |
| PaintCollector | Paint Timing API (PerformanceObserver) | Optimal |
| StyleMutationCollector | MutationObserver | Heuristic |
| ForcedReflowCollector | Property getter instrumentation | Heuristic |
| ReactProfilerCollector | React Profiler API | Optimal |
Browser Compatibility
- Chrome/Edge: Full support including memory metrics and LoAF (123+)
- Firefox/Safari: Most metrics supported, memory API and LoAF unavailable
- Memory API: Requires
performance.memory(Chrome-only) - Long Animation Frames: Requires Chrome 123+ or Edge 123+
- Compositor Layers: Requires Chrome DevTools Protocol
Development
# Run tests
npm test -w @github-ui/storybook-addon-performance-panel
# Type check
npm run tsc -w @github-ui/storybook-addon-performance-panel
# Lint
npm run lint -w @github-ui/storybook-addon-performance-panelRelated Files
- performance-decorator.tsx - Metrics collection in preview iframe
- performance-panel.tsx - UI panel in manager
- performance-types.ts - Shared types and constants
- collectors/ - Modular metric collector classes
Interpreting Metrics & Troubleshooting
This section provides guidance on how to analyze performance issues using the metrics panel.
Quick Health Check
Start by scanning these key indicators:
| Check | What to Look For | |-------|------------------| | FPS | Should be 55-60. Sustained drops indicate rendering issues | | INP | Should be <200ms. High values mean slow user interactions | | Long Tasks | Should be 0-1. More than 5 indicates main thread blocking | | CLS | Should be 0. Any value suggests layout instability | | Slow Updates | Should be 0. Non-zero means React renders exceeding frame budget |
Common Performance Patterns
🐌 Slow Initial Load / Mount
Symptoms:
- High
Mount Duration(>100ms) - Long Tasks spike on story change
- High TBT during load
Where to Look:
- Check
Mount Durationin React section - Look at
Long Taskscount andLongest Taskduration - Review
Script Eval Timein Resources section
Common Causes:
- Heavy component initialization
- Synchronous data fetching
- Large bundle imports
- Complex initial render tree
Fixes:
- Lazy load heavy dependencies
- Use
React.lazy()for code splitting - Defer non-critical initialization
- Memoize expensive computations
🎢 Janky Scrolling / Animations
Symptoms:
- FPS drops below 55
- High
Frame Time(>16.7ms) Dropped FramesincreasingFrame Jitterspikes
Where to Look:
- Watch FPS sparkline during interaction
- Check
Frame Stabilitypercentage - Look for
Thrashingscore increases
Common Causes:
- Layout thrashing (read/write cycles)
- Expensive scroll handlers
- Non-composited animations
- Large DOM mutations during scroll
Fixes:
- Use
transformandopacityfor animations (GPU-accelerated) - Debounce/throttle scroll handlers
- Use
will-changesparingly for animation targets - Batch DOM reads before writes
⏳ Slow Click/Keyboard Response
Symptoms:
- High
INP(>200ms) Input LatencyspikesLast Interactionshows high duration- Event timing breakdown shows delays
Where to Look:
- Click the "Inspect" button on slowest interaction
- Review timing breakdown:
[wait Xms] → [js Xms] → [paint Xms] - Check
Input Jitterfor inconsistency
Interpreting Timing Breakdown:
- Wait (input delay): Time before JS starts processing. High values indicate blocked main thread.
- JS (processing time): Time in event handlers. High values indicate expensive handlers.
- Paint (presentation delay): Time from handler end to visual update. High values indicate expensive rendering.
Common Causes by Phase: | Phase | If High... | Common Causes | |-------|------------|---------------| | Wait | Main thread blocked | Long tasks, heavy computation before click | | JS | Expensive handler | Complex state updates, sync operations | | Paint | Expensive render | Large DOM changes, layout recalculation |
Fixes:
- Break up long tasks with
scheduler.yield()orsetTimeout - Move expensive work to Web Workers
- Virtualize long lists
- Optimize React render paths (memoization, selective updates)
📦 Layout Shifts (CLS)
Symptoms:
CLSscore >0Layout Shift Countincreasing- Visual elements jumping around
Where to Look:
- Watch CLS during interactions
- Note the shift count to identify frequency
- Check if shifts correlate with data loading
Common Causes:
- Images without explicit dimensions
- Dynamically injected content
- Font loading (FOUT/FOIT)
- Ads or embeds loading
- Skeleton placeholders with wrong sizes
Fixes:
- Always set
width/heightoraspect-ratioon images/videos - Reserve space for dynamic content with min-height
- Use
font-display: optionalor preload fonts - Prefer transforms over layout-affecting properties for animations
🔄 React Re-render Issues
Symptoms:
- High
Slow Updatescount P95 Durationexceeding 16msRender Cascades> 0- High
Post-Mount Update Count
Where to Look:
- Check
Slow UpdatesandP95 Duration - Look for
Render Cascades(indicates useLayoutEffect issues) - Compare
Mount CountvsPost-Mount Update Count
Interpreting React Metrics:
- Slow Updates: Updates taking >16ms. Even 1 is problematic.
- P95 Duration: 95th percentile. Shows worst-case user experience.
- Render Cascades: setState during commit phase. Very expensive!
Common Causes:
- Missing
useMemo/useCallbackon expensive values - Prop drilling causing subtree re-renders
useLayoutEffecttriggering synchronous re-renders- Context providers with unstable values
- Effect dependencies causing infinite loops
Fixes:
- Use React DevTools Profiler to identify slow components
- Add
React.memo()to pure components - Memoize context values and callbacks
- Split contexts to reduce subscriber scope
- Move state closer to where it's used
🔥 Forced Reflows (Layout Thrashing)
Symptoms:
Forced Reflowscount >0Thrashingscore increasing- FPS drops during interactions
Where to Look:
- Check
Forced Reflowscount - Look for
Thrashingcorrelation with long frames - Review
Style Writesfrequency
What Causes Forced Reflow: Reading layout properties after writing styles forces synchronous layout calculation:
// ❌ BAD: Causes forced reflow
element.style.width = '100px'
const height = element.offsetHeight // Forces layout!
element.style.height = height + 'px'
// ✅ GOOD: Batch reads, then writes
const height = element.offsetHeight // Read first
element.style.width = '100px' // Write after
element.style.height = height + 'px'Layout-triggering Properties:
offsetTop/Left/Width/HeightscrollTop/Left/Width/HeightclientTop/Left/Width/HeightgetComputedStyle()getBoundingClientRect()
Fixes:
- Batch all DOM reads before any writes
- Use
requestAnimationFrameto defer layout-affecting work - Cache layout values when possible
- Use CSS transforms instead of top/left
💾 Memory Issues
Symptoms:
Memory Deltagrowing steadilyGC Pressure>1 MB/sPeak Memorykeeps increasing after reset
Where to Look:
- Watch
Memory Deltatrend over time - Check
GC Pressurefor allocation rate - Compare
Heap Usagebefore/after interactions
Common Causes:
- Event listeners not cleaned up
- Closures holding references
- Growing arrays/caches without limits
- Detached DOM nodes
- Unsubscribed observables/subscriptions
Fixes:
- Use React's cleanup functions in useEffect
- Implement LRU caches with size limits
- Use WeakMap/WeakSet for object references
- Profile with Chrome DevTools Memory tab
Debugging Workflow
Step 1: Baseline
- Open story in isolation
- Click Reset (🔄) to clear metrics
- Wait 2-3 seconds for metrics to stabilize
- Note baseline FPS, memory, and any initial issues
Step 2: Interact
- Perform the problematic interaction slowly
- Watch metrics change in real-time
- Note which metrics spike or degrade
Step 3: Identify
- Check the "worst" metric indicator
- Use the timing breakdown for input issues
- Click "Inspect" to highlight slow interaction targets
- Cross-reference with React DevTools if needed
Step 4: Fix & Verify
- Make changes to address the identified issue
- Reset metrics again
- Repeat the interaction
- Confirm metrics improved
Metric Correlations
Use these correlations to triangulate issues:
| If you see... | Also check... | Likely cause | |---------------|---------------|--------------| | Low FPS + High Long Tasks | TBT, Longest Task | Heavy JS execution | | Low FPS + High Style Writes | Thrashing, Forced Reflows | Layout thrashing | | High INP + High Wait phase | Long Tasks | Blocked main thread | | High INP + High JS phase | Slow Updates, P95 | Expensive handlers | | High INP + High Paint phase | CLS, DOM Churn | Expensive rendering | | High CLS + DOM Churn | Style Writes | Dynamic content | | Rising Memory + High DOM Elements | DOM Churn | DOM leak | | Render Cascades > 0 | Slow Updates | useLayoutEffect issues |
Browser-Specific Notes
Chrome/Edge (recommended for debugging):
- Full metric support including memory
- INP via Event Timing API
- Best for initial investigation
Firefox/Safari:
- No memory metrics
- No Event Timing API (INP shows "N/A")
- Use for cross-browser validation after Chrome debugging
