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:
- Fluidity: The UI should scroll and animate at 60fps without stuttering
- 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
handleFocusdirectly 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:
- Look for the
frametrack under your app’s process - Long frames appear as wider bars
- Zoom in on areas where frames are longer than 16.67ms

Correlating JS Work with Frame Drops
The key insight from Perfetto is the correlation between JS thread blocking and frame drops:
- User presses D-pad RIGHT
handleFocus()is calledsimulateBlockingWork(50)blocks JS thread for 50ms- Non-native animation adds more JS work
- During this time, 3-4 frames can’t be rendered
- 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