- Published on
Building a High-Performance Photo Swiper with React Native (hackathon journey)
The Challenge
During a 3-day hackathon with friends, I decided to build a photo management app that would help users sort through their photo library using a Tinder-like swiping interface. The goal was straightforward: present photos by month/year, let users swipe to keep or delete, and provide a confirmation screen before permanent deletion.
What made this project interesting was the performance constraint: the app needed to handle 1000+ photos smoothly without external gesture libraries or complex state management solutions.
The Technical Approach
Pure React Native Implementation
Instead of relying on third-party gesture libraries like react-native-gesture-handler
or react-native-reanimated
for the core swiping logic, I built the entire gesture system using React Native’s built-in PanResponder
and Animated
APIs. This decision wasn’t about reinventing the wheel—it was about having complete control over performance optimizations.
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: (event) =>
!!disableMultiTouch || event.nativeEvent.touches.length === 1,
onMoveShouldSetPanResponder: (event) =>
!!disableMultiTouch || event.nativeEvent.touches.length === 1,
onPanResponderMove: (_event, gestureState) => {
position.setValue({ x: gestureState.dx, y: gestureState.dy });
},
onPanResponderRelease: (_event, gestureState) => {
// Handle swipe logic with custom thresholds
},
}),
).current;
Optimized Rendering Strategy
The key performance insight was implementing a selective rendering system that only renders the current card and the next card in the stack:
const renderItems = useCallback(() => {
return items
.map((item, i) => {
if (i < currentIndex || i > currentIndex + Math.abs(prerenderItemsCount)) {
return null; // Skip rendering off-screen items
}
const isCurrent = i === currentIndex;
const isNext = i === currentIndex + 1;
const isOther = !isCurrent && !isNext;
return (
<Animated.View
{...(isCurrent ? panResponder.panHandlers : {})}
key={item.id}
style={[
styles.card,
isCurrent && rotateAndTranslate,
isNext && { opacity: nextCardOpacity, transform: [{ scale: nextCardScale }] },
isOther && styles.otherCard,
]}
>
{renderItem(item)}
</Animated.View>
);
})
.reverse();
}, [/* optimized dependencies */]);
This approach ensures that even with 1000+ photos, only 2-3 components are actually rendered at any given time.
Custom Animation Interpolations
Rather than using predefined easing functions, I implemented custom interpolations for smooth visual feedback:
const rotate = useMemo(
() =>
position.x.interpolate({
inputRange: [-width / 2, 0, width / 2],
outputRange: ['-25deg', '0deg', '20deg'],
extrapolate: 'clamp',
}),
[position.x, width],
);
const nextCardScale = useMemo(
() =>
position.x.interpolate({
inputRange: [-width / 2, 0, width / 2],
outputRange: [1, 0.8, 1],
extrapolate: 'clamp',
}),
[position.x, width],
);
Efficient State Management
For handling the photo library data, I implemented a custom Zustand store with optimized data structures:
// Hierarchical data structure: year -> month -> assets
const assets: AssetsState['assets'] = {};
allAssets.forEach((asset) => {
const year = new Date(asset.creationTime).getFullYear();
const month = new Date(asset.creationTime).getMonth() + 1;
if (!assets[year]) assets[year] = {};
if (!assets[year][month]) assets[year][month] = [];
assets[year][month].push(convertAssetToSimpleAsset(asset));
});
This hierarchical approach allows for efficient filtering and reduces memory footprint when dealing with large photo libraries.
Memory Management for Large Lists
In the review screen where users see all their decisions, I implemented aggressive list optimization:
<FlatList
windowSize={15 * 5}
initialNumToRender={15}
removeClippedSubviews={true}
getItemLayout={(_data, index) => ({
length: containerWidth ? Math.floor(containerWidth / numColumns) : 1,
offset: (containerWidth ? Math.floor(containerWidth / numColumns) : 1) * index,
index,
})}
/>
The getItemLayout
optimization is crucial for performance—it allows React Native to calculate item positions without measuring each component.
Performance Results
- Smooth 60fps animations even with 1000+ photos loaded
- Memory usage stays constant regardless of photo library size
- Instant swipe response with custom gesture handling
- Efficient photo loading with progressive rendering
Key Technical Decisions
- No external gesture libraries: Complete control over performance and behavior
- Selective rendering: Only render visible + 1 next item
- Custom interpolations: Smooth, natural-feeling animations
- Hierarchical data structure: Efficient filtering and memory usage
- Aggressive list optimization: FlatList with getItemLayout for review screen
Lessons Learned
Building a performant photo swiper without external libraries taught me that React Native’s built-in APIs are often sufficient for complex interactions when properly optimized. The key is understanding the rendering lifecycle and implementing selective rendering strategies.
For developers working on similar projects, the main takeaway is that performance optimization in React Native isn’t about adding more libraries—it’s about understanding how to minimize re-renders and implement efficient data structures.
Conclusion
The app successfully handles photo libraries of any size while maintaining smooth animations and responsive interactions. You can get the full code on this repository.
The app works perfectly and handles large photo libraries smoothly. However, I won’t be publishing it to the app stores—there are already plenty of similar photo management apps out there, and honestly, I can’t be bothered with the publishing process. The real value was in the technical challenge and the fun we had building it together during that weekend hackathon.
Sometimes the journey is more rewarding than the destination, and this project was definitely one of those cases. We learned a lot, had a great time coding together, and proved that you can build something performant and polished in just a few days when you focus on the right technical decisions.