React Native and GraphQL Performance: Building Faster Cross-Platform Mobile Apps
Optimize React Native apps with GraphQL, Apollo Client caching, typed queries, code splitting, FlatList tuning, and mobile performance patterns.
React Native and GraphQL Performance: Building Faster Cross-Platform Mobile Apps
Mobile performance is unforgiving. Users feel every slow screen, every unnecessary spinner, and every list that stutters while scrolling.
On Sourcing Genie, I worked on a React Native mobile application with 10+ screens, GraphQL data fetching, Apollo Client caching, TypeScript, Redux, and performance-focused loading patterns. One of the key wins was reducing API response time by 25% through better GraphQL integration and caching.
This guide explains the React Native and GraphQL performance patterns I use for production mobile apps.
Why GraphQL Works Well for Mobile
Mobile apps often need just enough data for each screen. REST endpoints can over-fetch or require multiple calls. GraphQL helps by letting the app request the exact fields it needs.
For mobile users, that can mean:
- Smaller payloads.
- Fewer network round trips.
- Better caching.
- Easier pagination.
- Stronger TypeScript types with generated queries.
The benefit is not automatic. GraphQL still needs careful query design and cache strategy.
Shape Queries Around Screens
Start with the screen, then design the query.
query SupplierList($search: String, $after: String) {
suppliers(search: $search, after: $after, first: 20) {
edges {
node {
id
name
category
rating
logoUrl
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
This query is intentionally small. A list screen does not need every supplier field. It needs enough data to render the list quickly and navigate to a detail screen.
Use Typed Query Hooks
Typed GraphQL queries reduce runtime errors and make refactoring safer.
const { data, loading, fetchMore, refetch } = useSupplierListQuery({
variables: {
search,
after: null,
},
fetchPolicy: "cache-and-network",
});
For mobile apps, type safety is more than developer convenience. App store releases are slower than web deployments, so catching query mistakes before release matters.
Cache for the Way Users Navigate
Mobile users move forward and backward constantly: list, detail, back to list, another detail. If the app refetches everything on every screen transition, it feels slow.
Apollo Client can keep recent data available:
const client = new ApolloClient({
uri: API_URL,
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
suppliers: {
keyArgs: ["search"],
merge(existing = { edges: [] }, incoming) {
return {
...incoming,
edges: [...existing.edges, ...incoming.edges],
};
},
},
},
},
},
}),
});
The type policy gives paginated lists predictable behavior. The cache understands how to merge pages instead of replacing the list every time fetchMore runs.
Avoid Loading Spinners for Everything
Good mobile UX separates initial loading from background refresh.
if (loading && !data) {
return <SupplierListSkeleton />;
}
return (
<SupplierList
suppliers={data?.suppliers.edges.map((edge) => edge.node) ?? []}
refreshing={loading}
onRefresh={() => refetch()}
/>
);
Skeletons work well for first load. Pull-to-refresh and subtle loading indicators work better once the user already has cached data.
Tune Lists Before They Become a Problem
Large lists are one of the most common React Native performance issues.
<FlatList
data={suppliers}
keyExtractor={(item) => item.id}
renderItem={renderSupplier}
initialNumToRender={10}
maxToRenderPerBatch={10}
windowSize={7}
removeClippedSubviews
/>
For even heavier lists, FlashList can be a strong upgrade. But before switching libraries, check the basics:
- Stable
keyExtractor. - Memoized
renderItem. - Fixed or predictable item heights.
- Lightweight row components.
- Images with defined dimensions.
Keep Images Predictable
Remote images are expensive on mobile. They affect layout, memory, and scroll smoothness.
function SupplierLogo({ uri }: { uri: string }) {
return (
<Image
source={{ uri }}
style={{ width: 48, height: 48, borderRadius: 8 }}
resizeMode="cover"
/>
);
}
Always define dimensions. Layout shifts are not just a web problem. On mobile, unstable image sizes can cause visible jumps and scroll jank.
Split Heavy Screens by Navigation
React Native apps can also benefit from code splitting, especially when a screen has heavy dependencies like charts, maps, or editors.
const AnalyticsScreen = React.lazy(() => import("./screens/AnalyticsScreen"));
Use this carefully. The priority is not splitting every screen. The priority is delaying code users may never open during a session.
Reduce Re-Renders in Form and Filter Screens
Search and filter screens often rerender too much. Keep input state local, debounce network calls, and avoid pushing every keystroke into global state.
const debouncedSearch = useDebouncedValue(search, 300);
const { data } = useSupplierListQuery({
variables: { search: debouncedSearch },
});
This protects the API and keeps the UI responsive while users type.
Performance Checklist for React Native and GraphQL
Before shipping a mobile performance pass, check:
- Are queries screen-specific and small?
- Is pagination handled in the Apollo cache?
- Does back navigation use cached data?
- Are images sized and optimized?
- Are large lists tuned?
- Are expensive screens loaded only when needed?
- Are search inputs debounced?
- Does the app behave well on slower Android devices?
- Are error and empty states designed?
The last point matters. A fast app that fails silently still feels broken.
Related Reading
Final Thoughts
React Native and GraphQL can make mobile apps faster when the architecture respects mobile constraints. Keep queries small, cache around navigation behavior, tune lists early, and avoid making users wait on data they already loaded.
That is how a cross-platform app starts to feel polished instead of merely functional.