Skip to content

feat(performance): add performance monitoring and optimization utilities#54

Merged
jbdevprimary merged 6 commits into
mainfrom
feat/issue-17-performance-optimization
Jan 18, 2026
Merged

feat(performance): add performance monitoring and optimization utilities#54
jbdevprimary merged 6 commits into
mainfrom
feat/issue-17-performance-optimization

Conversation

@jbdevprimary
Copy link
Copy Markdown
Contributor

@jbdevprimary jbdevprimary commented Jan 18, 2026

Summary

  • Add PerformanceMonitor class for tracking render times, mount times, frame rates, and general performance metrics
  • Add performance hooks for optimized React patterns:
    • useDebouncedValue / useDebouncedCallback - delay execution until after wait period
    • useThrottledCallback - limit function execution frequency
    • useStableCallback - stable reference for callbacks to prevent unnecessary re-renders
    • usePrevious - track previous value for comparison
    • useLazyValue - defer expensive computations
    • useStableArray / useStableObject - memoized collections
    • useIntersectionObserver - lazy loading support
    • useWindowDimensions - responsive design support
    • useRenderTime / useMountTime / usePerformanceTracking - component performance tracking
  • Add OptimizedList component as a performance-optimized FlatList wrapper with:
    • Pre-computed item layouts for faster scrolling
    • Viewability configuration for optimized rendering
    • Memoization helpers (createMemoizedRenderItem, withListItemMemo)
  • Add comprehensive test suite for PerformanceMonitor (13 tests)

Test plan

  • All tests pass (206 tests)
  • TypeScript type checking passes
  • Biome linting passes
  • Manual testing in Expo development build
  • Verify performance hooks work correctly in components
  • Test OptimizedList with large datasets

Closes #17

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • File tree now shows status indicators for added/modified/deleted items.
    • New performance toolkit: monitoring, reporting, hooks and an optimized list component for smoother rendering.
  • Improvements

    • Centralized theme color handling for more consistent editor preview styling.
    • Safer typing around global error utilities to improve type safety.
  • Tests

    • Added comprehensive test suites covering performance utilities and optimized list behavior.

✏️ Tip: You can customize this high-level summary in your review settings.

- Add PerformanceMonitor class for tracking render times, mount times, and FPS
- Add performance hooks: useDebouncedValue, useThrottledCallback, useDebouncedCallback
- Add useStableCallback, usePrevious, useLazyValue for stable references
- Add useStableArray, useStableObject for memoized collections
- Add useIntersectionObserver, useWindowDimensions for lazy loading
- Add useRenderTime, useMountTime, usePerformanceTracking for component metrics
- Add OptimizedList component as performance-optimized FlatList wrapper
- Add createMemoizedRenderItem and withListItemMemo helpers
- Add comprehensive test suite for PerformanceMonitor

Closes #17

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 18, 2026

📝 Walkthrough

Walkthrough

Adds a new performance module (monitor, hooks, OptimizedList) with tests, exports the performance API from the library, refactors FileTree and editor theming by centralizing helpers, and tightens typing in error handling. No public API removals.

Changes

Cohort / File(s) Summary
Performance Monitor
src/lib/performance/monitor.ts
New PerformanceMonitor class and perfMonitor singleton: metric recording, startTiming, render/mount tracking, frame tracking/FPS, slow-component detection, periodic reporting, summaries.
Performance Hooks
src/lib/performance/hooks.ts
New hooks: useRenderTime, useMountTime, usePerformanceTracking, useDebouncedValue, useThrottledCallback, useDebouncedCallback, usePrevious, useStableCallback, useLazyValue, useStableArray, useStableObject, useIntersectionObserver, useWindowDimensions.
Optimized List & Utilities
src/lib/performance/OptimizedList.tsx
New OptimizedList component (FlatList wrapper with fixed-item optimizations), createMemoizedRenderItem, withListItemMemo, defaultListItemPropsAreEqual, and related types (OptimizedListProps).
Performance Module Exports
src/lib/performance/index.ts, src/lib/index.ts
New performance index re-exports; src/lib/index.ts adds exports for monitor, hooks, OptimizedList and related types/functions.
Tests — Performance
src/lib/__tests__/OptimizedList.test.tsx, src/lib/__tests__/performance-hooks.test.ts, src/lib/__tests__/performance.test.ts
New comprehensive test suites covering OptimizedList, hooks, and PerformanceMonitor behaviors, timing, and reporting (mocks and fake timers).
Editor Theming
app/settings/editor.tsx
Centralized theme helpers: Theme type, themeColors mapping, getThemeColor; replaced inline theme branching for preview/token colors with centralized lookups.
File Tree UI
src/components/code/FileTree.tsx
Added FileTreeNodeRow component, getStatusColor/getStatusLabel helpers, conditional status badge rendering, and organized press handling for folder expansion/file selection.
Type Safety Fix
src/lib/error-handler.ts
Replaced permissive any cast with unknown and narrowed shape when accessing global ErrorUtils; removed ESLint disable.

Sequence Diagram(s)

sequenceDiagram
    participant Component as React Component
    participant Hook as useRenderTime / useMountTime
    participant Monitor as PerformanceMonitor
    participant Logger as Logger

    Component->>Hook: mount / render
    Hook->>Monitor: trackMount(componentName, duration)
    Hook->>Monitor: trackRender(componentName, duration)
    Monitor->>Monitor: record metrics & update componentStats
    Monitor->>Monitor: evaluate slow threshold
    alt slow component detected
        Monitor->>Logger: warn about slow component (details)
    end
    Monitor->>Monitor: accumulate metrics
    Note over Monitor: startReporting() triggers periodic reportMetrics()
    Monitor->>Logger: info(report summary)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hop on metrics, with whiskers keen,
I time each render, every scene,
Debounce my thoughts, throttle my pace,
Optimized lists in a leaf-lined race,
Smooth frames hum — a carrot-sweet grace.

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR partially addresses issue #17 by adding performance monitoring (PerformanceMonitor class, render/mount time tracking, frame rate tracking) and rendering optimizations (OptimizedList, memoization helpers, performance hooks), but does not fully implement bundle size reduction, network optimization, or memory management requirements. Address remaining issue #17 requirements in subsequent PRs: bundle size optimization, network optimization (batching, caching, retry), memory leak detection, and performance documentation.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(performance): add performance monitoring and optimization utilities' directly reflects the main changes: introduction of PerformanceMonitor, performance hooks, and OptimizedList component.
Out of Scope Changes check ✅ Passed All changes are within scope for performance optimization: the PerformanceMonitor, hooks, OptimizedList, error handler type improvement, and FileTree/EditorSettings refactoring for theme management are all directly related to performance monitoring and optimization objectives.
Docstring Coverage ✅ Passed Docstring coverage is 94.44% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

- Add 26 tests for performance hooks (useRenderTime, useMountTime, useDebouncedValue, useThrottledCallback, useDebouncedCallback, usePrevious, useStableCallback, useLazyValue, useStableArray, useStableObject, useWindowDimensions)
- Add 15 tests for OptimizedList component and helper functions
- Increase test coverage for new performance module

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Comment thread src/lib/__tests__/performance-hooks.test.ts Fixed
Comment thread src/lib/__tests__/performance-hooks.test.ts Fixed
Comment thread src/lib/__tests__/performance-hooks.test.ts Fixed
- Remove unused imports (React, waitFor)
- Fix import ordering
- Apply Biome formatting

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@jbdevprimary
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 18, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

jbdevprimary and others added 2 commits January 18, 2026 16:27
- Add tests for startReporting/stopReporting
- Add tests for disabled monitor behavior
- Add tests for slow render and mount logging
- Add tests for FPS metrics in getSummary
- Improve coverage from 78% to target 80%+

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Refactor EditorSettingsScreen to use theme color helper function
  reducing cognitive complexity from 33 to under 15
- Extract FileTreeNode helper components to reduce complexity from 22 to under 15
- Fix any type usage in error-handler.ts using unknown cast
- Fix test file formatting issues

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/lib/performance/hooks.ts`:
- Around line 239-252: The effect currently depends on a potentially new inline
options object each render, causing reruns; fix by stabilizing options before
useEffect: either memoize the incoming options inside the hook (e.g., create
stableOptions with useMemo or normalize via useRef) and use stableOptions in the
dependency array of the useEffect that creates the IntersectionObserver
(referencing elementRef, setIsVisible, and options), or change the API to
accept/extract individual primitive option fields (threshold, root, rootMargin)
and list those primitives in the effect deps so the observer only resets when
actual option values change; ensure observer.observe(element) and
observer.disconnect() still run against the stabilized options.

In `@src/lib/performance/OptimizedList.tsx`:
- Around line 22-23: The isLoadingMore prop on the OptimizedList component is
declared but unused; either remove it from the component's props/interface or
implement the footer loading UI: inside the OptimizedList functional component
(search for OptimizedList and its props/interface), wire isLoadingMore to the
FlatList/List's ListFooterComponent (or renderFooter function) so when
isLoadingMore is true the footer shows a loading indicator (ActivityIndicator or
similar) and when false it returns null; if you prefer to remove the API
surface, delete isLoadingMore from the props/interface and any related
docs/tests.
♻️ Duplicate comments (1)
src/lib/__tests__/performance-hooks.test.ts (1)

424-425: Remove unused variables instead of underscore-prefixing.

The _originalInnerWidth and _originalInnerHeight variables are declared but never used. The restoration logic uses originalWindow only (line 446-450). Underscore-prefixing suppresses lint warnings but doesn't address the underlying issue of dead code.

♻️ Suggested fix
     const originalWindow = global.window;
-    const _originalInnerWidth = global.window?.innerWidth;
-    const _originalInnerHeight = global.window?.innerHeight;
🧹 Nitpick comments (11)
app/settings/editor.tsx (1)

114-114: Consider typing theme state as Theme to avoid repeated casts.

The theme state is typed as string but repeatedly cast to Theme when calling getThemeColor. Typing the state properly would eliminate the casts and provide compile-time safety.

♻️ Suggested improvement
-  const [theme, setTheme] = useState('dark');
+  const [theme, setTheme] = useState<Theme>('dark');

Then remove the as Theme casts throughout the preview section.

Also applies to: 284-306

src/lib/__tests__/OptimizedList.test.tsx (1)

58-77: Consider strengthening the keyExtractor assertion.

The test verifies customKeyExtractor was called but doesn't confirm it was called for each item. A more precise assertion would verify the call count.

♻️ Suggested improvement
     // keyExtractor should be called for each item
-    expect(customKeyExtractor).toHaveBeenCalled();
+    expect(customKeyExtractor).toHaveBeenCalledTimes(mockData.length);
src/lib/__tests__/performance.test.ts (1)

67-82: Busy-wait timing test may be flaky in CI environments.

The spin-wait loop (while (Date.now() - startTime < 10)) can be unreliable under CPU contention. Consider using jest.useFakeTimers() with jest.advanceTimersByTime() if the implementation supports it, or increase the tolerance.

♻️ Alternative approach if flakiness occurs

If tests become flaky, consider mocking Date.now() or performance.now() to control timing deterministically:

it('should return a function that records duration', () => {
  const mockNow = jest.spyOn(Date, 'now');
  mockNow.mockReturnValueOnce(1000).mockReturnValueOnce(1015);
  
  const stopTiming = monitor.startTiming('timedOperation');
  const duration = stopTiming();

  expect(duration).toBe(15);
  mockNow.mockRestore();
});
src/lib/performance/monitor.ts (2)

61-69: Consider restarting reporting when config changes.

If configure() is called while reporting is active, the new reportInterval won't take effect until stopReporting()/startReporting() is called. This might be intentional, but consider documenting this behavior or auto-restarting the interval when config changes.


218-219: Fragile name-based filtering for render metrics.

Using m.name.includes('render') to filter render metrics is brittle—it would match unrelated metrics like 'prerender' or 'renderer-config'. Consider using a dedicated type field on PerformanceMetric or a specific naming convention (e.g., prefix with render:) that you control.

src/lib/performance/OptimizedList.tsx (3)

71-81: Simplify viewabilityConfig initialization.

Using useRef(...).current for a constant object is slightly awkward. Consider using useMemo for consistency, or just define the config outside the component if it's truly static.

Using useMemo
-  // Viewability config for optimization
-  const viewabilityConfig = useRef({
+  // Viewability config for optimization
+  const viewabilityConfig = useMemo(() => ({
     itemVisiblePercentThreshold: 50,
     minimumViewTime: 300,
-  }).current;
+  }), []);

107-118: Document usage constraint.

This helper creates a new MemoizedComponent on each call. If called during render, it would create a new component type each render, breaking React's reconciliation. Consider adding a JSDoc note that this should be called at module level or in useMemo:

/**
 * Create a memoized render item function.
 * 
 * `@note` Call this at module level or in useMemo, not during render.
 */

133-142: Consider comparing index as well.

This comparison function ignores the index parameter. If a list item's position changes (same id, different index), the component won't re-render. This could cause issues if rendering depends on index (e.g., alternating row styles, "first"/"last" indicators).

Include index in comparison
 export function defaultListItemPropsAreEqual<T extends { id?: string | number }>(
   prevProps: { item: T; index: number },
   nextProps: { item: T; index: number }
 ): boolean {
+  if (prevProps.index !== nextProps.index) {
+    return false;
+  }
   // Compare by id if available, otherwise by reference
   if (prevProps.item.id !== undefined && nextProps.item.id !== undefined) {
     return prevProps.item.id === nextProps.item.id;
   }
   return prevProps.item === nextProps.item;
 }
src/lib/performance/hooks.ts (3)

94-101: Potential stale closure for pending timeouts.

If callback changes while a timeout is pending (lines 95-101), the scheduled execution will use the new callback reference (due to the useCallback dependency), but the pending timeout was created with the old closure. This could cause unexpected behavior.

Consider storing the callback in a ref (similar to useStableCallback) to always use the latest reference:

Use ref for latest callback
 export function useThrottledCallback<T extends (...args: unknown[]) => unknown>(
   callback: T,
   delay: number
 ): T {
   const lastCall = useRef(0);
   const lastArgs = useRef<unknown[] | null>(null);
   const timeoutId = useRef<ReturnType<typeof setTimeout> | null>(null);
+  const callbackRef = useRef(callback);
+
+  useEffect(() => {
+    callbackRef.current = callback;
+  }, [callback]);

   const throttled = useCallback(
     (...args: unknown[]): unknown => {
       const now = Date.now();
       const remaining = delay - (now - lastCall.current);

       lastArgs.current = args;

       if (remaining <= 0) {
         lastCall.current = now;
         if (timeoutId.current) {
           clearTimeout(timeoutId.current);
           timeoutId.current = null;
         }
-        return callback(...args);
+        return callbackRef.current(...args);
       }

       if (!timeoutId.current) {
         timeoutId.current = setTimeout(() => {
           lastCall.current = Date.now();
           timeoutId.current = null;
           if (lastArgs.current) {
-            callback(...lastArgs.current);
+            callbackRef.current(...lastArgs.current);
           }
         }, remaining);
       }

       return undefined;
     },
-    [callback, delay]
+    [delay]
   ) as T;

130-141: Same stale closure issue as useThrottledCallback.

If callback changes while a timeout is pending, the scheduled execution will call the old callback from the previous closure. Apply the same ref pattern as suggested for useThrottledCallback.


211-228: Render-time ref mutation is a React anti-pattern.

Mutating ref.current during the render phase (line 224) can cause issues with React's concurrent features. The useMemo usage here is also unusual—it computes a boolean but the intent is side-effect (comparison), not memoization.

Consider using useEffect for the update or restructuring to use a proper memoization pattern:

Safer implementation
 export function useStableObject<T extends Record<string, unknown>>(obj: T): T {
-  const ref = useRef<T>(obj);
-
-  const isEqual = useMemo(() => {
-    const keys1 = Object.keys(ref.current);
-    const keys2 = Object.keys(obj);
-
-    if (keys1.length !== keys2.length) return false;
-
-    return keys1.every((key) => ref.current[key] === obj[key]);
-  }, [obj]);
-
-  if (!isEqual) {
-    ref.current = obj;
-  }
-
-  return ref.current;
+  const isEqual = (a: T, b: T): boolean => {
+    const keys1 = Object.keys(a);
+    const keys2 = Object.keys(b);
+    if (keys1.length !== keys2.length) return false;
+    return keys1.every((key) => a[key] === b[key]);
+  };
+
+  const ref = useRef<T>(obj);
+  
+  // Use useMemo to derive the stable value
+  return useMemo(() => {
+    if (isEqual(ref.current, obj)) {
+      return ref.current;
+    }
+    ref.current = obj;
+    return obj;
+  }, [obj]);
 }

Comment on lines +239 to +252
useEffect(() => {
const element = elementRef.current;
if (!element) return;

const observer = new IntersectionObserver(([entry]) => {
setIsVisible(entry.isIntersecting);
}, options);

observer.observe(element);

return () => {
observer.disconnect();
};
}, [options]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential infinite loop with inline options object.

If options is passed inline (e.g., useIntersectionObserver({ threshold: 0.5 })), a new object reference is created each render, causing the effect to re-run infinitely.

Consider stabilizing the options or documenting that callers must memoize them:

Option 1: Stabilize internally
 export function useIntersectionObserver(
   options: IntersectionObserverInit = {}
 ): [React.RefObject<Element | null>, boolean] {
   const elementRef = useRef<Element | null>(null);
   const [isVisible, setIsVisible] = useState(false);
+  
+  // Stabilize options to prevent infinite loops
+  const stableOptions = useStableObject(options as Record<string, unknown>) as IntersectionObserverInit;

   useEffect(() => {
     const element = elementRef.current;
     if (!element) return;

     const observer = new IntersectionObserver(([entry]) => {
       setIsVisible(entry.isIntersecting);
-    }, options);
+    }, stableOptions);

     observer.observe(element);

     return () => {
       observer.disconnect();
     };
-  }, [options]);
+  }, [stableOptions]);
Option 2: Extract individual options as dependencies
 export function useIntersectionObserver(
-  options: IntersectionObserverInit = {}
+  options: IntersectionObserverInit = {},
 ): [React.RefObject<Element | null>, boolean] {
+  const { root, rootMargin, threshold } = options;
   const elementRef = useRef<Element | null>(null);
   const [isVisible, setIsVisible] = useState(false);

   useEffect(() => {
     const element = elementRef.current;
     if (!element) return;

     const observer = new IntersectionObserver(([entry]) => {
       setIsVisible(entry.isIntersecting);
-    }, options);
+    }, { root, rootMargin, threshold });

     observer.observe(element);

     return () => {
       observer.disconnect();
     };
-  }, [options]);
+  }, [root, rootMargin, threshold]);
🤖 Prompt for AI Agents
In `@src/lib/performance/hooks.ts` around lines 239 - 252, The effect currently
depends on a potentially new inline options object each render, causing reruns;
fix by stabilizing options before useEffect: either memoize the incoming options
inside the hook (e.g., create stableOptions with useMemo or normalize via
useRef) and use stableOptions in the dependency array of the useEffect that
creates the IntersectionObserver (referencing elementRef, setIsVisible, and
options), or change the API to accept/extract individual primitive option fields
(threshold, root, rootMargin) and list those primitives in the effect deps so
the observer only resets when actual option values change; ensure
observer.observe(element) and observer.disconnect() still run against the
stabilized options.

Comment on lines +22 to +23
/** Whether to show footer loading indicator */
isLoadingMore?: boolean;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Unused isLoadingMore prop.

The isLoadingMore prop is declared in the interface but never used in the component implementation. Either remove it or implement the loading indicator logic (e.g., via ListFooterComponent).

Option 1: Remove unused prop
 export interface OptimizedListProps<T> extends Omit<FlatListProps<T>, 'getItemLayout'> {
   /** Height of each item (required for optimization) */
   itemHeight: number;
   /** Number of items to render above/below viewport */
   windowSize?: number;
   /** Threshold for onEndReached callback */
   endReachedThreshold?: number;
-  /** Whether to show footer loading indicator */
-  isLoadingMore?: boolean;
   /** Key extractor override */
   keyExtractor?: (item: T, index: number) => string;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/** Whether to show footer loading indicator */
isLoadingMore?: boolean;
export interface OptimizedListProps<T> extends Omit<FlatListProps<T>, 'getItemLayout'> {
/** Height of each item (required for optimization) */
itemHeight: number;
/** Number of items to render above/below viewport */
windowSize?: number;
/** Threshold for onEndReached callback */
endReachedThreshold?: number;
/** Key extractor override */
keyExtractor?: (item: T, index: number) => string;
🤖 Prompt for AI Agents
In `@src/lib/performance/OptimizedList.tsx` around lines 22 - 23, The
isLoadingMore prop on the OptimizedList component is declared but unused; either
remove it from the component's props/interface or implement the footer loading
UI: inside the OptimizedList functional component (search for OptimizedList and
its props/interface), wire isLoadingMore to the FlatList/List's
ListFooterComponent (or renderFooter function) so when isLoadingMore is true the
footer shows a loading indicator (ActivityIndicator or similar) and when false
it returns null; if you prefer to remove the API surface, delete isLoadingMore
from the props/interface and any related docs/tests.

- Add tests for useIntersectionObserver hook (observe, visibility, disconnect)
- Add test for useWindowDimensions resize handler
- Add test for useThrottledCallback cleanup on unmount
- Add test for low FPS detection and warning in PerformanceMonitor
- Add test for performance reporting with slow components
- Remove unused variables from test file
- Coverage on new code should now exceed 80% threshold

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@sonarqubecloud
Copy link
Copy Markdown

@jbdevprimary jbdevprimary merged commit e44424f into main Jan 18, 2026
13 checks passed
@jbdevprimary jbdevprimary deleted the feat/issue-17-performance-optimization branch January 18, 2026 23:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Production] Optimize app performance for production

1 participant