RORK LABJP
TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the roundTOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Articles/Dev Tools
Dev Tools/2026-05-02Intermediate

When Your FlatList Starts Stuttering: Migrating Rork Apps to FlashList v2

When your Rork app's long lists start feeling sluggish, migrating to FlashList v2 makes scrolling dramatically smoother. Here is the practical migration path, taking advantage of v2's removal of estimatedItemSize.

FlashList5FlatList9Performance23React Native209Rork515

Past 300 items, scrolling suddenly feels broken

When I shipped the first version of a journaling app generated with Rork, scrolling felt buttery during testing with sample data. Once real users hit 200 or 300 entries, however, scrolling visibly stuttered, and the timeline with image cards started flashing white placeholders for a frame as you swiped down.

My first guess was image caching. The actual culprit was that FlatList had hit its rendering ceiling. Swapping the same data into FlashList v2 made scrolling smooth again, and memory usage dropped noticeably. This article shares how to think about migrating a Rork-generated app to FlashList v2, along with the specific places I got stuck.

Why FlatList slows down isn't visible from your code

FlatList is React Native's built-in list, but under the hood it inherits from VirtualizedList and works by unmounting items that scroll off-screen. The catch is that every mount triggers React's reconciliation and a layout pass on the JS thread.

If your card has an image, a Reanimated animation, and a few TouchableOpacity regions, mounting a single card eats a few milliseconds. Multiply that by twenty cards on screen and scrolling no longer keeps up with your finger.

FlashList, built by Shopify, solves this problem with cell recycling. Instead of unmounting items, it reuses existing cells and swaps in new data — the same pattern UIKit and RecyclerView use natively. v2 makes this even smarter, so you no longer need to pass estimatedItemSize; the compiler and the internal measuring logic handle that for you.

The biggest v2 change: you no longer write estimatedItemSize

Anyone who used FlashList v1 will remember estimatedItemSize as a required prop. It was a hint about the average item height for the first render, and getting it wrong caused empty space, scroll jumps, and other strange artifacts.

v2 removes this prop entirely. The migration story is genuinely "the worse your v1 experience was, the more v2 will feel like a relief."

Migration steps for a Rork-generated app

Rork templates lean on FlatList by default, so we start with installation:

# Run from your project root
npx expo install @shopify/flash-list
 
# If you also use Reanimated, keep it on the latest matching version
npx expo install react-native-reanimated

Next, replace the components that render FlatList. Here is a minimal before/after.

// Before — FlatList version
import { FlatList } from "react-native";
 
export function PostList({ posts }: { posts: Post[] }) {
  return (
    <FlatList
      data={posts}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => <PostCard post={item} />}
      // We tuned the offscreen window and initial render count,
      // but the gains were limited
      windowSize={10}
      initialNumToRender={8}
      removeClippedSubviews
    />
  );
}
// After — FlashList v2 version
import { FlashList } from "@shopify/flash-list";
 
export function PostList({ posts }: { posts: Post[] }) {
  return (
    <FlashList
      data={posts}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => <PostCard post={item} />}
      // estimatedItemSize is no longer needed in v2
      // Even with image-heavy cards, 1000-item scrolling stays smooth
    />
  );
}

The point is that you can delete most of FlatList's tuning props. windowSize, initialNumToRender, removeClippedSubviews, getItemLayout — none of those are needed. I made the mistake of leaving old props in place during migration and missed the runtime warnings, so always glance at the console once after the swap.

Lists with variable-height cards stay reliable

FlashList v2 shows the biggest improvement on lists with mixed item heights — the kind of social timeline where text-only cards sit alongside image cards.

type FeedItem =
  | { type: "text"; id: string; body: string }
  | { type: "image"; id: string; uri: string; caption: string };
 
export function Timeline({ items }: { items: FeedItem[] }) {
  return (
    <FlashList<FeedItem>
      data={items}
      keyExtractor={(item) => item.id}
      // getItemType massively improves cell recycling efficiency
      getItemType={(item) => item.type}
      renderItem={({ item }) => {
        if (item.type === "image") return <ImageCard item={item} />;
        return <TextCard item={item} />;
      }}
    />
  );
}

getItemType exists in v1 too but remains essential in v2. With it, image cards and text cards live in separate recycle pools, and you avoid the re-renders caused by mismatched DOM structure between cell types. I skipped it the first time and saw a brief flicker every time the card type changed mid-scroll, so it is well worth adding when your item types are clearly distinct.

Three places people get stuck

Sharing the traps I personally fell into so you can skip them.

First: nesting inside a ScrollView. This applies to all VirtualizedList descendants, and FlashList is no exception. Wrap a FlashList in a <ScrollView> and virtualization disables itself, mounting every item, ending up slower than FlatList. When you need a scrolling parent layout, lean on ListHeaderComponent and ListFooterComponent instead. The companion piece Resolving the VirtualizedList nesting warning covers the broader context.

Second: building keyExtractor inline. It looks harmless, but a fresh function reference each render confuses v2's recycling heuristics. Define const keyExtractor = (item: Post) => item.id outside the component, or wrap it with useCallback.

Third: mutating the data array. FlashList watches the data reference to compute diffs. When sorting or filtering, always return a new array — setItems([...items, newItem]) — never items.push(newItem). FlatList behaves the same way, but FlashList's deeper internal optimization makes mutation bugs harder to spot visually. If you store the list in Zustand, Redux, or a similar store, double-check that your selectors return stable references between renders; otherwise the recycle pool will thrash and you'll lose the gains.

Working with Reanimated and gesture-driven cards

A common Rork-app pattern is a card that supports swipe actions, scale animations, or shared element transitions powered by Reanimated. With FlatList you often had to fight the list's re-render behavior to keep gesture state stable. With FlashList v2 you mostly get this for free, but two details matter.

import Animated, { useSharedValue, useAnimatedStyle, withSpring } from "react-native-reanimated";
import { FlashList } from "@shopify/flash-list";
 
function SwipeableCard({ post }: { post: Post }) {
  const offset = useSharedValue(0);
  const cardStyle = useAnimatedStyle(() => ({
    transform: [{ translateX: withSpring(offset.value) }],
  }));
  // Reset shared values when this cell is recycled for a different item
  // by keying the shared value initialization on post.id
  return <Animated.View style={cardStyle}>{/* card body */}</Animated.View>;
}

The first detail is to initialize shared values per item, not per cell. When FlashList recycles a cell to render a different post, the component instance is preserved but the props change. If you keep gesture state in a ref, the recycled cell will inherit the previous card's swipe offset. The cleanest fix is to drive shared values from props derived from post.id, or reset them inside an effect that depends on post.id.

The second detail is to wrap heavy cards in React.memo. FlashList itself does not memoize your row component, so a parent re-render — for example, when a new item arrives at the top — can trigger renders for all visible cards. memo plus a stable keyExtractor cuts those wasted renders effectively.

Measure the improvement, don't trust the feeling

You don't have to rely on subjective impressions. The React Native Performance Monitor surfaces JS and UI thread frame rates in the top-right corner once you shake the device on a dev build and enable Show Perf Monitor.

// Lightweight scroll perf wrapper for development builds
import { InteractionManager } from "react-native";
 
export function trackScrollPerformance(label: string) {
  const start = Date.now();
  return InteractionManager.runAfterInteractions(() => {
    const elapsed = Date.now() - start;
    if (__DEV__) {
      console.log(`[perf] ${label}: ${elapsed}ms`);
    }
    return elapsed;
  });
}

In my own measurements, a 500-item image card list ran at 35–40 fps on the JS thread with FlatList. After the FlashList v2 migration, it sat at 58–60 fps consistently, and UI thread frame drops nearly disappeared even while new images were loading mid-scroll.

Migrate one component at a time, starting with the worst offender

The closing thought I want to leave you with is that you don't have to swap your whole app at once. Pick the single laggiest list, replace it, measure the win, then move to the next. That order makes every migration tangible.

If you're going to try one thing today, choose the longest-list screen in your app and apply just the minimal example above. The replacement takes under thirty minutes and you'll feel the difference the moment you reopen the app. If your cards rely on animation, pair it with a quick review of combining Reanimated 3 with Gesture Handler to push the experience further.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Dev Tools2026-06-19
When Rork-Built Lists Stutter: Designing Image Caching and Prefetch
A FlatList from Rork starts stuttering once the images pile up. Here is how I restore smoothness with expo-image caching, recyclingKey, prefetch, and a move to FlashList, with the device numbers I measured.
Dev Tools2026-05-22
Why FlatList's onEndReached Fires Multiple Times — and How to Stop It
After wiring up infinite scroll in a Rork-generated FlatList, you may notice the same paginated request hitting your API two or three times in a row. Here's why onEndReached fires more often than you expect and how to add a two-layer defense that survives production.
Dev Tools2026-05-01
The Warning You Get When You Nest a FlatList Inside a ScrollView in Rork — Patterns That Actually Fix It
When you build a screen in Rork with a header and a long list below it, you'll see the 'VirtualizedLists should never be nested' warning. Here's why it happens and the right ways to fix it using ListHeaderComponent, SectionList, and FlashList.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →