<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jahanzaib Ramzan</title>
    <description>The latest articles on DEV Community by Jahanzaib Ramzan (@jahanzaibramzan).</description>
    <link>https://dev.to/jahanzaibramzan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4109630%2F6c0d7768-3e5b-4c60-9e7f-8b2966d00aad.PNG</url>
      <title>DEV Community: Jahanzaib Ramzan</title>
      <link>https://dev.to/jahanzaibramzan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jahanzaibramzan"/>
    <language>en</language>
    <item>
      <title>Scaling Firebase for Growing Mobile Apps</title>
      <dc:creator>Jahanzaib Ramzan</dc:creator>
      <pubDate>Fri, 04 Sep 2026 11:17:47 +0000</pubDate>
      <link>https://dev.to/jahanzaibramzan/scaling-firebase-for-growing-mobile-apps-1g5f</link>
      <guid>https://dev.to/jahanzaibramzan/scaling-firebase-for-growing-mobile-apps-1g5f</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://jahanzaibramzan.com/blog/scaling-firebase-for-mobile-apps" rel="noopener noreferrer"&gt;jahanzaibramzan.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Firebase is the fastest way I know to get a mobile app from idea to store. It's also very easy to build something that works beautifully at 1,000 users and falls over — or gets expensive — at 100,000. The difference is almost never Firebase itself. It's a few data-modeling and Cloud Functions decisions that are cheap to make early and painful to change later.&lt;/p&gt;

&lt;p&gt;This is what I've learned keeping a Firebase-backed React Native app healthy as it grew from around 10,000 to more than 300,000 users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model for reads, not for tidiness
&lt;/h2&gt;

&lt;p&gt;Firestore bills per document read, and every read is a network round trip on a phone. So the question for every screen is: how many documents does this screen need to open? If the answer is "one per item in the list," you have a problem that will get worse in proportion to your success.&lt;/p&gt;

&lt;p&gt;The instinct from relational databases is to normalise: a &lt;code&gt;users&lt;/code&gt; collection, a &lt;code&gt;posts&lt;/code&gt; collection, a &lt;code&gt;comments&lt;/code&gt; collection, and join them on the client. In Firestore, that turns a feed of 20 posts into 20 post reads plus 20 author reads plus 20 comment-count queries.&lt;/p&gt;

&lt;p&gt;Instead, denormalise what the screen shows. A post document should carry the author's display name and avatar URL, the like count, and the comment count. Update those copies when the source changes — usually from a Cloud Function, so the client never has to.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// posts/{postId}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;…&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;…&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;authorId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;u_123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Ayesha&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;avatarUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;…&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;   &lt;span class="c1"&gt;// copied, not joined&lt;/span&gt;
  &lt;span class="nx"&gt;likeCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;commentCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One read per list item, and the client stays simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep counters out of the client
&lt;/h2&gt;

&lt;p&gt;Counting is the classic Firestore trap. &lt;code&gt;collection("posts").where("authorId", "==", uid).get().size&lt;/code&gt; reads every document just to count them. At scale that's slow, expensive, and racy.&lt;/p&gt;

&lt;p&gt;For anything users see constantly — likes, followers, streaks, leaderboard points — maintain a stored counter and update it server-side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Cloud Function: keep commentCount in sync&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onCommentCreated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;onDocumentCreated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;posts/{postId}/comments/{commentId}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;postRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`posts/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;postRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;commentCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FieldValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For counters that many users hit at once (a viral post, a global leaderboard), a single document caps out at roughly one write per second. Use the distributed counter pattern — a handful of shard documents summed on read — or aggregate periodically with a scheduled function. For "count all documents" cases where the number just needs to be shown, &lt;code&gt;count()&lt;/code&gt; aggregation queries are much cheaper than reading every document, though they still aren't free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Precompute the expensive views
&lt;/h2&gt;

&lt;p&gt;A weekly leaderboard that's recalculated every time someone opens the screen is a scaling problem waiting to happen. The pattern that works: compute it once, on a schedule, and write the result to a small document the app reads.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buildWeeklyLeaderboard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;onSchedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;every 15 minutes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;users&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;weeklyPoints&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;desc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;leaderboards/weekly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FieldValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;serverTimestamp&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;displayName&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;avatarUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;avatarUrl&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;points&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;weeklyPoints&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;})),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now every user opening the leaderboard costs one read, regardless of how many users you have. The same idea applies to "recommended for you", "trending", daily challenge content, and anything else that's shared across many users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design for offline from day one
&lt;/h2&gt;

&lt;p&gt;Firestore's offline persistence is enabled by default on iOS and Android, and it's one of the best reasons to use Firebase for mobile. But it only helps if your data model plays along.&lt;/p&gt;

&lt;p&gt;Keep the documents a user interacts with most — their own profile, progress, settings — small and self-contained, so they cache well and can be written offline without conflicts. Avoid patterns where a single user action must update several documents atomically from the client; move that into a Cloud Function triggered by one write.&lt;/p&gt;

&lt;p&gt;In the React Native app, read from the cache first and let the server update arrive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unsubscribe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;onSnapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;users&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;progress&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;current&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;includeMetadataChanges&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setProgress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="nf"&gt;setIsFromCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fromCache&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Users get an instant screen; you get fewer "the app is slow" reviews.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write security rules that scale with you
&lt;/h2&gt;

&lt;p&gt;Rules are evaluated on every request, and rules that read other documents (&lt;code&gt;get()&lt;/code&gt; / &lt;code&gt;exists()&lt;/code&gt;) count as reads. A rule that checks a user's role by fetching their profile document on every read effectively doubles your read bill.&lt;/p&gt;

&lt;p&gt;Two habits keep this in check. First, put authorisation data on the auth token as custom claims, set from a Cloud Function, so rules can check &lt;code&gt;request.auth.token.role&lt;/code&gt; with no extra read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;admin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="o"&gt;=**&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;allow&lt;/span&gt; &lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, structure paths so ownership is obvious from the path itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;/{document=**} &lt;/span&gt;&lt;span class="err"&gt;{
&lt;/span&gt;  &lt;span class="nx"&gt;allow&lt;/span&gt; &lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;write&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test rules with the emulator suite before deploying. A rules bug at scale is either a data leak or a wave of permission-denied crashes, and both show up in reviews.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep Cloud Functions fast and cheap
&lt;/h2&gt;

&lt;p&gt;Functions scale automatically, but each cold start adds latency and every instance costs money. The things that matter most:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pin a region close to your users and your Firestore location. Cross-region calls add hundreds of milliseconds.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;minInstances: 1&lt;/code&gt; on the handful of functions that sit in the user's critical path (login, first-screen data) so they don't cold start.&lt;/li&gt;
&lt;li&gt;Keep functions small and single-purpose. A 40 MB bundle that imports the entire Admin SDK plus half of npm starts slower than one that imports what it uses.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;onCall&lt;/code&gt; functions for client-to-server RPC rather than raw HTTPS; you get auth context and typed errors for free.&lt;/li&gt;
&lt;li&gt;Add idempotency to triggers. Firestore triggers can fire more than once; an &lt;code&gt;increment(1)&lt;/code&gt; that runs twice is a bug you'll only notice in the data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Watch the Functions dashboard for execution time and error rate per function; the slow one is usually obvious.&lt;/p&gt;

&lt;h2&gt;
  
  
  Control cost before it controls you
&lt;/h2&gt;

&lt;p&gt;A Firebase bill that surprises you is a data-model bill. Once denormalisation, stored counters, and precomputed views are in place, the remaining levers are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pagination everywhere.&lt;/strong&gt; Never load a collection without &lt;code&gt;limit()&lt;/code&gt;. Use &lt;code&gt;startAfter()&lt;/code&gt; with a cursor, not offset.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bundle static content.&lt;/strong&gt; Reference data that rarely changes (vocabulary lists, level definitions, category trees) can be shipped as a Firestore data bundle or plain JSON in Cloud Storage with a version stamp, rather than read per user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage image variants.&lt;/strong&gt; Generate resized images at upload with the Resize Images extension so phones download thumbnails, not originals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget alerts.&lt;/strong&gt; Set a Google Cloud budget with alerts at 50/80/100%. It costs nothing and it's the only way to catch a runaway loop before month-end.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Plan the migrations you'll need
&lt;/h2&gt;

&lt;p&gt;You will change your data model. Firestore has no schema migrations, so plan for it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add a &lt;code&gt;schemaVersion&lt;/code&gt; field to documents you expect to evolve.&lt;/li&gt;
&lt;li&gt;Write client code that tolerates missing fields rather than crashing on old documents.&lt;/li&gt;
&lt;li&gt;For large backfills, run a Cloud Function or a script with the Admin SDK in batches of 500 writes, with a checkpoint so you can resume.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The app I've spent the most time on has been through several of these; none were fun, but all were manageable because the versions were explicit and the client was forgiving.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Model each screen for one or two reads. Keep counters and aggregates in Cloud Functions. Precompute anything shared across users. Design documents to cache and write offline. Keep rules read-free. Paginate. Set budget alerts.&lt;/p&gt;

&lt;p&gt;Do those things while the app is small and Firebase will carry you a very long way. If your app is already past that point and the bill or the latency is climbing, that's the kind of audit I do — see &lt;a href="https://jahanzaibramzan.com/services" rel="noopener noreferrer"&gt;Services&lt;/a&gt; or &lt;a href="https://jahanzaibramzan.com/contact" rel="noopener noreferrer"&gt;get in touch&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>firebase</category>
      <category>reactnative</category>
      <category>database</category>
      <category>mobile</category>
    </item>
    <item>
      <title>10 Tips to Improve Performance in React Native Apps</title>
      <dc:creator>Jahanzaib Ramzan</dc:creator>
      <pubDate>Fri, 04 Sep 2026 11:16:11 +0000</pubDate>
      <link>https://dev.to/jahanzaibramzan/10-tips-to-improve-performance-in-react-native-apps-2aif</link>
      <guid>https://dev.to/jahanzaibramzan/10-tips-to-improve-performance-in-react-native-apps-2aif</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://jahanzaibramzan.com/blog/react-native-performance-tips" rel="noopener noreferrer"&gt;jahanzaibramzan.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most React Native performance problems I get called in to fix aren't exotic. They're the same handful of issues, repeated: lists that re-render everything on every keystroke, images that are ten times larger than the box they sit in, screens that fetch more data than they show, and a startup path that does far too much work before the first frame. This is the list I actually work through on client apps, roughly in the order I check them.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Measure before you touch anything
&lt;/h2&gt;

&lt;p&gt;Performance work without measurement is guessing. Before changing code, get a baseline for the three things users feel: time to first interactive screen, frame drops during scroll and animation, and the delay between a tap and visible feedback.&lt;/p&gt;

&lt;p&gt;The built-in Performance Monitor (shake → &lt;em&gt;Perf Monitor&lt;/em&gt;) shows JS and UI frame rates. For anything deeper use Flipper with the React DevTools plugin, or the React Native DevTools profiler. On Android, &lt;code&gt;adb shell dumpsys gfxinfo &amp;lt;package&amp;gt;&lt;/code&gt; gives frame timing without any extra tooling.&lt;/p&gt;

&lt;p&gt;Write the numbers down. Then when you apply one of the fixes below, you'll know whether it helped or whether you just made the code more complicated.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Replace FlatList with FlashList for long lists
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;FlatList&lt;/code&gt; recreates list item components as they scroll into view. On a feed with hundreds of items and images, that's the single biggest source of dropped frames I see. Shopify's &lt;code&gt;@shopify/flash-list&lt;/code&gt; recycles item views instead, and on the same data it typically cuts scroll jank dramatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FlashList&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@shopify/flash-list&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FlashList&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;renderItem&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PostCard&lt;/span&gt; &lt;span class="na"&gt;post&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;estimatedItemSize&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;keyExtractor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one thing it needs is &lt;code&gt;estimatedItemSize&lt;/code&gt; — an honest guess at the average row height. Get that roughly right and the rest is a drop-in replacement. If you can't migrate yet, at least set &lt;code&gt;windowSize&lt;/code&gt;, &lt;code&gt;maxToRenderPerBatch&lt;/code&gt;, and &lt;code&gt;removeClippedSubviews&lt;/code&gt; on &lt;code&gt;FlatList&lt;/code&gt; — but migrate.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Stop list items from re-rendering
&lt;/h2&gt;

&lt;p&gt;Even with FlashList, if every row re-renders when unrelated state changes, you're back to jank. Two habits fix most of it.&lt;/p&gt;

&lt;p&gt;Wrap row components in &lt;code&gt;React.memo&lt;/code&gt;, and make sure the props you pass are stable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PostCard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PostCard&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onPress&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Pressable&lt;/span&gt; &lt;span class="na"&gt;onPress&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;onPress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Pressable&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// In the parent — stable callback, not a new arrow function per render&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handlePress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Post&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The common mistake is memoizing the child and then passing it a fresh &lt;code&gt;{ style: {...} }&lt;/code&gt; object or inline function on every render, which defeats the memo entirely. If a memoized component still re-renders, the React DevTools "Highlight updates" toggle will show you which prop changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Size images for the box they fill
&lt;/h2&gt;

&lt;p&gt;A 4000×3000 photo rendered into a 120×90 thumbnail still has to be decoded at full size. Multiply that by a scrolling list and you've got memory pressure, decode stalls, and OOM crashes on low-end Android.&lt;/p&gt;

&lt;p&gt;Serve images at roughly the display size (2× for retina is fine), and cache them on disk. &lt;code&gt;expo-image&lt;/code&gt; handles caching, placeholders, and transitions well; &lt;code&gt;react-native-fast-image&lt;/code&gt; does the same on bare RN.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Image&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expo-image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Image&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;thumbUrl&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// a 240px variant, not the original&lt;/span&gt;
  &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;contentFit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"cover"&lt;/span&gt;
  &lt;span class="na"&gt;cachePolicy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"memory-disk"&lt;/span&gt;
  &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;blurhash&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your backend is Firebase Storage, generate resized variants at upload time with the Resize Images extension rather than resizing on the client.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Make sure Hermes is on, and check your bundle
&lt;/h2&gt;

&lt;p&gt;Hermes has been the default engine for a while, but I still find apps that turned it off during a migration and never turned it back on. Confirm it in &lt;code&gt;android/gradle.properties&lt;/code&gt; (&lt;code&gt;hermesEnabled=true&lt;/code&gt;) and the iOS Podfile, then check at runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isHermes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;global&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;HermesInternal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While you're there, look at what's actually in your JS bundle. &lt;code&gt;npx react-native-bundle-visualizer&lt;/code&gt; shows it as a treemap. Every time I run it on a client project there's something surprising — a full &lt;code&gt;lodash&lt;/code&gt; import for two functions, &lt;code&gt;moment&lt;/code&gt; with every locale, an icon library pulling in thousands of glyphs. Swapping to &lt;code&gt;lodash/debounce&lt;/code&gt;, &lt;code&gt;date-fns&lt;/code&gt;, and a tree-shakeable icon set can cut startup by hundreds of milliseconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Move work off the startup path
&lt;/h2&gt;

&lt;p&gt;Cold start is where apps feel slowest, and it's usually self-inflicted. The pattern to look for: the root component fetching remote config, hydrating a persisted store, initialising analytics, checking auth, and registering push notifications — all before rendering anything.&lt;/p&gt;

&lt;p&gt;Split it into what the first screen actually needs and what can wait:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Needed to render the first screen&lt;/span&gt;
  &lt;span class="nf"&gt;restoreAuthSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Everything else after first paint&lt;/span&gt;
  &lt;span class="nx"&gt;InteractionManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runAfterInteractions&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;initAnalytics&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;registerForPushNotifications&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;prefetchSecondaryData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Persisted state (Redux Persist, Zustand's &lt;code&gt;persist&lt;/code&gt;) should only include what you need on launch; a 2 MB cached feed in AsyncStorage is a startup cost, not a feature. Lazy-load heavy screens with &lt;code&gt;React.lazy&lt;/code&gt; so their code isn't parsed until someone navigates there.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Keep animations on the UI thread
&lt;/h2&gt;

&lt;p&gt;Any animation driven by JS state updates competes with everything else the JS thread is doing, so it stutters the moment a list scrolls or a network response arrives. Reanimated runs animations as worklets on the UI thread and stays smooth under load.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;progress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSharedValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAnimatedStyle&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;translateY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withTiming&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For gestures, pair it with &lt;code&gt;react-native-gesture-handler&lt;/code&gt; so the whole interaction stays off the JS thread. For the old &lt;code&gt;Animated&lt;/code&gt; API, &lt;code&gt;useNativeDriver: true&lt;/code&gt; gets you part of the way for opacity and transforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Fetch less, cache more
&lt;/h2&gt;

&lt;p&gt;Slow screens are often just waiting on the network. Three things help.&lt;/p&gt;

&lt;p&gt;First, don't fetch on every focus. TanStack Query (or SWR) gives you stale-while-revalidate for free: show cached data immediately, refresh in the background.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;post&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fetchPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;staleTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, fetch only what the screen shows. A list needs titles and thumbnails, not the full body of every item.&lt;/p&gt;

&lt;p&gt;Third, if you're on Firestore: enable offline persistence (it's on by default on mobile), model data so a screen is one or two reads rather than a fan-out of N queries, and precompute counters and aggregates in Cloud Functions instead of counting on the client. Firestore bills per document read and each read is a round trip — both add up fast on a feed.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Watch for expensive re-renders from context and stores
&lt;/h2&gt;

&lt;p&gt;A single &lt;code&gt;React.Context&lt;/code&gt; holding the whole app state means every consumer re-renders whenever any field changes. The same applies to selecting the whole store from Redux or Zustand.&lt;/p&gt;

&lt;p&gt;Split contexts by how often they change (auth vs. theme vs. feed data), and select narrowly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Re-renders only when `unreadCount` changes&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useStore&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unreadCount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Re-renders on every store update — avoid&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useStore&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;why-did-you-render&lt;/code&gt; is worth adding in development for a week; it's blunt but it finds these fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Test on the phone your users actually have
&lt;/h2&gt;

&lt;p&gt;Everything above will look fine on an iPhone 15 or a Pixel 8. Most of the world isn't on those. Keep one genuinely cheap Android device — something with 3–4 GB of RAM from a few years ago — and make it part of the QA pass before every release. Enable &lt;em&gt;Don't keep activities&lt;/em&gt; in developer options once in a while too; it exposes state-restoration bugs that only appear when the OS kills your app in the background.&lt;/p&gt;

&lt;p&gt;Release builds only. Debug builds run the JS through the Metro dev server with extra checks, and are not representative of what users get.&lt;/p&gt;

&lt;h2&gt;
  
  
  A workflow that sticks
&lt;/h2&gt;

&lt;p&gt;The fixes above are fairly mechanical. What keeps an app fast is doing them consistently: set a performance budget (for example, first screen under 1.5 s on the test device, no dropped frames on the main feed), check it in CI or at least before each release, and treat a regression like a bug rather than a polish task for later.&lt;/p&gt;

&lt;p&gt;I've applied this checklist on apps ranging from a Quran-learning app with 300K+ users on Firebase to solo Android projects, and the outcome is usually the same: the first two or three items fix most of the complaints, and the rest keep them from coming back.&lt;/p&gt;

&lt;p&gt;If you have a React Native app that has gotten slow and you'd like a second pair of eyes on it, I do technical audits — see &lt;a href="https://jahanzaibramzan.com/services" rel="noopener noreferrer"&gt;Services&lt;/a&gt; or &lt;a href="https://jahanzaibramzan.com/contact" rel="noopener noreferrer"&gt;get in touch&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>performance</category>
      <category>mobile</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
