Skip to content

Latest commit

 

History

History
547 lines (414 loc) · 14.1 KB

File metadata and controls

547 lines (414 loc) · 14.1 KB

Jest Performance Optimization Guide - Mobile Test Suite

Executive Summary

Current Performance: 38.8 seconds (1231 tests, 61 test suites) Tests per second: 31.7 tests/sec Primary bottleneck: Jest startup overhead + async operations not properly cleaned up


Detailed Performance Analysis

Test Execution Metrics (with --maxWorkers=1 --no-coverage)

Total time: 5 minutes 38 seconds (338 seconds) Actual test time: 38.8 seconds Overhead time: 299 seconds (89% of total!)

Metric Value
Test suites 61 total (57 passed, 3 failed, 1 skipped)
Individual tests 1,231 total (1,203 passed, 16 failed, 12 skipped)
Execution time 38.8s
Estimated time 49s (Jest prediction)
Average per test 31.5ms
Average per suite 636ms

Critical Issue Detected:

Jest did not exit one second after the test run has completed.
This usually means that there are asynchronous operations that weren't
stopped in your tests.

Impact: Adds ~5 minutes of "waiting for cleanup" time!


Bottleneck Analysis

1. Memory Leak / Async Cleanup Issue 🔴 CRITICAL

Problem: Jest hangs for 5+ minutes after tests complete Root cause: Asynchronous operations (timers, promises, event listeners) not properly cleaned up Impact: Makes test runs 8.7x slower than they should be

Affected areas (likely culprits):

  1. WebView tests - TrackerAuthWebView.test.jsx (678 lines)

    • Uses setTimeout in mock implementation
    • May not be properly cleaning up timers
  2. React Query hooks - Multiple files use @tanstack/react-query

    • useSyncTracker.test.js (512 lines)
    • useConnectedTrackers.test.js (301 lines)
    • useDisconnectTracker.test.js (428 lines)
    • Query client may have active subscriptions
  3. Mock implementations - jest.setup.js

    • WebView mock uses setTimeout without cleanup
    • May create dangling promises

Evidence from jest.setup.js (lines 171-177):

React.useEffect(() => {
  if (onLoadStart) onLoadStart();
  const timer = setTimeout(() => {
    if (onLoadEnd) onLoadEnd();
  }, 0);
  return () => clearTimeout(timer);  // Cleanup exists but may not run
}, []);

Fix: Add proper cleanup in afterEach hooks


2. Large Test Suites ⚠️ MODERATE

Largest test files by line count:

File Lines Type Impact
productCatalog.test.js 2,237 Utility Many test cases
voice-recording-registry-integration.test.js 1,301 Integration Complex mocking
NewProductCapture.flow.test.js 1,151 Component 7.6s runtime
mealPatterns.test.js 841 Utility Pattern matching
photo-supplement-followup.test.js 784 Integration Photo analysis

Slowest running tests (from verbose output):

  1. real-image-analysis.test.js - 9.9 seconds
  2. NewProductCapture.flow.test.js - 7.6 seconds
  3. profile.modal-flow.test.jsx - 6.9 seconds (FAILING)
  4. history.test.jsx - 6.5 seconds (FAILING)

3. Component Tests vs Unit Tests

Component tests (React Testing Library):

  • Slower: 500-7000ms per suite
  • Heavy rendering, DOM queries
  • Examples: home.test.jsx, profile.test.jsx, history.test.jsx

Unit tests (Pure functions):

  • Faster: 50-500ms per suite
  • No rendering overhead
  • Examples: All transformers/*.test.js files

Breakdown:

  • Component/Integration tests: ~15 suites (~45% of time)
  • Utility/Unit tests: ~45 suites (~20% of time)
  • Cleanup overhead: ~35% of test time (the 5-minute hang)

Root Causes of Slow Tests

1. Open Handles (Critical - 89% of total time)

What: Async operations not cleaned up Where:

  • WebView mocks (setTimeout)
  • React Query subscriptions
  • Event listeners
  • Network requests (fetch mocks)

How to detect:

npm test -- --detectOpenHandles

How to fix:

// In jest.setup.js or individual test files
afterEach(() => {
  jest.clearAllTimers();
  jest.clearAllMocks();
  // Clean up any subscriptions
});

2. React Native Rendering Overhead (Moderate)

Component tests are inherently slower:

  • Each test mounts/unmounts React components
  • React Testing Library queries (getByText, findByRole) can be slow
  • Mock implementations add overhead

Example - Slow component test:

// home.test.jsx (346 lines)
// Renders full home screen with multiple components
render(<HomeScreen />);
// Heavy DOM queries
const button = screen.getByRole('button', { name: /submit/i });

Optimization:

  • Use simpler queries (getByTestId faster than getByRole)
  • Render smaller component slices instead of full screens
  • Use shallow rendering where possible

3. Test Suite Organization

Current: All tests run every time Problem: No separation of fast vs slow tests

Suggested structure:

__tests__/
├── unit/           # Pure functions (fast - 5-10s)
├── integration/    # API mocking (medium - 10-20s)
└── component/      # React components (slow - 20-40s)

Optimization Recommendations

High Priority - Fix Open Handles (Impact: 5+ minutes saved)

1. Add global cleanup in jest.setup.js:

// jest.setup.js - Add at end
afterEach(() => {
  jest.clearAllTimers();
  jest.useRealTimers();
  jest.restoreAllMocks();
});

afterAll(() => {
  jest.clearAllTimers();
});

2. Fix WebView mock to avoid timer leaks:

// jest.setup.js - Update WebView mock
jest.mock('react-native-webview', () => {
  const React = require('react');
  const { View } = require('react-native');

  return {
    WebView: React.forwardRef(({ source, onMessage, onLoadStart, onLoadEnd, testID, ...props }, ref) => {
      const timerRef = React.useRef(null);

      React.useImperativeHandle(ref, () => ({
        injectJavaScript: jest.fn(),
        postMessage: (message) => {
          if (onMessage) {
            onMessage({ nativeEvent: { data: message } });
          }
        },
      }));

      React.useEffect(() => {
        if (onLoadStart) onLoadStart();

        // Use setImmediate instead of setTimeout (faster and safer)
        timerRef.current = setImmediate(() => {
          if (onLoadEnd) onLoadEnd();
        });

        // CRITICAL: Cleanup timer
        return () => {
          if (timerRef.current) {
            clearImmediate(timerRef.current);
          }
        };
      }, [onLoadStart, onLoadEnd]);

      return React.createElement(View, { testID: testID || 'webview', source, onMessage, ...props });
    }),
  };
});

3. Add React Query cleanup:

// In test files using React Query
import { QueryClient } from '@tanstack/react-query';

let queryClient;

beforeEach(() => {
  queryClient = new QueryClient({
    defaultOptions: {
      queries: { retry: false },
      mutations: { retry: false },
    },
  });
});

afterEach(() => {
  queryClient.clear();
  queryClient.cancelQueries();
});

Expected impact: Reduce total time from 5m38s to ~40s (88% faster)


Medium Priority - Conditional Test Running (Impact: 10-20s saved)

1. Skip component tests if no component files changed:

# .husky/pre-commit
CHANGED_FILES=$(git diff --cached --name-only)
if echo "$CHANGED_FILES" | grep -q "mobile/src/components/"; then
  npm test -- --testPathPattern="components"
else
  npm test -- --testPathIgnorePatterns="components"
fi

2. Use Jest's --onlyChanged flag:

# package.json
"scripts": {
  "test:changed": "jest --onlyChanged --no-coverage"
}

Expected impact: Run only 5-10 test suites instead of all 61 (80-90% faster for small changes)


Medium Priority - Optimize Slow Tests (Impact: 5-10s saved)

1. Reduce rendering in component tests:

// BEFORE (slow)
render(<HomeScreen />);  // Renders entire screen

// AFTER (faster)
render(<VoiceRecordingButton onPress={mockFn} />);  // Just the button

2. Use getByTestId instead of getByRole:

// BEFORE (slow)
const button = screen.getByRole('button', { name: /submit voice/i });

// AFTER (faster)
const button = screen.getByTestId('voice-submit-button');

3. Mock heavy modules:

// Mock image analysis (heavy computation)
jest.mock('@/utils/photoAnalysis', () => ({
  analyzeImage: jest.fn(() => Promise.resolve({ /* mock result */ })),
}));

Expected impact: Reduce slowest tests from 7-10s to 3-5s each


Low Priority - Parallelization (Impact: 2-5s saved)

Current: --maxWorkers=2 (running 2 suites in parallel) Optimization: Increase to --maxWorkers=4 on powerful machines

// package.json
{
  "scripts": {
    "test:ci": "jest --ci --coverage --maxWorkers=2",
    "test": "jest --maxWorkers=4"  // More parallelism for local dev
  }
}

Note: Only helps after fixing open handles issue

Expected impact: 20-30% faster (only if open handles fixed first)


Recommended Test Configuration

Fast Pre-Commit Configuration

// package.json
{
  "scripts": {
    "test": "jest",
    "test:fast": "jest --onlyChanged --no-coverage --maxWorkers=4",
    "test:ci": "jest --ci --coverage --maxWorkers=2",
    "test:debug": "jest --detectOpenHandles --verbose"
  }
}

Optimized .husky/pre-commit

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

# Get changed files
CHANGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)

# Only run mobile tests if mobile/ files changed
if echo "$CHANGED_FILES" | grep -q "^mobile/"; then
  cd mobile

  # Run only tests for changed files (much faster)
  npm run test:fast

  cd ..
fi

# ... other checks

Expected Performance After Optimization

Scenario Before After Savings
Fix open handles only 5m38s 40s 88%
+ Use --onlyChanged 5m38s 5-10s 95-98%
+ Optimize slow tests 5m38s 3-8s 96-99%
Full suite (all optimizations) 5m38s 25-30s 90%

Realistic pre-commit time (after all optimizations):

  • No mobile changes: 0s (skipped)
  • Few mobile files changed: 5-10s (only affected tests)
  • Many mobile files changed: 25-30s (full suite, but much faster)

Test Quality Issues Detected

Failing Tests (16 failures in 3 suites)

1. ConnectedDeviceCard.test.jsx - Schema mismatch

// Expected: trackerId
// Received: deviceId
// FIX: Update test to use deviceId (matches schema migration)

2. history.test.jsx - 6.5s runtime, multiple failures

  • Likely integration test with heavy mocking
  • May need updated mocks

3. profile.modal-flow.test.jsx - 6.9s runtime, multiple failures

  • Modal flow testing
  • Async operations not resolving

Recommendation: Fix these before optimizing (ensure tests actually work)


Implementation Plan

Week 1: Critical Fixes

  1. ✅ Add global cleanup in jest.setup.js (1 hour)
  2. ✅ Fix WebView mock timer leaks (30 min)
  3. ✅ Add React Query cleanup (1 hour)
  4. ✅ Run --detectOpenHandles to find remaining issues (30 min)
  5. ✅ Fix failing tests (2-3 hours)

Expected result: 5m38s → 40s (88% faster)

Week 2: Smart Test Running

  1. ✅ Add test:fast script with --onlyChanged (15 min)
  2. ✅ Update .husky/pre-commit to use conditional testing (30 min)
  3. ✅ Test on real commits (1 hour)

Expected result: 40s → 5-10s for typical commits (75-87% faster)

Week 3: Test Optimization (Optional)

  1. ⏸️ Refactor slow component tests (2-4 hours)
  2. ⏸️ Add testID attributes for faster queries (1-2 hours)
  3. ⏸️ Mock heavy modules (1 hour)

Expected result: 40s → 25-30s for full suite (25-37% faster)


Monitoring

Before Optimization

npm test 2>&1 | grep "Time:"
# Output: Time: 38.827 s, estimated 49 s
# + 5 minutes of hanging

After Optimization (Expected)

npm test 2>&1 | grep "Time:"
# Output: Time: 25-30s
# + No hanging (immediate exit)

Key Metrics to Track

  • Total test time
  • Tests per second
  • Number of open handles
  • Pass rate (should stay 100% after fixing failures)

Comparison: Jest vs Deno Tests

Metric Mobile Jest Edge Function Deno
Total tests 1,231 86
Total time 38.8s (+ 5min overhead) 0.3s
Per-test average 31.5ms 3.5ms
Startup overhead 5+ minutes Negligible
Open handles issue Yes ❌ No ✅
Parallelization maxWorkers=2 Single-threaded
Framework Jest + React Native Deno native

Why Deno is 90x faster per test:

  1. No React Native rendering overhead
  2. No heavy mocking framework
  3. Native TypeScript support (no compilation)
  4. Lightweight test runner
  5. No cleanup issues

Lesson for Jest optimization:

  • Minimize rendering (test smaller components)
  • Reduce mocking complexity
  • Ensure proper cleanup
  • Use simpler queries

Additional Resources

Debugging Tools

# Find open handles
npm test -- --detectOpenHandles

# Run single test file
npm test -- src/app/(tabs)/__tests__/home.test.jsx

# Debug slow tests
npm test -- --verbose --maxWorkers=1

# Check for memory leaks
npm test -- --logHeapUsage

Jest Performance Guides


Conclusion

The mobile Jest test suite is slow due to:

  1. Open handles causing 5+ minutes of hanging (89% of time) - CRITICAL
  2. Heavy React Native component rendering
  3. Large test suites without smart filtering
  4. Some genuinely slow tests (image analysis, etc.)

Primary recommendation: Fix open handles first (will save 5 minutes immediately), then add smart test filtering (will save another 20-30s on typical commits).

Expected final performance:

  • Pre-commit for typical changes: 5-10 seconds (vs current 5m38s)
  • Full test suite: 25-30 seconds (vs current 5m38s)
  • Overall improvement: 95-98% faster

Last Updated: February 3, 2026 Analysis Date: February 3, 2026 Current Status: 1,231 tests in 38.8s + 5min overhead = 5m38s total Target Status: 1,231 tests in 25-30s + no overhead = 30s total