Performance Architecture
Deep dive into how SectionFlow achieves smooth 60fps scrolling.
Rendering Pipeline
User Scrolls
│
▼
┌─────────────────┐
│ Scroll Event │ (throttled to 16ms)
└────────┬────────┘
│
▼
┌─────────────────┐
│ Calculate │ Which items should be visible?
│ Visible Range │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Diff Items │ What changed since last frame?
│ │ - Items leaving viewport
│ │ - Items entering viewport
└────────┬────────┘
│
▼
┌─────────────────┐
│ Recycle Cells │ Return cells for leaving items
│ │ Acquire cells for entering items
└────────┬────────┘
│
▼
┌─────────────────┐
│ Update Layout │ Position cells using transforms
│ │ (no layout recalculation)
└────────┬────────┘
│
▼
┌─────────────────┐
│ Render Frame │ Native view updates
└─────────────────┘
Key Optimizations
1. Throttled Scroll Events
SectionFlow processes scroll events efficiently:
// Default: 16ms throttle (60fps)
scrollEventThrottle={16}
Each scroll event triggers:
- Visible range calculation: O(log n) binary search
- Cell diff: O(visible items)
- Layout updates: O(changed items)
2. Binary Search for Visibility
Instead of iterating through all items:
// O(n) - slow
function findFirstVisible(items, scrollOffset) {
for (let i = 0; i < items.length; i++) {
if (items[i].offset >= scrollOffset) return i;
}
}
// O(log n) - fast
function findFirstVisible(items, scrollOffset) {
let low = 0, high = items.length - 1;
while (low < high) {
const mid = (low + high) >>> 1;
if (items[mid].offset < scrollOffset) low = mid + 1;
else high = mid;
}
return low;
}
3. Transform-Based Positioning
Cells are positioned using transforms, not layout:
// Uses GPU-accelerated transforms
<Animated.View
style={{
transform: [{ translateY: offset }],
}}
/>
Benefits:
- Avoids triggering layout recalculation
- GPU-accelerated animations
- Smooth 60fps updates
4. Batched Updates
Multiple cell changes are batched:
// Instead of:
cell1.setPosition(100); // triggers render
cell2.setPosition(200); // triggers render
cell3.setPosition(300); // triggers render
// SectionFlow does:
batchUpdate(() => {
cell1.setPosition(100);
cell2.setPosition(200);
cell3.setPosition(300);
}); // single render
5. Incremental Rendering
Large lists render incrementally:
<SectionFlow
initialNumToRender={10} // Render 10 items first
maxToRenderPerBatch={10} // Then 10 more per frame
updateCellsBatchingPeriod={50} // 50ms between batches
/>
This keeps the main thread responsive during initial load.
Sticky Header Implementation
Sticky headers use a separate overlay layer:
┌─────────────────────────────────┐
│ Sticky Header Overlay │ ◄── Fixed position
│ (renders current section │ layer
│ header) │
├─────────────────────────────────┤
│ │
│ Scroll Content │ ◄── Scrollable
│ ┌───────────────────────────┐ │ content
│ │ Section Header (hidden │ │
│ │ when sticky is showing) │ │
│ ├───────────────────────────┤ │
│ │ Item 1 │ │
│ │ Item 2 │ │
│ │ Item 3 │ │
│ └───────────────────────────┘ │
│ │
└─────────────────────────────────┘
How It Works
- Track scroll position
- Determine which section header should be sticky
- Render that header in the overlay
- Hide the original header to prevent doubling
function useStickyHeader(scrollOffset, sections, layoutCache) {
// Find section whose header should be sticky
for (let i = sections.length - 1; i >= 0; i--) {
const sectionOffset = layoutCache.getOffset(sections[i].key);
if (scrollOffset >= sectionOffset) {
return sections[i];
}
}
return null;
}
Collapsible Sections
Collapse animations are performant:
State Management
// Collapsed state is a Set for O(1) lookups
const [collapsedSections, setCollapsedSections] = useState<Set<string>>(
new Set(initialCollapsedSections)
);
// Toggle is O(1)
const toggleSection = (key: string) => {
setCollapsedSections(prev => {
const next = new Set(prev);
if (next.has(key)) next.delete(key);
else next.add(key);
return next;
});
};
Layout Updates
When a section collapses:
- Items are removed from render (cells returned to pool)
- Following sections shift up (transform update)
- Total content height updates (scroll bar adjusts)
All using transforms - no layout recalculation.
Memory Management
Cell Pool Sizing
Pools automatically size based on viewport:
const optimalPoolSize = Math.ceil(viewportHeight / estimatedItemSize) + buffer;
Pool Cleanup
Unused cells are cleaned up periodically:
// Every 30 seconds, trim pools to optimal size
setInterval(() => {
pools.forEach((pool, type) => {
if (pool.length > optimalSize) {
pool.length = optimalSize;
}
});
}, 30000);
Memory Pressure Handling
On low memory warnings:
AppState.addEventListener('memoryWarning', () => {
// Release all pooled cells
pools.clear();
// They'll be recreated as needed
});
Profiling Tips
React DevTools Profiler
- Open React DevTools
- Go to Profiler tab
- Record while scrolling
- Look for:
- Long render times (>16ms)
- Unnecessary re-renders
- Cascading updates
Native Performance Monitor
- Shake device / Cmd+D
- Enable "Performance Monitor"
- Watch for:
- JS FPS drops
- UI FPS drops
- RAM usage
Common Bottlenecks
| Issue | Cause | Solution |
|---|---|---|
| JS FPS drops | Expensive render | Memoize, simplify |
| UI FPS drops | Layout thrashing | Use transforms |
| RAM spikes | Too many cells | Reduce windowSize |
| Initial lag | Too many items | Reduce initialNumToRender |