Vega App Performance Series, Part 2: Investigate Performance Issues

Introduction

In Part 1, we covered the key metrics for measuring fluidity and responsiveness in Vega apps: UI fluidity, key pressed/released latency, and how to use the KPI Visualizer. Now let’s put that knowledge to investigate performance issues.

This article walks through a real example of identifying performance issues that impact both fluidity and responsiveness. We’ll examine a sample app with lower performance to see exactly how common anti-patterns impact metrics, then use Perfetto traces to diagnose root causes. In Part 3, we’ll cover the fixes and optimization techniques.

Sample Code: The examples in this series are based on the Vega Developer Workshop for TV Apps repository. The README includes a hands-on workshop you can follow along with if you’d like to experiment with these patterns yourself.


Investigating Fluidity and Responsiveness Issues

Let’s walk through a real example using a sample video streaming app with a home screen that displays content in horizontally scrolling rows. This single example demonstrates anti-patterns that impact both fluidity (smooth scrolling) and responsiveness (fast reaction to D-pad input).

Understanding the Problem

When users navigate through content rows with the D-pad, two things need to happen smoothly:

  1. Fluidity: The UI should scroll and animate at 60fps without stuttering
  2. Responsiveness: Focus should move immediately when a button is pressed

Both can be degraded by the same root cause: blocking the JavaScript thread. When the JS thread is busy, frames get dropped (impacting fluidity) and input events queue up (impacting responsiveness).

Anti-Pattern Based Implementation

Here’s a home screen implementation with anti-patterns that cause both fluidity and responsiveness issues:

import {FlatList, Animated} from 'react-native';

/**
 * DEGRADED HomeScreen - demonstrates anti-patterns affecting:
 * - UI Fluidity (dropped frames during scrolling)
 * - Key Pressed/Released Latency (slow response to D-pad input)
 */

const HomeScreenDegraded = ({navigation}) => {
  // ... data fetching code ...

  return (
    <ScrollView style={styles.container}>
      {contentRows.map((row, rowIndex) => (
        <ContentRowComponentDegraded
          key={row.title}
          row={row}
          onItemPress={handleItemPress}
        />
      ))}
    </ScrollView>
  );
};

// ANTI-PATTERN: Component not wrapped in React.memo
const ContentRowComponentDegraded = ({row, onItemPress}) => {
  return (
    <View style={styles.rowContainer}>
      <Text style={styles.rowHeader}>{row.title}</Text>
      {/* ANTI-PATTERN: FlatList without optimization props */}
      <FlatList
        data={row.items}
        horizontal
        showsHorizontalScrollIndicator={false}
        // MISSING: getItemLayout - forces measurement of each item
        // MISSING: windowSize, initialNumToRender, maxToRenderPerBatch
        renderItem={({item, index}) => (
          <MovieCardDegraded
            item={item}
            onPress={() => onItemPress(item)} // ANTI-PATTERN: Anonymous function
            extraData={{timestamp: Date.now()}} // ANTI-PATTERN: New object every render
          />
        )}
        keyExtractor={(item) => item.id}
      />
    </View>
  );
};
// ANTI-PATTERN: Component not wrapped in React.memo
const MovieCardDegraded = ({item, onPress, extraData}) => {
  const [focused, setFocused] = useState(false);
  const scaleAnim = useRef(new Animated.Value(1)).current;

  /**
   * ANTI-PATTERN: Blocking work + non-native animation in focus handler
   *
   * Real scenario: Developer checks if user has access to content on focus,
   * and adds a scale animation for visual feedback.
   */
  const handleFocus = () => {
    setFocused(true);

    // BAD: Simulating synchronous entitlement/subscription check (50ms)
    simulateBlockingWork(50);

    // BAD: Non-native animation - runs on JS thread
    Animated.spring(scaleAnim, {
      toValue: 1.05,
      useNativeDriver: false, // Blocks JS thread!
      friction: 8,
    }).start();
  };

  const handleBlur = () => {
    setFocused(false);
    simulateBlockingWork(20); // BAD: More blocking work

    Animated.spring(scaleAnim, {
      toValue: 1,
      useNativeDriver: false,
      friction: 8,
    }).start();
  };

  // ANTI-PATTERN: Inline style computation on every render
  const getCardStyle = () => [
    styles.card,
    focused && styles.cardFocused,
    focused && {shadowColor: '#fff', shadowOpacity: 0.5, shadowRadius: 8},
  ];

  return (
    <Animated.View style={{transform: [{scale: scaleAnim}]}}>
      <Pressable
        style={getCardStyle()}
        onFocus={handleFocus}
        onBlur={handleBlur}
        onPress={onPress}>
        <Image source={{uri: item.images.thumbnail}} style={styles.thumbnail} />
      </Pressable>
    </Animated.View>
  );
};

Anti-Patterns by KPI Impact

Fluidity Issues (dropped frames, low FPS):

Anti-Pattern Why It Impacts Fluidity
FlatList without getItemLayout Forces measurement of each item during scroll
Missing windowSize, initialNumToRender Renders too many items, overwhelming the render thread
No React.memo on components Unnecessary re-renders during navigation
New object references as props Triggers child re-renders even when data hasn’t changed

Responsiveness Issues (slow focus response, input lag):

Anti-Pattern Why It Impacts Responsiveness
Blocking work in focus handlers (50ms) JS thread can’t process next input until work completes
Non-native animations (useNativeDriver: false) Animation calculations block the JS thread
Inline style computation Creates new objects on every render, adding GC pressure

Measuring the Impact

Degraded Implementation Results:

App Event Response Time - Focus:     0.5ms - 41.4ms (mean: 5.2ms)
3+ Consecutive Dropped Frames:       19 instances
5+ Consecutive Dropped Frames:       4-6 instances
Fluidity %:                          96.8% - 97.2%
GWSI Average FPS:                    49-50 fps
GWSI Animating FPS:                  51-52 fps

The results show:

  • 41.4ms max focus response time: The 50ms blocking work in handleFocus directly causes this
  • 19 instances of 3+ consecutive dropped frames: Frames are being dropped during navigation
  • ~97% fluidity: Below the 99%+ target
  • ~50 fps: Well below the 60fps target

Analyzing with Perfetto

Every KPI Visualizer test generates Perfetto trace files in the output directory:

output/2025-12-09_11-43-11/
├── iter_1_vs_trace    # Perfetto trace for iteration 1
├── iter_2_vs_trace    # Perfetto trace for iteration 2
└── iter_3_vs_trace    # Perfetto trace for iteration 3

Open a trace in Perfetto UI or click it in Vega Studio.

What to Look For: UIManagerBinding::dispatchEvent

The most important slice to examine in this case is UIManagerBinding::dispatchEvent. This shows how long each focus/blur event takes to process on the JS thread.

Degraded trace analysis:

Metric Value
Max event duration 52.66ms
Avg event duration 17.39ms
Focus events (~50ms) 21 events
Blur events (~22ms) 4 events

The trace shows focus events taking 50-52ms: this directly corresponds to the 50ms simulateBlockingWork() call in handleFocus(). Blur events take ~22ms, matching the 20ms blocking work in handleBlur().

What to Look For: Frame Timing

For fluidity issues, examine the frame slices:

Metric Value
Total frames 468
Frames over 16ms 6
Frames over 33ms 1
Frames over 50ms 1
Max frame duration 62.94ms
Avg frame duration 3.11ms

A frame taking 62.94ms means approximately 4 frames were dropped (62.94ms / 16.67ms ≈ 4). This correlates with the “5+ Consecutive Dropped Frames” KPI.

How to find this in Perfetto:

  1. Look for the frame track under your app’s process
  2. Long frames appear as wider bars
  3. Zoom in on areas where frames are longer than 16.67ms

frame_perfetto

Correlating JS Work with Frame Drops

The key insight from Perfetto is the correlation between JS thread blocking and frame drops:

  1. User presses D-pad RIGHT
  2. handleFocus() is called
  3. simulateBlockingWork(50) blocks JS thread for 50ms
  4. Non-native animation adds more JS work
  5. During this time, 3-4 frames can’t be rendered
  6. User sees stuttering and delayed focus movement

Root cause identified: Synchronous blocking work and non-native animations in focus handlers blocking the JS thread, causing both dropped frames (fluidity) and delayed input processing (responsiveness).

For more on trace analysis, see Inspect Traces and Investigate JavaScript Thread Performance.


What’s Next

Now that you know how to identify and diagnose performance issues, head over to Part 3: Optimizing for Fluidity and Responsiveness where we’ll cover:

  • The optimized implementation that fixes these issues
  • Why Vega’s Carousel component outperforms FlatList
  • Best practices for keeping your app performant

Additional Resources

Performance Measurement

Debugging Tools


Last updated: December 2025