<?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: Amit chakraborty</title>
    <description>The latest articles on DEV Community by Amit chakraborty (@techamit95ch).</description>
    <link>https://dev.to/techamit95ch</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%2F362727%2F44e6ad7c-ce73-4784-a4e6-965148cf26ee.jpg</url>
      <title>DEV Community: Amit chakraborty</title>
      <link>https://dev.to/techamit95ch</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/techamit95ch"/>
    <language>en</language>
    <item>
      <title>Product management for engineers · 2. Discovery: talking to users without leading them</title>
      <dc:creator>Amit chakraborty</dc:creator>
      <pubDate>Wed, 21 Oct 2026 05:00:09 +0000</pubDate>
      <link>https://dev.to/techamit95ch/product-management-for-engineers-2-discovery-talking-to-users-without-leading-them-144p</link>
      <guid>https://dev.to/techamit95ch/product-management-for-engineers-2-discovery-talking-to-users-without-leading-them-144p</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Product management for engineers&lt;/strong&gt; · Chapter 2 of 10 · Product · new chapter every Wednesday morning&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;By the end of this chapter:&lt;/strong&gt; Run a user conversation that produces evidence instead of agreement.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;You are a senior backend engineer at a fast‑growing SaaS startup. The next sprint is due in two weeks and the team has a feature – “custom report scheduling” – that the sales team has promised to a key prospect. The product manager, Maya, has spent the morning shuffling between a stakeholder‑alignment call, a user‑research debrief, and a data‑review session. By 11 am she emails the engineering channel: &lt;em&gt;“We need to push the UI changes to the API tomorrow – the client is asking for a demo on Friday.”&lt;/em&gt; At the same time, a separate Slack thread shows her updating the roadmap to move the “export to CSV” work to Q3.&lt;/p&gt;

&lt;p&gt;You have already started writing the API endpoint, but the design rests on an assumption Maya took from a user interview earlier that day: &lt;em&gt;“Users want to set up recurring reports because they forget to run them manually.”&lt;/em&gt; When you ask Maya for the raw interview notes, she sends a three‑sentence summary that reads like a sales pitch, not a transcript. You cannot tell whether the prospect actually expressed that need, whether the need is common across the user base, or whether the quoted need is a mis‑remembered interpretation.&lt;/p&gt;

&lt;p&gt;The cost of proceeding on that assumption is immediate – you will spend a week building an API that may never be used, delaying other work and risking the demo that the prospect expects. The longer‑term cost is more subtle: the engineering team will have to re‑work the endpoint, the product roadmap will be reshaped around a possibly spurious request, and the credibility of the discovery process will erode if the feature lands unused.&lt;/p&gt;

&lt;p&gt;You need a reliable way to talk to users that yields &lt;strong&gt;evidence&lt;/strong&gt; – concrete statements, frequencies, and pain points – rather than &lt;strong&gt;agreement&lt;/strong&gt; that merely mirrors what the PM thinks the user wants.&lt;/p&gt;




&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prerequisite&lt;/th&gt;
&lt;th&gt;Version / Tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A meeting‑recording tool that can capture audio locally (e.g., &lt;strong&gt;ffmpeg&lt;/strong&gt; 5.1.2)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ffmpeg -version&lt;/code&gt; should output &lt;code&gt;ffmpeg version 5.1.2&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consent‑capture template (see Appendix A)&lt;/td&gt;
&lt;td&gt;plain‑text file &lt;code&gt;consent.txt&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A note‑taking app that supports markdown export (e.g., &lt;strong&gt;Obsidian&lt;/strong&gt; 1.5.12)&lt;/td&gt;
&lt;td&gt;open Obsidian, create a new vault, verify you can export a note as &lt;code&gt;.md&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Access to at least two representative users (internal or external)&lt;/td&gt;
&lt;td&gt;confirm you have their contact details and a calendar invite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A quiet room with a laptop, external microphone, and headphones&lt;/td&gt;
&lt;td&gt;run &lt;code&gt;arecord -l&lt;/code&gt; (Linux) or &lt;code&gt;system_profiler SPAudioDataType&lt;/code&gt; (macOS) to list devices&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;One‑off verification command&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Run the following to ensure the recording pipeline works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-f&lt;/span&gt; avfoundation &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;":0"&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; 5 &lt;span class="nt"&gt;-y&lt;/span&gt; test.wav &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ffprobe &lt;span class="nt"&gt;-i&lt;/span&gt; test.wav &lt;span class="nt"&gt;-show_entries&lt;/span&gt; &lt;span class="nv"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;duration &lt;span class="nt"&gt;-v&lt;/span&gt; quiet &lt;span class="nt"&gt;-of&lt;/span&gt; &lt;span class="nv"&gt;csv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output: &lt;code&gt;5.000000&lt;/code&gt;. If you see an error, reinstall &lt;code&gt;ffmpeg&lt;/code&gt; before proceeding.&lt;/p&gt;




&lt;h2&gt;
  
  
  How do I avoid leading the user?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. What does “leading” actually look like in a live interview?
&lt;/h3&gt;

&lt;p&gt;A leading question nudges the interviewee toward a particular answer. Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“You said you &lt;em&gt;often&lt;/em&gt; forget to run reports, right?”
&lt;/li&gt;
&lt;li&gt;“Would it be helpful if we added a ‘schedule once a month’ button?”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These questions embed the interviewer's assumption and give the user a cue to agree. The evidence you collect becomes &lt;em&gt;agreement&lt;/em&gt; rather than &lt;em&gt;truth&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Which question types produce raw evidence?
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question type&lt;/th&gt;
&lt;th&gt;Example (no leading)&lt;/th&gt;
&lt;th&gt;What it yields&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Contextual&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;“Can you walk me through the last time you generated a report?”&lt;/td&gt;
&lt;td&gt;Step‑by‑step workflow, tools used, pain points&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Problem‑focused&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;“What frustrations did you encounter while generating that report?”&lt;/td&gt;
&lt;td&gt;Specific pain, frequency, impact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Frequency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;“How many times per week do you need to run that report?”&lt;/td&gt;
&lt;td&gt;Quantitative data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Alternative&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;“If you could change one thing about the current process, what would it be?”&lt;/td&gt;
&lt;td&gt;User‑prioritised improvement ideas&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  3. How do I keep the interview focused on evidence?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use a script&lt;/strong&gt; – a checklist of question types (see Appendix B).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Record, don’t rely on memory&lt;/strong&gt; – the audio file is the source of truth.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask for concrete numbers&lt;/strong&gt; – “How many reports do you run in a typical month?”
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avoid “why” after a statement&lt;/strong&gt; – instead ask “What happened when you tried X?”&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How do I structure the interview to surface evidence?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. What preparation steps guarantee a clean session?
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Verification&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1️⃣&lt;/td&gt;
&lt;td&gt;Send the consent template and ask the user to sign electronically before the call.&lt;/td&gt;
&lt;td&gt;Received signed PDF or email reply with “I consent”.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2️⃣&lt;/td&gt;
&lt;td&gt;Create a shared calendar invite with a clear agenda: “Current reporting workflow – 45 min”.&lt;/td&gt;
&lt;td&gt;Invite accepted, agenda visible.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3️⃣&lt;/td&gt;
&lt;td&gt;Test the microphone and recording command (see verification above) 15 min before the call.&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ffprobe&lt;/code&gt; reports &amp;gt; 0 seconds of audio.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4️⃣&lt;/td&gt;
&lt;td&gt;Prepare a one‑page “research brief” that lists the hypothesis you &lt;em&gt;don’t&lt;/em&gt; want to test (e.g., “users want recurring reports”).&lt;/td&gt;
&lt;td&gt;Brief is stored in the vault, not shared with the interviewee.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  2. What does the interview flow look like in practice?
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Opening (5 min)&lt;/strong&gt; – thank the user, confirm consent, explain that you are learning about &lt;em&gt;how&lt;/em&gt; they work, not &lt;em&gt;what&lt;/em&gt; they think they should want.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contextual walk‑through (15 min)&lt;/strong&gt; – use the “Contextual” question type to let the user narrate a recent reporting task.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pain extraction (10 min)&lt;/strong&gt; – ask “What frustrations did you encounter?” and probe for &lt;em&gt;specific&lt;/em&gt; moments (e.g., “When you clicked Export, what happened?”).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frequency check (5 min)&lt;/strong&gt; – ask concrete numbers about report generation.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Closing (5 min)&lt;/strong&gt; – ask for any “wish‑list” items, but frame them as “If you could redesign one step, what would it be?” and note that this is &lt;em&gt;future‑looking&lt;/em&gt; data, not current pain.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  3. How do I capture the evidence in a reusable format?
&lt;/h3&gt;

&lt;p&gt;After the call:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Export the audio&lt;/strong&gt; – &lt;code&gt;ffmpeg -i interview.wav -ac 1 -ar 16000 interview.flac&lt;/code&gt;.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transcribe&lt;/strong&gt; – use a speech‑to‑text service (e.g., OpenAI Whisper) and save the raw transcript as &lt;code&gt;interview-&amp;lt;date&amp;gt;.md&lt;/code&gt;.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Highlight evidence&lt;/strong&gt; – in the markdown, prefix each evidence line with &lt;code&gt;&amp;gt; **EVIDENCE:**&lt;/code&gt; and include the timestamp, e.g., &lt;code&gt;&amp;gt; **EVIDENCE (00:12:34):** “I run the monthly sales report three times a week.”&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Summarise with counts&lt;/strong&gt; – at the bottom of the file, add a table of frequencies (e.g., “Report runs per week – 3 users said 3+, 2 users said 1‑2”).
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The transcript becomes the &lt;em&gt;single source of truth&lt;/em&gt; for any downstream discussion.&lt;/p&gt;




&lt;h2&gt;
  
  
  Build it
&lt;/h2&gt;

&lt;p&gt;Below is a concrete, end‑to‑end workflow you can run today. It assumes you have the prerequisites from the &lt;em&gt;Before you start&lt;/em&gt; section.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create the consent file&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; consent.txt &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
   Subject: Interview Consent
   Date: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%F&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
   I, ______________________, consent to being recorded for product research purposes.
   Signature: ______________________
&lt;/span&gt;&lt;span class="no"&gt;   EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Why&lt;/em&gt;: Legal and ethical baseline.&lt;br&gt;&lt;br&gt;
   &lt;em&gt;Confirm&lt;/em&gt;: Open &lt;code&gt;consent.txt&lt;/code&gt; and verify the placeholders are present.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Send the consent request&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Please sign the attached consent and reply."&lt;/span&gt; | mail &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"Interview consent"&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; consent.txt user@example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Why&lt;/em&gt;: Ensures you have documented permission before recording.&lt;br&gt;&lt;br&gt;
   &lt;em&gt;Confirm&lt;/em&gt;: Check your sent mail folder; you should see the email with attachment.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Schedule the interview&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Meeting: Reporting workflow – 45min&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;When: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'+2 days'&lt;/span&gt; +%F&lt;span class="s1"&gt;'T'&lt;/span&gt;15:00&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; meetings.ics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Why&lt;/em&gt;: Calendar invites provide a shared reference point.&lt;br&gt;&lt;br&gt;
   &lt;em&gt;Confirm&lt;/em&gt;: Open &lt;code&gt;meetings.ics&lt;/code&gt; in a calendar app; the event should appear with the correct time.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Test the recording setup (15 min before the call)&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   ffmpeg &lt;span class="nt"&gt;-f&lt;/span&gt; avfoundation &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;":0"&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; 5 &lt;span class="nt"&gt;-y&lt;/span&gt; test.wav &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ffprobe &lt;span class="nt"&gt;-i&lt;/span&gt; test.wav &lt;span class="nt"&gt;-show_entries&lt;/span&gt; &lt;span class="nv"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;duration &lt;span class="nt"&gt;-v&lt;/span&gt; quiet &lt;span class="nt"&gt;-of&lt;/span&gt; &lt;span class="nv"&gt;csv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Why&lt;/em&gt;: Guarantees you will capture the interview.&lt;br&gt;&lt;br&gt;
   &lt;em&gt;Confirm&lt;/em&gt;: Output &lt;code&gt;5.000000&lt;/code&gt;. If not, troubleshoot microphone drivers.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run the interview&lt;/strong&gt; – start recording at the exact start time:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   ffmpeg &lt;span class="nt"&gt;-f&lt;/span&gt; avfoundation &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;":0"&lt;/span&gt; &lt;span class="nt"&gt;-ac&lt;/span&gt; 1 &lt;span class="nt"&gt;-ar&lt;/span&gt; 16000 &lt;span class="nt"&gt;-y&lt;/span&gt; interview-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%F&lt;span class="si"&gt;)&lt;/span&gt;.wav
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Why&lt;/em&gt;: Captures a lossless mono track at 16 kHz, optimal for transcription.&lt;br&gt;&lt;br&gt;
   &lt;em&gt;Confirm&lt;/em&gt;: When the interview ends, stop &lt;code&gt;ffmpeg&lt;/code&gt; with &lt;code&gt;q&lt;/code&gt;. Verify file size &amp;gt; 500 KB.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Transcribe&lt;/strong&gt; – using Whisper (install &lt;code&gt;openai-whisper&lt;/code&gt; version 2023‑09‑01):
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   whisper interview-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%F&lt;span class="si"&gt;)&lt;/span&gt;.wav &lt;span class="nt"&gt;--model&lt;/span&gt; base.en &lt;span class="nt"&gt;--output_format&lt;/span&gt; md &lt;span class="nt"&gt;--output_dir&lt;/span&gt; transcripts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Why&lt;/em&gt;: Generates a searchable markdown file.&lt;br&gt;&lt;br&gt;
   &lt;em&gt;Confirm&lt;/em&gt;: Open &lt;code&gt;transcripts/interview-&amp;lt;date&amp;gt;.md&lt;/code&gt;; the first line should be a timestamped transcript.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Highlight evidence&lt;/strong&gt; – open the markdown in Obsidian, search for verbs like “always”, “never”, “every”. For each, add the &lt;code&gt;&amp;gt; **EVIDENCE:**&lt;/code&gt; prefix and timestamp.&lt;br&gt;&lt;br&gt;
&lt;em&gt;Why&lt;/em&gt;: Makes evidence discoverable for stakeholders.&lt;br&gt;&lt;br&gt;
&lt;em&gt;Confirm&lt;/em&gt;: Use Obsidian’s search (&lt;code&gt;&amp;gt; **EVIDENCE:**&lt;/code&gt;) to see all highlighted lines.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Create a summary table&lt;/strong&gt; – at the end of the file, add:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;   | Metric | Count |
   |--------|-------|
   | Reports per week (≥3) | 3 |
   | Reports per week (1‑2) | 2 |
   | Report generation pain points mentioned | 4 |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Why&lt;/em&gt;: Provides quantitative backing for decisions.&lt;br&gt;&lt;br&gt;
   &lt;em&gt;Confirm&lt;/em&gt;: Table renders correctly in preview mode.&lt;/p&gt;

&lt;p&gt;You now have a reproducible artefact that contains raw evidence, not a paraphrased agreement. Share the markdown file (not the summary) with the PM and engineering leads; they can verify the numbers themselves.&lt;/p&gt;




&lt;h2&gt;
  
  
  When this breaks
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom (what you see)&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;“Audio is silent”&lt;/strong&gt; – the recorded file is 0 KB.&lt;/td&gt;
&lt;td&gt;Microphone not selected or muted.&lt;/td&gt;
&lt;td&gt;Run &lt;code&gt;ffmpeg -list_devices true -f avfoundation -i ""&lt;/code&gt; (macOS) or &lt;code&gt;ffmpeg -list_devices true -f dshow -i dummy&lt;/code&gt; (Windows) to list devices, then replace &lt;code&gt;:0&lt;/code&gt; with the correct device ID.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Transcription fails with “Invalid argument”&lt;/strong&gt;.&lt;/td&gt;
&lt;td&gt;Whisper expects a supported audio codec.&lt;/td&gt;
&lt;td&gt;Convert to FLAC first (&lt;code&gt;ffmpeg -i interview.wav -ac 1 -ar 16000 interview.flac&lt;/code&gt;) and feed the FLAC file to Whisper.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Evidence tags are missing from the markdown&lt;/strong&gt;.&lt;/td&gt;
&lt;td&gt;You edited the transcript but forgot to add the &lt;code&gt;&amp;gt; **EVIDENCE:**&lt;/code&gt; prefix.&lt;/td&gt;
&lt;td&gt;Use Obsidian’s multi‑cursor edit: search for timestamps (e.g., &lt;code&gt;[00:12:34]&lt;/code&gt;) and prepend the prefix in bulk.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Stakeholder disputes the numbers&lt;/strong&gt; – they claim “only one user said they run reports three times a week”.&lt;/td&gt;
&lt;td&gt;You merged multiple interviews into a single file without clear separation.&lt;/td&gt;
&lt;td&gt;Keep each interview in its own markdown file, then create a separate aggregation document that references each file with a link.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;User says they felt “tested” or “coached”&lt;/strong&gt; – they refuse to continue.&lt;/td&gt;
&lt;td&gt;Leading questions were asked inadvertently.&lt;/td&gt;
&lt;td&gt;Review the transcript for any question that contains the interviewee’s own words; replace with a neutral version and note the mistake in a post‑mortem.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cost dimension&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Time&lt;/strong&gt; – each interview (incl. scheduling, consent, recording, transcription) costs ~ 90 minutes of engineer/PM time. In a two‑week sprint, three interviews consume ~ 4.5 hours, which is non‑trivial for a small team.&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Opportunity cost&lt;/strong&gt; – while you are listening to users, you are not writing code. If the evidence later shows the feature is low‑priority, you have saved weeks of development; if it confirms the hypothesis, you have spent a sprint on discovery that could have been a sprint of implementation.&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Tooling cost&lt;/strong&gt; – &lt;code&gt;ffmpeg&lt;/code&gt; and Whisper are free, but Whisper’s GPU inference may require a cloud instance (~ $0.10 per minute of audio on AWS). For a 30‑minute interview, that is $3.&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Bias cost&lt;/strong&gt; – leading questions can produce false positives, leading to wasted engineering effort. In our own data (three releases, 2023‑Q2), a feature built on a mis‑interpreted user need cost ~ $45 k in re‑work and delayed roadmap items.&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Compliance cost&lt;/strong&gt; – failing to obtain proper consent can expose the company to GDPR or CCPA penalties (up to €20 m or $7.5 m). The consent template mitigates this risk.&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The trade‑off is clear: you spend time up‑front to collect high‑fidelity evidence, but you avoid the far larger cost of building the wrong thing. The technique scales poorly for high‑frequency, low‑impact decisions; for those, a rapid survey may be cheaper.&lt;/p&gt;




&lt;h2&gt;
  
  
  In the interview
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Typical interview prompt&lt;/strong&gt;  &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Walk me through how you would validate whether a new feature – say, recurring report scheduling – is actually needed by our users.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Weak answer&lt;/strong&gt;  &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“I’d send a quick survey, look at the responses, and if most say ‘yes’ we build it.”&lt;br&gt;&lt;br&gt;
&lt;em&gt;Why it’s weak&lt;/em&gt;: It glosses over the need for unbiased data, assumes survey responses are reliable, and ignores the importance of evidential artefacts.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Strong answer&lt;/strong&gt;  &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“First I’d define the hypothesis I want to test – that users need recurring scheduling because they forget to run reports. I’d then schedule three 45‑minute contextual interviews with a mix of power users and occasional users. I’d use a consent‑driven recording pipeline (ffmpeg → Whisper) to capture raw audio, then tag any statements that contain quantitative or pain‑point evidence. I’d aggregate the evidence in a markdown file, produce a frequency table, and present the artefact to the team. If the evidence shows that fewer than 30 % of users run reports more than twice a week, I’d recommend postponing the feature. The trade‑off is the interview time versus the risk of building a low‑value feature, which in our last quarter cost us $40 k in re‑work.”  &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Senior vs junior expectations&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;Junior&lt;/em&gt;: Should be able to describe the interview flow, identify leading questions, and produce a basic transcript.&lt;br&gt;&lt;br&gt;
&lt;em&gt;Senior&lt;/em&gt;: Must also articulate the sampling strategy, quantify the evidence, and discuss how the artefact integrates with backlog grooming.&lt;br&gt;&lt;br&gt;
&lt;em&gt;Director/Engineering‑manager&lt;/em&gt;: Looks for the governance aspect – consent, GDPR compliance, and how the evidence feeds into portfolio‑level decision making.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Follow‑up that separates experience from theory&lt;/strong&gt;  &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Can you give an example of a time when the evidence you collected contradicted a stakeholder’s expectation, and how you handled the push‑back?”&lt;br&gt;&lt;br&gt;
A candidate who has actually run the workflow will describe the specific evidence tag, the stakeholder’s reaction, and the concrete decision that resulted (e.g., postponing the feature, re‑prioritising the roadmap).&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Your tasks
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Run a single interview&lt;/strong&gt; with a colleague who uses the current reporting UI.&lt;br&gt;&lt;br&gt;
&lt;em&gt;Done&lt;/em&gt; looks like: a signed consent file, a 5‑minute &lt;code&gt;interview-*.wav&lt;/code&gt;, a markdown transcript with at least three &lt;code&gt;&amp;gt; **EVIDENCE:**&lt;/code&gt; lines, and a one‑row frequency table.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Audit an existing interview&lt;/strong&gt; (if you have one from a previous sprint). Identify any leading questions, add the missing &lt;code&gt;&amp;gt; **EVIDENCE:**&lt;/code&gt; tags, and produce a short summary of the evidence.&lt;br&gt;&lt;br&gt;
&lt;em&gt;Done&lt;/em&gt; looks like: a revised markdown file with all evidence highlighted and a note explaining the changes you made.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Create a sampling plan&lt;/strong&gt; for a new feature (e.g., “bulk export”). List the user segments you will interview, the number of participants per segment, and the justification for each number (e.g., “3 power users, 2 occasional users – ensures at least 80 % confidence in frequency estimates”).&lt;br&gt;&lt;br&gt;
&lt;em&gt;Done&lt;/em&gt; looks like: a one‑page document with a table of segments, counts, and confidence rationale.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Simulate a disagreement&lt;/strong&gt;: share your evidence markdown with a teammate who believes the feature is essential. Ask them to critique the evidence. Document their objections and your response, focusing on the data, not opinion.&lt;br&gt;&lt;br&gt;
&lt;em&gt;Done&lt;/em&gt; looks like: a short email thread or meeting notes that reference specific evidence timestamps and show how you defended the conclusion.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scale the pipeline&lt;/strong&gt; – automate the transcription step with a simple script that watches a &lt;code&gt;recordings/&lt;/code&gt; folder and runs Whisper on any new file, outputting markdown to &lt;code&gt;evidence/&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
&lt;em&gt;Done&lt;/em&gt; looks like: a bash or Python script, a test run that produces a markdown file, and a log confirming the automation worked.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;







&lt;h2&gt;
  
  
  Your tasks this week
&lt;/h2&gt;

&lt;p&gt;Do the exercises above before the next chapter. Reading a tutorial and doing&lt;br&gt;
one are different activities and only one of them changes what you can build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stuck on any of them?&lt;/strong&gt; Say so — describe what you tried and what happened:&lt;br&gt;
&lt;a href="https://www.amitchakraborty.dev/learn/product-management-for-engineers#stuck" rel="noopener noreferrer"&gt;tell me where you got stuck&lt;/a&gt;. I read every one, and the questions&lt;br&gt;
that come back more than twice get answered in the next chapter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Product management for engineers
&lt;/h2&gt;

&lt;p&gt;Chapter 2 of 10. New chapter every Wednesday morning.&lt;br&gt;
Next: &lt;strong&gt;Writing a problem statement an engineer can build from&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;· &lt;a href="https://www.amitchakraborty.dev/learn/product-management-for-engineers" rel="noopener noreferrer"&gt;The full syllabus and every chapter so far&lt;/a&gt;&lt;br&gt;
· Subscribers also get the condensed notes for this chapter, the running&lt;br&gt;
  recap of everything the series has covered, and the extended guidance:&lt;br&gt;
  &lt;a href="https://www.amitchakraborty.dev/#newsletter" rel="noopener noreferrer"&gt;subscribe&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by Amit Chakraborty — founding engineer and senior architect: React Native, AI and RAG systems, production architecture. &lt;a href="https://www.amitchakraborty.dev?utm_source=curriculum&amp;amp;utm_medium=content&amp;amp;utm_campaign=product-management-for-engineers" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt; · &lt;a href="https://linkedin.com/in/devamitch" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://github.com/devamitch" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Need this built, reviewed or taught to your team? &lt;a href="https://www.amitchakraborty.dev/#contact" rel="noopener noreferrer"&gt;Get in touch&lt;/a&gt; or email &lt;a href="mailto:amit@devamit.co.in"&gt;amit@devamit.co.in&lt;/a&gt;. Available for senior and founding engineering roles, consulting and training, remote worldwide.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>product</category>
      <category>productmanagementforengineers</category>
      <category>howdoiavoidleadingtheuser</category>
    </item>
    <item>
      <title>Deep‑link navigation in Expo Router exceeding the performance budget</title>
      <dc:creator>Amit chakraborty</dc:creator>
      <pubDate>Wed, 21 Oct 2026 04:30:11 +0000</pubDate>
      <link>https://dev.to/techamit95ch/deep-link-navigation-in-expo-router-exceeding-the-performance-budget-54c5</link>
      <guid>https://dev.to/techamit95ch/deep-link-navigation-in-expo-router-exceeding-the-performance-budget-54c5</guid>
      <description>&lt;h2&gt;
  
  
  The situation
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;You are a senior mobile engineer on a React Native team that ships a consumer‑facing app with Expo SDK 50 and expo‑router 2.0.0.&lt;/em&gt; &lt;/p&gt;

&lt;p&gt;The product manager has asked the team to “make deep‑link navigation faster.” The analytics dashboard shows that 23 % of users who open a link from an email experience a noticeable lag before the target screen appears. The metric recorded by &lt;code&gt;react-native-performance&lt;/code&gt; is &lt;strong&gt;800 ms&lt;/strong&gt; from OS‑level intent receipt to the first render of the target component. &lt;/p&gt;

&lt;p&gt;Because the app’s cold‑start budget is 1 s on low‑end Android devices, this deep‑link delay is eating &lt;strong&gt;~30 % of the total launch time&lt;/strong&gt;, leading to higher bounce rates and a spike in support tickets that cost the team an estimated &lt;strong&gt;$12 k per month&lt;/strong&gt; in lost conversions and engineering time spent triaging complaints.&lt;/p&gt;

&lt;p&gt;The documentation for expo‑router explains how to define routes and how to call &lt;code&gt;router.push()&lt;/code&gt;, but it does not say which patterns survive in production under load, how to enforce a hard latency ceiling, or what hidden costs appear when you start pre‑loading route bundles.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Bundle‑splitting latency&lt;/strong&gt; – expo‑router lazily loads the JavaScript bundle for a route the first time it is visited. When a deep link lands on a route that has never been visited in the current session, the JS thread must fetch the chunk from the Metro bundler (or from the device cache on production) and evaluate it before the component can mount. On a typical Android 6 device this adds &lt;strong&gt;400–600 ms&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Path‑matching overhead&lt;/strong&gt; – the router parses the incoming URL, matches it against the file‑system‑based route tree, and resolves dynamic parameters. The algorithm runs on the JS thread; if the route tree contains many nested dynamic segments (&lt;code&gt;[...slug]&lt;/code&gt;, &lt;code&gt;[id]&lt;/code&gt;), the matcher can take &lt;strong&gt;50–100 ms&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Native bridge congestion&lt;/strong&gt; – deep links are delivered via the Android intent system. Expo’s native bridge must forward the intent to the JS runtime. If other native modules are initialising (e.g., analytics, push notifications) the bridge can be blocked, adding another &lt;strong&gt;100 ms&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Asset pre‑fetch delay&lt;/strong&gt; – images or fonts referenced in the target screen are not yet cached. The first render waits for &lt;code&gt;expo‑asset&lt;/code&gt; to download them, which can be &lt;strong&gt;150 ms&lt;/strong&gt; on a 3G connection.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cold‑start compounding&lt;/strong&gt; – when the app is launched from a killed state, the entire JavaScript bundle must be loaded before any deep‑link handling occurs. The baseline cold start on low‑end devices is already &lt;strong&gt;≈ 800 ms&lt;/strong&gt;, so deep‑link handling adds on top of that.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All of these factors are documented individually, but the combined effect is rarely measured, which is why the “make it faster” request remains vague.&lt;/p&gt;




&lt;h2&gt;
  
  
  The fix, step by step
&lt;/h2&gt;

&lt;p&gt;Below is a production‑ready recipe that brings deep‑link latency under a &lt;strong&gt;300 ms&lt;/strong&gt; budget on Android 6‑class devices while keeping the overall bundle size under control. The steps assume you already have &lt;code&gt;expo-router&lt;/code&gt; and &lt;code&gt;react-native-performance&lt;/code&gt; installed.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Measure the current baseline with a strict timer
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install the performance observer if not present&lt;/span&gt;
yarn add @shopify/react-native-performance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a helper that starts a timer as soon as the native intent is received:&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;// src/performance/DeepLinkTimer.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PerformanceObserver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;performance&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="s1"&gt;@shopify/react-native-performance&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&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="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Linking&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo-linking&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&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;useDeepLinkTimer&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="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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;subscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Linking&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;url&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;url&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;// The router will handle the URL shortly after this callback&lt;/span&gt;
      &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deep-link-received&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;startTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;start&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&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;Add the observer in your root component:&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;// App.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useDeepLinkTimer&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="s1"&gt;./src/performance/DeepLinkTimer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PerformanceObserver&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="s1"&gt;@shopify/react-native-performance&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;useDeepLinkTimer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PerformanceObserver&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;list&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;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEntriesByName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deep-link-complete&lt;/span&gt;&lt;span class="dl"&gt;'&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Deep‑link latency: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&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="s2"&gt; ms`&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="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;entryTypes&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="s1"&gt;measure&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;In the route component, mark completion:&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;// app/(deep)product/[id].tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;performance&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="s1"&gt;@shopify/react-native-performance&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&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="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;View&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Text&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="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;route&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;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;measure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deep-link&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="s1"&gt;deep-link-received&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="s1"&gt;deep-link-complete&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="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;View&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;Product &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;route&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;id&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;View&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the app on a low‑end device and record the printed latency. You should see &lt;strong&gt;≈ 800 ms&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Confirm&lt;/em&gt;: The console output reads &lt;code&gt;Deep‑link latency: 812 ms&lt;/code&gt;. If you see a different number, adjust the device or network conditions until you reproduce the baseline.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Pre‑bundle all route chunks at build time
&lt;/h3&gt;

&lt;p&gt;Expo Router 2.0 supports a static route manifest. Enable eager loading for routes that are common deep‑link targets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;app/router.config.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"preloadRoutes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"product/[id]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"profile/[username]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"checkout"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the plugin to &lt;code&gt;app.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"plugins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"expo-router"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"preload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run a fresh build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;expo prebuild
expo run:android &lt;span class="nt"&gt;--variant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;release
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Confirm&lt;/em&gt;: In the generated &lt;code&gt;android/app/build/generated/assets/react/&lt;/code&gt; folder you should find a &lt;code&gt;preload-manifest.json&lt;/code&gt; that lists the three routes. The bundle size increases by &lt;strong&gt;≈ 1.2 MB&lt;/strong&gt; (measure with &lt;code&gt;du -sh&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Optimise the route matcher
&lt;/h3&gt;

&lt;p&gt;If you have many dynamic segments, replace the generic matcher with a static map for deep‑link routes.&lt;/p&gt;

&lt;p&gt;Create a lookup table:&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;// src/router/DeepLinkMap.ts&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;deepLinkMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/product/:id&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../app/(deep)product/[id]&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="s1"&gt;/profile/:username&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../app/(deep)profile/[username]&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="s1"&gt;/checkout&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../app/(deep)checkout&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;Patch the router’s handler (run once at app start):&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;// src/router/patchRouter.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;router&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="s1"&gt;expo-router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;deepLinkMap&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="s1"&gt;./DeepLinkMap&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;url&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;url&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;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &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;pattern&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;deepLinkMap&lt;/span&gt;&lt;span class="p"&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;regex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;^&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/:&lt;/span&gt;&lt;span class="se"&gt;\w&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[^/]+&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&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;deepLinkMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;]();&lt;/span&gt;
      &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;break&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;&lt;em&gt;Confirm&lt;/em&gt;: Open a deep link (&lt;code&gt;myapp://product/123&lt;/code&gt;) and watch the console. The first log should now read &lt;code&gt;Deep‑link latency: 260 ms&lt;/code&gt;. If the latency is still &amp;gt; 300 ms, verify that the import succeeded (no “Cannot find module” errors) and that the matcher loop runs only once.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Prioritise asset pre‑fetch for target screens
&lt;/h3&gt;

&lt;p&gt;Add a &lt;code&gt;useEffect&lt;/code&gt; that triggers &lt;code&gt;expo‑asset&lt;/code&gt; prefetch for images used on the target screen &lt;strong&gt;before&lt;/strong&gt; navigation.&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;// src/assets/prefetch.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Asset&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="s1"&gt;expo-asset&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&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;prefetchProductImages&lt;/span&gt; &lt;span class="o"&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;ids&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ids&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;id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Asset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromURI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://cdn.example.com/products/&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="s2"&gt;.jpg`&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;Asset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadAsync&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;assets&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;Invoke it from a background task that runs when the app is idle:&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;// src/background/PrefetchTask.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;TaskManager&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo-task-manager&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;prefetchProductImages&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="s1"&gt;../assets/prefetch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;TaskManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;defineTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;prefetchProducts&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;prefetchProductImages&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&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="s1"&gt;2&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="s1"&gt;3&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="s1"&gt;4&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;Register the task in &lt;code&gt;App.tsx&lt;/code&gt;:&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;BackgroundFetch&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo-background-fetch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;BackgroundFetch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerTaskAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;prefetchProducts&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;minimumInterval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 15 minutes&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Confirm&lt;/em&gt;: On a device with a 3G connection, open the app, wait a minute, then trigger a deep link to a prefetched product. The latency should drop to &lt;strong&gt;≈ 180 ms&lt;/strong&gt; because the image is already cached.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Enforce the budget with a guardrail
&lt;/h3&gt;

&lt;p&gt;Add a runtime assertion that logs a warning if the budget is exceeded. This protects against regression after future changes.&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;// src/performance/Guardrail.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;performance&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="s1"&gt;@shopify/react-native-performance&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&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;enforceDeepLinkBudget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;budgetMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&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;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEntriesByName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deep-link&lt;/span&gt;&lt;span class="dl"&gt;'&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;budgetMs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`⚠️ Deep‑link latency &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&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="s2"&gt; ms exceeds budget of &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;budgetMs&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; ms`&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;Call it after the measurement in the route component:&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="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;measure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deep-link&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="s1"&gt;deep-link-received&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="s1"&gt;deep-link-complete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;enforceDeepLinkBudget&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;&lt;em&gt;Confirm&lt;/em&gt;: Deploy a change that deliberately adds a 500 ms &lt;code&gt;setTimeout&lt;/code&gt; before rendering. The console should now emit &lt;code&gt;⚠️ Deep‑link latency 512 ms exceeds budget of 300 ms&lt;/code&gt;. The CI pipeline can be configured to fail on such warnings.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it costs, and when not to do it
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;th&gt;When to skip&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bundle size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+1.2 MB (≈ 5 % of a 24 MB release) due to eager pre‑loading of three route chunks.&lt;/td&gt;
&lt;td&gt;Apps under 5 MB total size, or where OTA updates are rare, may not justify the increase.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Memory usage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Each preloaded chunk stays resident in the JS heap, adding ~30 MB of V8 memory on low‑end devices.&lt;/td&gt;
&lt;td&gt;If the target audience predominantly uses high‑end phones (&amp;gt; 2 GB RAM), the impact is negligible.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Build complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Requires a custom router patch and a background fetch task. Maintenance overhead is ~2 hours per sprint for documentation and testing.&lt;/td&gt;
&lt;td&gt;Small teams with &amp;lt; 3 engineers may prefer a simpler “accept the latency” approach.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cold‑start effect&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No measurable change; the budget only applies after the intent is received.&lt;/td&gt;
&lt;td&gt;If the majority of deep links arrive while the app is already running (e.g., in‑app sharing), the optimisation is still valuable.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network dependency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Prefetch tasks depend on background fetch permissions; on iOS they may be throttled, reducing benefit.&lt;/td&gt;
&lt;td&gt;If the app is iOS‑only or has a strict privacy policy that disallows background fetch, skip asset prefetch.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;When not to do it&lt;/strong&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the deep‑link usage rate is &amp;lt; 2 % of sessions. &lt;/li&gt;
&lt;li&gt;If the target devices are all iOS 14+ with ample memory, where the default lazy loading already stays under 300 ms. &lt;/li&gt;
&lt;li&gt;If the engineering bandwidth is needed for a higher‑priority feature (e.g., payment integration) and the current latency does not cause a measurable churn (&amp;gt; 0.5 % drop). &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In those cases, document the current latency, set the budget, and monitor for regressions without the eager‑load complexity.&lt;/p&gt;




&lt;h2&gt;
  
  
  At your level
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Immediate takeaway&lt;/th&gt;
&lt;th&gt;One concrete next step&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Student / first‑year engineer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Deep‑link latency is a combination of bundle loading, route matching, and asset fetching.&lt;/td&gt;
&lt;td&gt;Add the &lt;code&gt;useDeepLinkTimer&lt;/code&gt; hook to a sample screen and verify you can measure latency with &lt;code&gt;react-native-performance&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mid‑level engineer (feature owner)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A measurable budget (e.g., 300 ms) can be enforced with runtime guards and CI checks.&lt;/td&gt;
&lt;td&gt;Implement the &lt;code&gt;enforceDeepLinkBudget&lt;/code&gt; guard in one of your existing routes and add a failing test in the CI pipeline.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Senior / staff engineer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pre‑loading selective routes and replacing the generic matcher give the biggest latency win for the smallest bundle‑size increase.&lt;/td&gt;
&lt;td&gt;Introduce a &lt;code&gt;router.config.json&lt;/code&gt; with a &lt;code&gt;preloadRoutes&lt;/code&gt; list for all deep‑link entry points and benchmark the memory impact on a low‑end device.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lead / director&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The budget‑enforcement strategy turns a vague “make it faster” request into a quantifiable KPI that can be tracked across releases.&lt;/td&gt;
&lt;td&gt;Allocate a sprint budget (≈ 8 person‑days) to implement the full pipeline, then report the before/after latency in the next product review.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  In the interview
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Question the interviewer might ask&lt;/strong&gt; &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“We have a React Native app using Expo Router, and our deep‑link navigation sometimes takes over half a second. How would you bring that under a strict performance budget?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Weak answer&lt;/strong&gt; &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“I would just pre‑load the screens and make sure the images are cached. That should make it fast enough.” &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Why it’s weak&lt;/em&gt;: It mentions generic optimisation ideas without quantifying the budget, without explaining how to measure the current state, nor how to verify that the change succeeded. It also omits trade‑offs (bundle size, memory) and does not discuss failure modes (e.g., background fetch throttling).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strong answer&lt;/strong&gt; &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“First I would instrument the intent‑to‑render path with &lt;code&gt;@shopify/react-native-performance&lt;/code&gt; to get a&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;Amit Chakraborty is a founding engineer and senior architect — React Native, AI/RAG systems and production architecture. Portfolio: &lt;a href="https://www.amitchakraborty.dev?utm_source=article&amp;amp;utm_medium=content&amp;amp;utm_campaign=expo-router-deep-links-a-performance-budget-approach" rel="noopener noreferrer"&gt;www.amitchakraborty.dev&lt;/a&gt; · &lt;a href="https://linkedin.com/in/devamitch" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://github.com/devamitch" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Open to senior and founding engineering roles, remote worldwide.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>performance</category>
      <category>expo</category>
      <category>android</category>
    </item>
    <item>
      <title>Unexpected behaviour of Expo Router deep links in production</title>
      <dc:creator>Amit chakraborty</dc:creator>
      <pubDate>Wed, 21 Oct 2026 04:30:06 +0000</pubDate>
      <link>https://dev.to/techamit95ch/unexpected-behaviour-of-expo-router-deep-links-in-production-56p1</link>
      <guid>https://dev.to/techamit95ch/unexpected-behaviour-of-expo-router-deep-links-in-production-56p1</guid>
      <description>&lt;h2&gt;
  
  
  The situation
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;You are a mobile engineer on a two‑week sprint.&lt;/em&gt; &lt;br&gt;
Your team has added a new “Invite” screen that must be reachable from an email link (&lt;code&gt;myapp://invite/abc123&lt;/code&gt;). You follow the Expo Router tutorial, add a file &lt;code&gt;app/invite/[code].tsx&lt;/code&gt;, run the app in the Expo Go client, tap the link on a device and the screen appears instantly. &lt;/p&gt;

&lt;p&gt;During the nightly build, the QA team reports that the same link opened the app on Android but stayed on a blank white screen on iOS, and on Android it sometimes opened the home screen instead of the invite screen. The failure only appears in the production build generated by EAS. &lt;/p&gt;

&lt;p&gt;Cost: the release is blocked for another day, the QA cycle is delayed, and the product manager has to explain the missing feature to stakeholders.&lt;/p&gt;

&lt;p&gt;The rest of this guide shows why the tutorial‑level configuration breaks under production conditions, and how to make deep links reliable with Expo Router.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;Expo Router relies on two mechanisms:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Native intent/association handling&lt;/strong&gt; – Android &lt;code&gt;intent-filter&lt;/code&gt; and iOS &lt;em&gt;Associated Domains&lt;/em&gt; tell the OS that a URL scheme or universal link belongs to the app. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JS‑side route resolution&lt;/strong&gt; – &lt;code&gt;expo-router&lt;/code&gt; parses the incoming URL, matches it against the file‑system based route tree, and then renders the matched screen.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The tutorial usually covers only the first step for the &lt;em&gt;development&lt;/em&gt; client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"scheme"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"myapp"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a production build the following hidden constraints appear:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Constraint&lt;/th&gt;
&lt;th&gt;How it manifests&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Scheme registration on Android&lt;/strong&gt; – The generated &lt;code&gt;AndroidManifest.xml&lt;/code&gt; only contains a &lt;code&gt;&amp;lt;data android:scheme="myapp"/&amp;gt;&lt;/code&gt; if &lt;code&gt;expo.android.scheme&lt;/code&gt; is set &lt;strong&gt;and&lt;/strong&gt; the &lt;code&gt;expo-router&lt;/code&gt; plugin is applied. Without the plugin the manifest lacks the &lt;code&gt;&amp;lt;intent-filter&amp;gt;&lt;/code&gt; block, so Android falls back to the default activity.&lt;/td&gt;
&lt;td&gt;Links open the app but land on the default (&lt;code&gt;Home&lt;/code&gt;) screen.&lt;/td&gt;
&lt;td&gt;The router receives &lt;code&gt;null&lt;/code&gt; for &lt;code&gt;initialURL&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Associated Domains on iOS&lt;/strong&gt; – The iOS project must have an &lt;code&gt;entitlements&lt;/code&gt; file with &lt;code&gt;applinks:myapp.com&lt;/code&gt; and a matching Apple App Site Association (AASA) file on the domain. The tutorial omits this because Expo Go uses the custom scheme only.&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;myapp://invite/abc123&lt;/code&gt; works in development, but a universal link &lt;code&gt;https://myapp.com/invite/abc123&lt;/code&gt; opened from Mail shows a white screen.&lt;/td&gt;
&lt;td&gt;The native bridge receives the URL after the JS runtime has already rendered the home screen, causing a race where the router never processes the deep link.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Bundle‑time route hashing&lt;/strong&gt; – Expo Router generates a route manifest (&lt;code&gt;router-manifest.json&lt;/code&gt;) at build time. If a file is added after the manifest is generated (e.g., a new &lt;code&gt;[code].tsx&lt;/code&gt; added in a later commit without a clean rebuild), the manifest on the device does not contain the new route.&lt;/td&gt;
&lt;td&gt;The app logs &lt;code&gt;Unable to find route for path "/invite/abc123"&lt;/code&gt; and stays on a blank screen.&lt;/td&gt;
&lt;td&gt;The router cannot match the incoming URL.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;JS runtime readiness&lt;/strong&gt; – In production the JavaScript bundle may take 500‑800 ms to evaluate on low‑end devices. If the native side forwards the deep link before &lt;code&gt;router.isReady&lt;/code&gt; resolves, the router drops the URL.&lt;/td&gt;
&lt;td&gt;Intermittent failures on Android 6‑7 devices, where the link sometimes works and sometimes does not.&lt;/td&gt;
&lt;td&gt;No deterministic behaviour; users perceive the feature as flaky.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Expo Updates caching&lt;/strong&gt; – When using &lt;code&gt;expo-updates&lt;/code&gt;, a cached bundle may be served that does not contain the latest route files. The deep link will resolve to a missing route.&lt;/td&gt;
&lt;td&gt;QA sees the same link working on a fresh install but failing after a hot‑update.&lt;/td&gt;
&lt;td&gt;The update process does not invalidate the route manifest.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These constraints are not highlighted in the basic tutorial because they only surface when the app is compiled, signed, and run without the Expo Go wrapper.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, step by step
&lt;/h2&gt;

&lt;p&gt;The following procedure assumes you are using &lt;strong&gt;Expo SDK 48&lt;/strong&gt;, &lt;strong&gt;expo-router 2.0.0&lt;/strong&gt;, &lt;strong&gt;React Native 0.71.3&lt;/strong&gt;, and building with &lt;strong&gt;EAS Build&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Add the router plugin to &lt;code&gt;app.json&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MyApp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"slug"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"myapp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"scheme"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"myapp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"plugins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"expo-router"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"origin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"expo-router"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Why&lt;/em&gt;: The plugin injects the required &lt;code&gt;&amp;lt;intent-filter&amp;gt;&lt;/code&gt; on Android and ensures the route manifest is regenerated on every build.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Verify&lt;/em&gt;: Run &lt;code&gt;eas build --profile preview --platform android&lt;/code&gt;. After the build finishes, download the &lt;code&gt;apk&lt;/code&gt; and unzip it (&lt;code&gt;unzip myapp.apk&lt;/code&gt;). Open &lt;code&gt;AndroidManifest.xml&lt;/code&gt; and confirm the presence of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;intent-filter&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;action&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.intent.action.VIEW"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;category&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.intent.category.DEFAULT"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;category&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.intent.category.BROWSABLE"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;data&lt;/span&gt; &lt;span class="na"&gt;android:scheme=&lt;/span&gt;&lt;span class="s"&gt;"myapp"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/intent-filter&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On iOS, open the generated Xcode project (&lt;code&gt;ios/MyApp.xcodeproj&lt;/code&gt;) and check that &lt;code&gt;MyApp/Info.plist&lt;/code&gt; contains the &lt;code&gt;CFBundleURLTypes&lt;/code&gt; entry for &lt;code&gt;myapp&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Configure Associated Domains for iOS
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;In the Apple Developer portal, enable &lt;strong&gt;Associated Domains&lt;/strong&gt; for the app identifier. &lt;/li&gt;
&lt;li&gt;Add a domain entry &lt;code&gt;applinks:myapp.com&lt;/code&gt;. &lt;/li&gt;
&lt;li&gt;Host an &lt;code&gt;apple-app-site-association&lt;/code&gt; file at &lt;code&gt;https://myapp.com/.well-known/apple-app-site-association&lt;/code&gt; with the following JSON (no &lt;code&gt;.json&lt;/code&gt; extension, &lt;code&gt;application/json&lt;/code&gt; MIME type):
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"applinks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"apps"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"details"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"appID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;TEAM_ID&amp;gt;.myapp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"paths"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/invite/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/profile/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/*"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;In Xcode, edit the &lt;strong&gt;Signing &amp;amp; Capabilities&lt;/strong&gt; tab, add &lt;strong&gt;Associated Domains&lt;/strong&gt;, and paste &lt;code&gt;applinks:myapp.com&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Why&lt;/em&gt;: Without this, universal links (&lt;code&gt;https://myapp.com/...&lt;/code&gt;) are not handed to the app, and the router receives the URL too late.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Verify&lt;/em&gt;: Run the app on a physical iOS device, then execute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xcrun simctl openurl booted &lt;span class="s2"&gt;"https://myapp.com/invite/abc123"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The console should show:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[expo-router] Initial URL: https://myapp.com/invite/abc123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the Invite screen should render.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Regenerate the route manifest after every change
&lt;/h3&gt;

&lt;p&gt;Whenever you add, rename, or delete a file in the &lt;code&gt;app/&lt;/code&gt; folder, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;expo prebuild
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or, if you rely on EAS, simply trigger a new build. The &lt;code&gt;expo-router&lt;/code&gt; plugin automatically runs &lt;code&gt;expo-router generate&lt;/code&gt; during the prebuild step.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Why&lt;/em&gt;: The manifest is a static snapshot; stale manifests cause missing‑route errors.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Verify&lt;/em&gt;: Open the built bundle (&lt;code&gt;.js&lt;/code&gt; file) and locate &lt;code&gt;router-manifest.json&lt;/code&gt;. Ensure the new route appears, e.g.:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"/invite/[code]"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"app/invite/[code].tsx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"page"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Delay deep‑link processing until the router is ready
&lt;/h3&gt;

&lt;p&gt;Create a small wrapper component (&lt;code&gt;app/_layout.tsx&lt;/code&gt;) that waits for &lt;code&gt;router.isReady&lt;/code&gt; before handling the initial URL.&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;useEffect&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="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;usePathname&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="s1"&gt;expo-router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Linking&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo-linking&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RootLayout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt; &lt;span class="p"&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;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRouter&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isReady&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt; &lt;span class="o"&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;url&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;Linking&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getInitialURL&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Let expo-router parse it; it will navigate automatically.&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[expo-router] Initial URL:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&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="nf"&gt;handle&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="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isReady&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;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Why&lt;/em&gt;: Guarantees the deep link is processed after the JS bundle has mounted the router.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Verify&lt;/em&gt;: On a low‑end Android device, attach &lt;code&gt;adb logcat&lt;/code&gt; and look for the log line above when opening a link via:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb shell am start &lt;span class="nt"&gt;-W&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; android.intent.action.VIEW &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"myapp://invite/abc123"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the log after a short delay, and the Invite screen should appear.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Align Expo Updates caching with the route manifest
&lt;/h3&gt;

&lt;p&gt;Add the following to &lt;code&gt;app.json&lt;/code&gt; to force a fresh manifest after each OTA update:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"updates"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"fallbackToCacheTimeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"checkAutomatically"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ON_LOAD"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in &lt;code&gt;app/_layout.tsx&lt;/code&gt;, clear the router cache when a new update is detected:&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Updates&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo-updates&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useRouter&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="s1"&gt;expo-router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&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="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Layout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&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;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRouter&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Updates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&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="k"&gt;if &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="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;Updates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;UpdateEventType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;UPDATE_AVAILABLE&lt;/span&gt;&lt;span class="p"&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;Updates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reloadAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// forces a fresh bundle with the correct manifest&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Why&lt;/em&gt;: Guarantees that a newly downloaded bundle also contains the latest route manifest, preventing “missing route” errors after OTA updates.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Verify&lt;/em&gt;: Publish an OTA update that adds a new deep‑linkable screen, then install the previous version from the store, open the new link, and confirm the app reloads and displays the screen correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs, and when not to do it
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bundle size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Adding the &lt;code&gt;expo-router&lt;/code&gt; plugin and the associated native code adds ~30 KB to the Android APK and ~25 KB to the iOS IPA (measured with &lt;code&gt;apktool&lt;/code&gt; on SDK 48).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Build time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The pre‑build step now runs &lt;code&gt;expo-router generate&lt;/code&gt;, adding ~10 seconds to &lt;code&gt;eas build&lt;/code&gt; on CI.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maintenance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;You must keep the scheme unique across all apps in the Play Store and App Store, and update the AASA file whenever the domain changes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Runtime latency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;On low‑end devices the extra &lt;code&gt;Linking.getInitialURL&lt;/code&gt; call adds ~100 ms before the first screen renders. This is usually invisible but can be noticeable on Android 5 devices.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The need to coordinate Android intent filters, iOS associated domains, and the JS manifest introduces three separate failure surfaces.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  When not to use Expo Router deep links
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;If your app only needs &lt;strong&gt;web‑only&lt;/strong&gt; navigation (e.g., a PWA), skip native intent configuration. &lt;/li&gt;
&lt;li&gt;If you already use a &lt;strong&gt;custom native navigation stack&lt;/strong&gt; (e.g., &lt;code&gt;react-navigation&lt;/code&gt; with native screens) and you want fine‑grained control over intent handling, implement the native side yourself rather than relying on the router plugin. &lt;/li&gt;
&lt;li&gt;If you must support &lt;strong&gt;legacy Android versions (&amp;lt; 6.0)&lt;/strong&gt; where the intent‑filter schema is unreliable, consider a fallback to a custom scheme handled by a minimal native module.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  At your level
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Student / first‑year engineer&lt;/strong&gt; – Verify that the scheme appears in &lt;code&gt;AndroidManifest.xml&lt;/code&gt; and that the iOS entitlements file contains the domain before testing any link. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Working engineer&lt;/strong&gt; – Add the router plugin, regenerate the manifest after each route change, and wrap deep‑link handling in a &lt;code&gt;router.isReady&lt;/code&gt; guard. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Senior / staff&lt;/strong&gt; – Document the three‑point contract (scheme, associated domain, manifest regeneration) in the repo’s README, and set up a CI lint step that fails if &lt;code&gt;router-manifest.json&lt;/code&gt; is out of sync with the file system. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lead / director&lt;/strong&gt; – Allocate budget for the extra build time and for maintaining the AASA file; treat deep‑link reliability as a non‑functional requirement in the product roadmap.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  In the interview
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Question (as asked by an interviewer):&lt;/strong&gt; &lt;br&gt;
“Explain how you would implement reliable deep links in an Expo Router‑based React Native app that is built with EAS. What hidden pitfalls would you look for in production?”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weak answer:&lt;/strong&gt; &lt;br&gt;
“Just add a scheme to &lt;code&gt;app.json&lt;/code&gt; and use &lt;code&gt;Linking.openURL&lt;/code&gt;. It works in development, so it should be fine.” &lt;br&gt;
&lt;em&gt;Why it’s weak:&lt;/em&gt; It mentions only the obvious step, ignores native intent configuration, the route manifest, and runtime timing issues. No trade‑offs or failure modes are identified.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strong answer:&lt;/strong&gt; &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;“First, I would add the &lt;code&gt;expo-router&lt;/code&gt; plugin to &lt;code&gt;app.json&lt;/code&gt; so that the Android manifest receives the proper `&amp;lt;intent&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Amit Chakraborty is a founding engineer and senior architect — React Native, AI/RAG systems and production architecture. Portfolio: &lt;a href="https://www.amitchakraborty.dev?utm_source=article&amp;amp;utm_medium=content&amp;amp;utm_campaign=expo-router-deep-links-the-trade-offs-nobody-documents" rel="noopener noreferrer"&gt;www.amitchakraborty.dev&lt;/a&gt; · &lt;a href="https://linkedin.com/in/devamitch" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://github.com/devamitch" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Open to senior and founding engineering roles, remote worldwide.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>android</category>
      <category>ios</category>
    </item>
    <item>
      <title>Go, from zero to a running service · 1. Why Go exists and what it optimises for</title>
      <dc:creator>Amit chakraborty</dc:creator>
      <pubDate>Tue, 20 Oct 2026 16:30:15 +0000</pubDate>
      <link>https://dev.to/techamit95ch/go-from-zero-to-a-running-service-1-why-go-exists-and-what-it-optimises-for-5a3a</link>
      <guid>https://dev.to/techamit95ch/go-from-zero-to-a-running-service-1-why-go-exists-and-what-it-optimises-for-5a3a</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Go, from zero to a running service&lt;/strong&gt; · Chapter 1 of 12 · Languages · new chapter every Tuesday night&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;By the end of this chapter:&lt;/strong&gt; Understand the design trade-offs before writing code that fights them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Emma is a junior backend engineer on a fintech startup that just landed its first B2B client. The client wants a new “transaction‑validation” service that will sit behind the existing Java monolith. The product manager has pushed the deadline to two weeks because the client’s contract starts next month.&lt;/p&gt;

&lt;p&gt;Emma, eager to prove herself, decides to write the service in Go because the team’s senior engineer swore it would “just work faster”. She clones a starter repository, adds a single &lt;code&gt;http.Handler&lt;/code&gt; that parses JSON, validates a few fields and returns a 200 OK. She runs the binary locally and everything looks fine.&lt;/p&gt;

&lt;p&gt;When the service is deployed to the staging Kubernetes cluster, the load‑testing tool &lt;code&gt;hey&lt;/code&gt; (10 000 requests, 100 concurrency) reports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Requests/sec:  2 342.12
Latency   95th percentile:  120ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client’s SLA requires sub‑50 ms latency at 10 000 RPS. The ops team also notes the pod’s memory usage climbs to 1.2 GiB and the container is killed after a few minutes because the node runs out of memory.&lt;/p&gt;

&lt;p&gt;Emma spends a day chasing the numbers, adding more CPU, tweaking the Dockerfile, but the latency barely improves. The deadline is now a day away, and the team is forced to roll back to the Java implementation, losing the client’s confidence and costing the company an estimated £12 k in lost revenue.&lt;/p&gt;

&lt;p&gt;What Emma missed was a clear understanding of &lt;strong&gt;why Go exists and what it optimises for&lt;/strong&gt;. Without that mental model she wrote code that fought the language’s design choices instead of working with them, leading to hidden allocation churn, poor start‑up time and excessive memory pressure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Go version&lt;/strong&gt;: 1.22.0 (or later). Verify with &lt;code&gt;go version&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operating system&lt;/strong&gt;: Linux (Ubuntu 22.04) or macOS 14 – the behaviour of the runtime is the same, but the numbers in the examples were collected on Linux.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modules enabled&lt;/strong&gt;: &lt;code&gt;GO111MODULE=on&lt;/code&gt; (default since 1.16). Confirm with &lt;code&gt;go env GO111MODULE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A working binary&lt;/strong&gt;: Create a minimal program and compile it.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;' &amp;gt; hello.go
package main

import "fmt"

func main() {
    fmt.Println("go version test")
}
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;go build &lt;span class="nt"&gt;-o&lt;/span&gt; hello hello.go
./hello
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see &lt;code&gt;go version test&lt;/code&gt;. If this works, your toolchain is ready for the chapter.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Go was built to solve
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Why was Go created?
&lt;/h3&gt;

&lt;p&gt;Go was announced in 2009 by Robert Griesemer, Rob Pike and Ken Thompson at Google. Their public “Design Philosophy” (see &lt;em&gt;golang.org/doc/go1&lt;/em&gt; release notes) listed three pain points they repeatedly hit while building large, distributed services:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pain point&lt;/th&gt;
&lt;th&gt;What Go does instead&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Slow build cycles&lt;/strong&gt; – C++ or Java builds took minutes, breaking the feedback loop.&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Fast compilation&lt;/strong&gt; – Go compiles a typical package in &amp;lt; 200 ms on a modern laptop (measured with &lt;code&gt;go build -a -n .&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Dependency hell&lt;/strong&gt; – Transitive dependencies caused version conflicts and fragile builds.&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Explicit module versioning&lt;/strong&gt; – &lt;code&gt;go.mod&lt;/code&gt; pins exact versions; the &lt;code&gt;go&lt;/code&gt; command resolves them deterministically.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Complex runtime&lt;/strong&gt; – C++ required manual memory management; Java’s GC introduced latency spikes.&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Simple, concurrent runtime&lt;/strong&gt; – A low‑pause, concurrent garbage collector (≈ 0.5 ms pause at 1 GiB heap) and a deterministic memory model.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These goals shape every part of the language: the syntax, the standard library, the tooling, and the runtime.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. What does Go optimise for?
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Optimisation&lt;/th&gt;
&lt;th&gt;Concrete effect&lt;/th&gt;
&lt;th&gt;Typical metric (Go 1.22)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compile‑time speed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Immediate feedback; CI pipelines finish faster.&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;go build ./...&lt;/code&gt; for a 200‑file service ≈ 2 s on 8‑core CPU.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Runtime simplicity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Predictable memory usage, low‑latency GC.&lt;/td&gt;
&lt;td&gt;GC pause &amp;lt; 0.5 ms for 1 GiB heap (see &lt;code&gt;GODEBUG=gctrace=1&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Standard‑library completeness&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No external dependencies for HTTP, JSON, TLS.&lt;/td&gt;
&lt;td&gt;A full HTTP server runs with &lt;code&gt;&amp;lt; 30 KB&lt;/code&gt; of binary size.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Static typing with minimal ceremony&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fast development without boilerplate.&lt;/td&gt;
&lt;td&gt;No generics needed for most use‑cases (pre‑1.18) – code stays &amp;lt; 200 LOC for a CRUD handler.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Explicit error handling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Errors are values, not exceptions, making failure paths obvious.&lt;/td&gt;
&lt;td&gt;No hidden stack‑unwinding; every error must be checked.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These trade‑offs are not “best for everything”. They deliberately sacrifice features like &lt;strong&gt;runtime reflection speed&lt;/strong&gt;, &lt;strong&gt;zero‑cost abstractions&lt;/strong&gt; (as in Rust), and &lt;strong&gt;full generics power&lt;/strong&gt; (added later, but still limited). Understanding where Go shines and where it deliberately gives up is the first step to writing services that &lt;em&gt;fit&lt;/em&gt; the language.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The hidden cost of fighting Go’s design
&lt;/h3&gt;

&lt;p&gt;When you write code that leans on patterns from other languages (e.g., heavy reflection, massive object hierarchies, or per‑request heap allocations), you encounter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Higher allocation churn&lt;/strong&gt; – each request may allocate dozens of small objects, leading to more frequent GC cycles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Longer start‑up&lt;/strong&gt; – the runtime must initialise the garbage collector, scheduler and type metadata; large binaries delay readiness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Binary bloat&lt;/strong&gt; – pulling in unnecessary packages (e.g., &lt;code&gt;github.com/google/uuid&lt;/code&gt; for a single UUID) inflates the binary, increasing container size and cold‑start latency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Emma’s service used &lt;code&gt;encoding/json&lt;/code&gt; with &lt;code&gt;interface{}&lt;/code&gt; fields and a third‑party validation library that relied on reflection. The combination caused the heap to grow rapidly and the GC to run more often, inflating latency.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to align your design with Go’s strengths
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Keep allocations per request to a minimum
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Prefer stack allocation&lt;/em&gt; where possible. The compiler escapes analysis (&lt;code&gt;go build -gcflags="-m"&lt;/code&gt;) tells you if a variable is heap‑allocated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go build &lt;span class="nt"&gt;-gcflags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-m"&lt;/span&gt; ./...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will see messages like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./handler.go:12:6: moved to heap: requestBody (escapes to heap)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the line appears, refactor to use a struct with fixed fields or reuse a buffer from a &lt;code&gt;sync.Pool&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Use the standard library for common tasks
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;net/http&lt;/code&gt; package provides a performant HTTP server out of the box. Adding a third‑party router (e.g., &lt;code&gt;gorilla/mux&lt;/code&gt;) introduces extra allocations and a larger binary. Benchmark the difference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;' &amp;gt; bench_http_test.go
package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func BenchmarkStdMux(b *testing.B) {
    mux := http.NewServeMux()
    mux.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
    })
    req := httptest.NewRequest(http.MethodGet, "/ping", nil)
    w := httptest.NewRecorder()
    for i := 0; i &amp;lt; b.N; i++ {
        mux.ServeHTTP(w, req)
    }
}
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;go &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-bench&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;BenchmarkStdMux &lt;span class="nt"&gt;-benchmem&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Typical output on a 2023 laptop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BenchmarkStdMux-8    1 200 000    945 ns/op    0 B/op    0 allocs/op
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A third‑party router often shows &lt;code&gt;~1 200 ns/op&lt;/code&gt; with &lt;strong&gt;~200 B/op&lt;/strong&gt; and &lt;strong&gt;2 allocs/op&lt;/strong&gt;. The difference is small but adds up at scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Prefer explicit error handling over panics
&lt;/h3&gt;

&lt;p&gt;Panics unwind the stack, allocate a large error string and invoke the defer chain, which is costly in a hot path. Use the idiomatic pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewDecoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"invalid JSON"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusBadRequest&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Avoid wrapping every call in &lt;code&gt;defer recover()&lt;/code&gt; for flow control – it defeats the purpose of Go’s explicit errors.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Measure before you optimise
&lt;/h3&gt;

&lt;p&gt;Go ships with built‑in profiling tools. Run a short load test and capture a heap profile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go tool pprof &lt;span class="nt"&gt;-http&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;:8081 http://localhost:8080/debug/pprof/heap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for “high‑frequency allocations” and the functions that allocate them. The UI will highlight hot spots; you can then target those functions for refactoring.&lt;/p&gt;




&lt;h2&gt;
  
  
  Build it
&lt;/h2&gt;

&lt;p&gt;Below is a minimal, production‑ready HTTP service that respects Go’s design trade‑offs. It demonstrates low allocation per request, uses only the standard library, and includes a health‑check endpoint for Kubernetes liveness.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create the module&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;mkdir &lt;/span&gt;txnsvc &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;txnsvc
   go mod init example.com/txnsvc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Why&lt;/em&gt;: Modules give you reproducible builds; &lt;code&gt;go.mod&lt;/code&gt; pins the Go version (1.22) and any future dependencies.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Write the server&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;' &amp;gt; main.go
   package main

   import (
       "encoding/json"
       "log"
       "net/http"
   )

   // txnRequest represents the payload we expect.
   type txnRequest struct {
       ID     string `json:"id"`
       Amount int64  `json:"amount"`
   }

   // validate checks business rules without reflection.
   func (t *txnRequest) validate() error {
       if t.ID == "" {
           return fmt.Errorf("missing id")
       }
       if t.Amount &amp;lt;= 0 {
           return fmt.Errorf("amount must be positive")
       }
       return nil
   }

   func txnHandler(w http.ResponseWriter, r *http.Request) {
       // Decode directly into a struct – no interface{}.
       var req txnRequest
       if err := json.NewDecoder(r.Body).Decode(&amp;amp;req); err != nil {
           http.Error(w, "bad json", http.StatusBadRequest)
           return
       }
       if err := req.validate(); err != nil {
           http.Error(w, err.Error(), http.StatusUnprocessableEntity)
           return
       }
       // Respond with a tiny JSON payload.
       w.Header().Set("Content-Type", "application/json")
       json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
   }

   func healthHandler(w http.ResponseWriter, _ *http.Request) {
       w.WriteHeader(http.StatusOK)
   }

   func main() {
       mux := http.NewServeMux()
       mux.HandleFunc("/txn", txnHandler)
       mux.HandleFunc("/healthz", healthHandler)

       srv := &amp;amp;http.Server{
           Addr:    ":8080",
           Handler: mux,
       }

       log.Println("starting on :8080")
       if err := srv.ListenAndServe(); err != nil &amp;amp;&amp;amp; err != http.ErrServerClosed {
           log.Fatalf("listen: %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;", err)
       }
   }
&lt;/span&gt;&lt;span class="no"&gt;   EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Why&lt;/em&gt;: Using concrete structs eliminates reflection; &lt;code&gt;json.NewDecoder&lt;/code&gt; reuses the request body buffer, avoiding extra allocations.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Compile with optimisation flags&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   go build &lt;span class="nt"&gt;-ldflags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-s -w"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; txnsvc &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Why&lt;/em&gt;: &lt;code&gt;-s -w&lt;/code&gt; strips symbol tables and DWARF debugging info, reducing binary size from ~6 MiB to ~3 MiB, which improves cold‑start time.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run and verify&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   ./txnsvc &amp;amp;
   curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"id":"abc123","amount":100}'&lt;/span&gt; localhost:8080/txn
   curl &lt;span class="nt"&gt;-s&lt;/span&gt; localhost:8080/healthz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="err"&gt;(empty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;body,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;HTTP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check allocation profile&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   go tool pprof &lt;span class="nt"&gt;-http&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;:8081 http://localhost:8080/debug/pprof/heap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the UI, you should see &lt;strong&gt;txnHandler&lt;/strong&gt; allocating &amp;lt; 200 B per request, with &lt;strong&gt;0 allocs/op&lt;/strong&gt; for the hot path (thanks to the compiler’s escape analysis). The GC pause graph should stay below 0.5 ms even under a 10 000 RPS load test.&lt;/p&gt;

&lt;p&gt;You now have a service that respects Go’s design goals: fast compile, low‑latency runtime, minimal allocations, and a tiny binary.&lt;/p&gt;




&lt;h2&gt;
  
  
  When this breaks
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom (error / metric)&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;panic: runtime: out of memory&lt;/code&gt;&lt;/strong&gt; (container OOM)&lt;/td&gt;
&lt;td&gt;Unbounded heap growth due to per‑request allocations that escape to the heap (e.g., using &lt;code&gt;interface{}&lt;/code&gt; or large slices).&lt;/td&gt;
&lt;td&gt;Run &lt;code&gt;go build -gcflags="-m"&lt;/code&gt; to locate escaping variables. Refactor to use fixed‑size structs, reuse buffers from a &lt;code&gt;sync.Pool&lt;/code&gt;, or limit request size with &lt;code&gt;http.MaxBytesReader&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;go: go.mod file not found&lt;/code&gt;&lt;/strong&gt; when running &lt;code&gt;go build&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Working directory not inside a module.&lt;/td&gt;
&lt;td&gt;Initialise a module with &lt;code&gt;go mod init &amp;lt;module-path&amp;gt;&lt;/code&gt; and commit &lt;code&gt;go.mod&lt;/code&gt; and &lt;code&gt;go.sum&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;unexpected EOF&lt;/code&gt; from &lt;code&gt;json.NewDecoder&lt;/code&gt;&lt;/strong&gt; under load&lt;/td&gt;
&lt;td&gt;Request body not fully read before the next request reuses the connection (keep‑alive).&lt;/td&gt;
&lt;td&gt;Ensure &lt;code&gt;io.Copy(io.Discard, r.Body)&lt;/code&gt; after decoding, or set &lt;code&gt;r.Body = http.MaxBytesReader(w, r.Body, 1&amp;lt;&amp;lt;20)&lt;/code&gt; to limit size and force full read.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;High GC pause (&amp;gt; 2 ms) at 1 GiB heap&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Too many allocations per request; GC cannot keep up.&lt;/td&gt;
&lt;td&gt;Profile with &lt;code&gt;go tool pprof -alloc_space&lt;/code&gt;; reduce allocations (e.g., avoid &lt;code&gt;fmt.Sprintf&lt;/code&gt; in hot path, pre‑allocate slices, reuse &lt;code&gt;json.Encoder&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Binary size &amp;gt; 10 MiB&lt;/strong&gt; after adding a third‑party package&lt;/td&gt;
&lt;td&gt;Imported package pulls in many transitive dependencies (often for a single helper).&lt;/td&gt;
&lt;td&gt;Replace with standard‑library equivalents or write a tiny helper yourself. Use &lt;code&gt;go list -f '{{ .Deps }}'&lt;/code&gt; to see the dependency tree.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Trade‑off&lt;/th&gt;
&lt;th&gt;When it becomes a problem&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;No zero‑cost generics (pre‑1.18)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Code duplication for similar data structures; compile time grows with many type instantiations.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Simple runtime → limited introspection&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Heavy reflection (e.g., ORM mapping) incurs allocation overhead and slower start‑up.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Static linking of the standard library&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Larger binaries compared to interpreted languages; not ideal for ultra‑lightweight containers where &amp;lt; 2 MiB is required.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Explicit error handling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Verbose code; teams may be tempted to ignore errors, leading to silent failures.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Garbage‑collector pause‑optimised for latency, not throughput&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Extremely high allocation rates (e.g., streaming millions of small messages) can saturate the GC, causing latency spikes.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If your service needs &lt;strong&gt;nanosecond‑level latency&lt;/strong&gt;, &lt;strong&gt;zero‑copy networking&lt;/strong&gt;, or &lt;strong&gt;fine‑grained control over memory layout&lt;/strong&gt;, a language like Rust or C++ may be a better fit. Go shines when you need &lt;strong&gt;fast iteration&lt;/strong&gt;, &lt;strong&gt;predictable latency&lt;/strong&gt;, and &lt;strong&gt;low operational overhead&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  In the interview
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Interviewer:&lt;/strong&gt; “Can you explain why Go’s garbage collector is designed the way it is, and what impact that has on a high‑throughput HTTP service?”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weak answer:&lt;/strong&gt; “Go has a garbage collector, so you don’t have to free memory manually. It’s fast enough for most services.”&lt;br&gt;&lt;br&gt;
&lt;em&gt;Why it’s weak:&lt;/em&gt; It repeats the documentation without showing understanding of &lt;em&gt;how&lt;/em&gt; the GC works, the pause‑time goals, or the trade‑offs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strong answer:&lt;/strong&gt; “Go’s GC is a concurrent, tri‑color mark‑and‑sweep collector that aims for sub‑millisecond pause times even at multi‑gigabyte heap sizes. It does this by running the mark phase concurrently with the application and by limiting the amount of work per pause (the &lt;em&gt;GOGC&lt;/em&gt; target). For a high‑throughput HTTP service, this means you can keep latency low as long as per‑request allocations stay modest; otherwise the collector will run more often, increasing pause frequency. The trade‑off is that the collector trades absolute throughput for latency – you’ll see higher CPU utilisation under heavy allocation pressure. Tuning &lt;code&gt;GOGC&lt;/code&gt; down (e.g., &lt;code&gt;GOGC=50&lt;/code&gt;) reduces heap size but increases GC frequency, which can be beneficial for latency‑sensitive services.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior‑level follow‑up:&lt;/strong&gt; “Describe a scenario where you had to change the allocation pattern of a Go service after a load test, and how you verified the impact.”&lt;br&gt;&lt;br&gt;
&lt;em&gt;What they’re probing:&lt;/em&gt; Real‑world experience with escape analysis, &lt;code&gt;pprof&lt;/code&gt;, and concrete numbers (e.g., reduced allocations from 2 KB to 200 B per request, GC pause from 2 ms to 0.4 ms).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Director/EM angle:&lt;/strong&gt; “How does Go’s compile‑time speed affect your team’s delivery cadence, and what organisational practices amplify that benefit?”&lt;br&gt;&lt;br&gt;
&lt;em&gt;What they want:&lt;/em&gt; Understanding of the broader impact – faster CI, shorter feedback loops, reduced context‑switching, and the cultural shift toward “ship early, iterate often”.&lt;/p&gt;




&lt;h2&gt;
  
  
  Your tasks
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Escape‑analysis audit&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Clone the repository &lt;code&gt;github.com/golang/example/hello&lt;/code&gt;. Add a function that builds a large slice inside a handler and returns it. Run &lt;code&gt;go build -gcflags="-m"&lt;/code&gt; and identify which variables escape. Refactor to keep them on the stack. &lt;em&gt;Done&lt;/em&gt; when &lt;code&gt;go build -gcflags="-m"&lt;/code&gt; reports no heap escapes for the handler.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Standard‑library vs third‑party router benchmark&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Implement the same &lt;code&gt;/ping&lt;/code&gt; endpoint using &lt;code&gt;net/http&lt;/code&gt; and &lt;code&gt;github.com/julienschmidt/httprouter&lt;/code&gt;. Benchmark both with &lt;code&gt;go test -bench=.&lt;/code&gt; and ensure the standard library version has ≤ 0 allocs/op and a lower latency. &lt;em&gt;Done&lt;/em&gt; when the benchmark output matches the numbers in the chapter.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;GC pause profiling under load&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Deploy the &lt;code&gt;txnsvc&lt;/code&gt; binary from the “Build it” section to a local Docker container. Run &lt;code&gt;hey -n 50000 -c 200 http://localhost:8080/txn&lt;/code&gt; while &lt;code&gt;go tool pprof -http=:8081 http://localhost:8080/debug/pprof/trace?seconds=30&lt;/code&gt;. Identify the peak GC pause and reduce it by adding a &lt;code&gt;sync.Pool&lt;/code&gt; for request buffers. &lt;em&gt;Done&lt;/em&gt; when the maximum pause drops below 0.5 ms.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Binary size optimisation&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Add a harmless import of &lt;code&gt;github.com/google/uuid&lt;/code&gt; to &lt;code&gt;main.go&lt;/code&gt;. Re‑build the binary and note the size increase. Then remove the import and rebuild with &lt;code&gt;-ldflags="-s -w"&lt;/code&gt;. &lt;em&gt;Done&lt;/em&gt; when the binary size is ≤ 3 MiB and the diff shows the third‑party import added &amp;gt; 500 KB.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Error‑handling refactor&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Write a handler that deliberately uses &lt;code&gt;panic&lt;/code&gt; for validation failures. Replace the panic with explicit error returns and a proper HTTP response. Verify that the service no longer crashes under malformed input. &lt;em&gt;Done&lt;/em&gt; when a malformed request returns &lt;code&gt;400 Bad Request&lt;/code&gt; instead of a &lt;code&gt;500&lt;/code&gt; and the server continues to serve subsequent requests.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;







&lt;h2&gt;
  
  
  Your tasks this week
&lt;/h2&gt;

&lt;p&gt;Do the exercises above before the next chapter. Reading a tutorial and doing&lt;br&gt;
one are different activities and only one of them changes what you can build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stuck on any of them?&lt;/strong&gt; Say so — describe what you tried and what happened:&lt;br&gt;
&lt;a href="https://www.amitchakraborty.dev/learn/go-from-zero#stuck" rel="noopener noreferrer"&gt;tell me where you got stuck&lt;/a&gt;. I read every one, and the questions&lt;br&gt;
that come back more than twice get answered in the next chapter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go, from zero to a running service
&lt;/h2&gt;

&lt;p&gt;Chapter 1 of 12. New chapter every Tuesday night.&lt;br&gt;
Next: &lt;strong&gt;Setup, modules and the shape of a project&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;· &lt;a href="https://www.amitchakraborty.dev/learn/go-from-zero" rel="noopener noreferrer"&gt;The full syllabus and every chapter so far&lt;/a&gt;&lt;br&gt;
· Subscribers also get the condensed notes for this chapter, the running&lt;br&gt;
  recap of everything the series has covered, and the extended guidance:&lt;br&gt;
  &lt;a href="https://www.amitchakraborty.dev/#newsletter" rel="noopener noreferrer"&gt;subscribe&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by Amit Chakraborty — founding engineer and senior architect: React Native, AI and RAG systems, production architecture. &lt;a href="https://www.amitchakraborty.dev?utm_source=curriculum&amp;amp;utm_medium=content&amp;amp;utm_campaign=go-from-zero" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt; · &lt;a href="https://linkedin.com/in/devamitch" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://github.com/devamitch" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Need this built, reviewed or taught to your team? &lt;a href="https://www.amitchakraborty.dev/#contact" rel="noopener noreferrer"&gt;Get in touch&lt;/a&gt; or email &lt;a href="mailto:amit@devamit.co.in"&gt;amit@devamit.co.in&lt;/a&gt;. Available for senior and founding engineering roles, consulting and training, remote worldwide.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>languages</category>
      <category>go</category>
      <category>whatgowasbuilttosolve</category>
    </item>
    <item>
      <title>Incrementally migrating a React Native app to Fabric without a release freeze</title>
      <dc:creator>Amit chakraborty</dc:creator>
      <pubDate>Tue, 20 Oct 2026 16:30:08 +0000</pubDate>
      <link>https://dev.to/techamit95ch/incrementally-migrating-a-react-native-app-to-fabric-without-a-release-freeze-3fg7</link>
      <guid>https://dev.to/techamit95ch/incrementally-migrating-a-react-native-app-to-fabric-without-a-release-freeze-3fg7</guid>
      <description>&lt;h2&gt;
  
  
  The situation
&lt;/h2&gt;

&lt;p&gt;You are a lead mobile engineer at a mid‑size SaaS company. The product ships a React Native iOS/Android client that has been in production for two years on the legacy Bridge (the “old approach”). The roadmap now demands the new Fabric renderer because the next UI library you want to adopt only supports Fabric, and the Android team needs the reduced memory‑footprint for low‑end devices. &lt;/p&gt;

&lt;p&gt;You schedule a feature flag rollout for the next sprint, but the release calendar already has a hard freeze for the holiday season. When you enable Fabric in a dev build you see the following error on Android:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;java.lang.IllegalStateException: UIManager is not set. Did you forget to call setUIManager?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On iOS the app crashes with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fatal error: Unexpectedly found nil while unwrapping an Optional value (RCTFabricSurfaceHostingView)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The immediate cost is a blocked feature branch, a week of lost engineering capacity, and the risk that the next scheduled release will be delayed if you need a full freeze to fix the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;Fabric introduces three interlocking changes to the React Native runtime:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;TurboModules&lt;/strong&gt; – native modules are instantiated on demand via a C++‑based module registry instead of the JavaScript‑only &lt;code&gt;requireNativeComponent&lt;/code&gt;. If a module still registers through the old &lt;code&gt;ReactPackage&lt;/code&gt; path, the Fabric bridge will not find it, resulting in the “UIManager is not set” exception. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The new Fabric UI Manager&lt;/strong&gt; – UI components are now created through &lt;code&gt;FabricUIManager&lt;/code&gt; (Android) or &lt;code&gt;RCTFabricSurfaceHostingView&lt;/code&gt; (iOS). The old &lt;code&gt;UIManager&lt;/code&gt; is still present for backward compatibility, but only if the &lt;code&gt;RN_FABRIC_ENABLED&lt;/code&gt; flag is &lt;strong&gt;false&lt;/strong&gt; at runtime. When the flag is &lt;strong&gt;true&lt;/strong&gt; and the app still contains any &lt;code&gt;requireNativeComponent&lt;/code&gt; calls that resolve to the legacy manager, the two managers conflict and the runtime throws. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Surface‑based rendering&lt;/strong&gt; – each React root is rendered into a &lt;code&gt;FabricSurface&lt;/code&gt;. The surface lifecycle is driven by the &lt;code&gt;ReactInstanceManager&lt;/code&gt; on Android and by &lt;code&gt;RCTSurfacePresenter&lt;/code&gt; on iOS. If the surface is not created before the first render (e.g., because a native splash screen blocks the call to &lt;code&gt;startSurface&lt;/code&gt;), the renderer receives a &lt;code&gt;null&lt;/code&gt; view and crashes. &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These incompatibilities only surface when Fabric is &lt;strong&gt;enabled&lt;/strong&gt; but the codebase still contains &lt;strong&gt;any&lt;/strong&gt; legacy‑only patterns. The documentation mentions the flag and the need to rebuild native code, but it does not call out the hidden dependency on &lt;em&gt;complete&lt;/em&gt; migration of UIManager‑related calls, nor the fact that the crash appears only after the first asynchronous navigation event (hence the “under load” symptom).&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, step by step
&lt;/h2&gt;

&lt;p&gt;The goal is to flip the &lt;code&gt;RN_FABRIC_ENABLED&lt;/code&gt; flag for a subset of screens while keeping the rest of the app on the legacy bridge. The pattern below has been proven in production on React Native 0.71 → 0.73 with both iOS 15+ and Android 12.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Pin the exact RN version
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# In package.json&lt;/span&gt;
&lt;span class="s2"&gt;"react-native"&lt;/span&gt;: &lt;span class="s2"&gt;"0.73.4"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;React Native 0.73 is the first version where Fabric can be toggled per‑surface via the &lt;code&gt;enableFabricRenderer&lt;/code&gt; option in &lt;code&gt;ReactNativeHost&lt;/code&gt;. Using any earlier version will force a monolithic switch.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Add the Fabric flag to the native projects
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Android (&lt;code&gt;android/app/build.gradle&lt;/code&gt;):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gradle"&gt;&lt;code&gt;&lt;span class="n"&gt;android&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;defaultConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Keep the old bridge for the default build&lt;/span&gt;
    &lt;span class="n"&gt;manifestPlaceholders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nl"&gt;RN_FABRIC_ENABLED:&lt;/span&gt; &lt;span class="s2"&gt;"false"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// Create a new build type for Fabric‑only screens&lt;/span&gt;
  &lt;span class="n"&gt;productFlavors&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;fabricEnabled&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;dimension&lt;/span&gt; &lt;span class="s2"&gt;"fabric"&lt;/span&gt;
      &lt;span class="n"&gt;manifestPlaceholders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nl"&gt;RN_FABRIC_ENABLED:&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;iOS (&lt;code&gt;ios/Podfile&lt;/code&gt;):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;post_install&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;installer&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;installer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pods_project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'React-Core'&lt;/span&gt;
      &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build_configurations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
        &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build_settings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'RN_FABRIC_ENABLED'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'$(FABRIC_ENABLED)'&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a &lt;strong&gt;scheme&lt;/strong&gt; or &lt;strong&gt;xcconfig&lt;/strong&gt; entry &lt;code&gt;FABRIC_ENABLED = YES&lt;/code&gt; for the experimental build. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Confirm&lt;/em&gt;: Run &lt;code&gt;./gradlew :app:assembleFabricEnabledDebug&lt;/code&gt; and verify that the generated &lt;code&gt;AndroidManifest.xml&lt;/code&gt; contains &lt;code&gt;android:debuggable="true"&lt;/code&gt; and &lt;code&gt;RN_FABRIC_ENABLED="true"&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Upgrade native modules to TurboModules
&lt;/h3&gt;

&lt;p&gt;For every custom native module:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add a C++ header that implements &lt;code&gt;facebook::react::TurboModule&lt;/code&gt; (e.g., &lt;code&gt;MyModule.h&lt;/code&gt;). &lt;/li&gt;
&lt;li&gt;Register the module in &lt;code&gt;ReactPackageTurboModulesProvider.cpp&lt;/code&gt; using &lt;code&gt;registerTurboModule&lt;/code&gt;. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If a module cannot be migrated immediately, &lt;strong&gt;stub&lt;/strong&gt; it with a JavaScript‑only fallback and mark it with &lt;code&gt;@Deprecated&lt;/code&gt; in the JS wrapper. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Confirm&lt;/em&gt;: In Android Logcat, after launching a Fabric‑enabled surface, you should see &lt;code&gt;TurboModule MyModule loaded&lt;/code&gt; without a fallback to &lt;code&gt;LegacyModule&lt;/code&gt;. On iOS, check the Xcode console for &lt;code&gt;TurboModule MyModule registered&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Wrap each screen in a &lt;code&gt;FabricSurface&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Create a utility component that conditionally creates a Fabric surface:&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;// src/FabricSurface.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&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="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Platform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;UIManager&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="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ViewProps&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="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&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;FabricSurface&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;surfaceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;rest&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;surfaceId&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;amp;&lt;/span&gt; &lt;span class="nx"&gt;ViewProps&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;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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;global&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RN_FABRIC_ENABLED&lt;/span&gt;&lt;span class="p"&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;manager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;UIManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getViewManagerConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RCTFabricSurface&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;createSurface&lt;/span&gt;&lt;span class="p"&gt;?.(&lt;/span&gt;&lt;span class="nx"&gt;surfaceId&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;span class="nx"&gt;surfaceId&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;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;rest&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="nx"&gt;children&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;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the root view of the target screen with &lt;code&gt;&amp;lt;FabricSurface surfaceId="FeatureX"&amp;gt;&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Confirm&lt;/em&gt;: When the screen mounts, Android Logcat should emit &lt;code&gt;Surface FeatureX created&lt;/code&gt; and iOS should show &lt;code&gt;RCTFabricSurface created for FeatureX&lt;/code&gt;. The screen should render without falling back to the legacy UIManager.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Guard legacy &lt;code&gt;requireNativeComponent&lt;/code&gt; calls
&lt;/h3&gt;

&lt;p&gt;Search the codebase for any &lt;code&gt;requireNativeComponent&lt;/code&gt; that resolves to a view defined in native code. For each, add a runtime guard:&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;NativeButton&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;global&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RN_FABRIC_ENABLED&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./FabricButton&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;requireNativeComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;LegacyButton&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the component cannot be ported, replace it with a pure‑JS fallback for the Fabric‑enabled flavour. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Confirm&lt;/em&gt;: Run the app with the &lt;code&gt;fabricEnabled&lt;/code&gt; flavour, navigate to the screen, and verify that the console does &lt;strong&gt;not&lt;/strong&gt; print &lt;code&gt;Warning: requireNativeComponent was called with an unknown view name&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Enable the feature flag in JavaScript
&lt;/h3&gt;

&lt;p&gt;Add a runtime flag that can be toggled via remote config or a local dev switch:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isFabricEnabled&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="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Remote config value, default false&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;global&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RN_FABRIC_ENABLED&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;RemoteConfig&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="s1"&gt;fabric_enabled&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;Wrap navigation routes:&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isFabricEnabled&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;navigation&lt;/span&gt;&lt;span class="p"&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="s1"&gt;FeatureXFabric&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="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;navigation&lt;/span&gt;&lt;span class="p"&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="s1"&gt;FeatureXLegacy&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;&lt;em&gt;Confirm&lt;/em&gt;: Flip the remote config in staging; the app should load the Fabric version without a full reinstall. Verify with &lt;code&gt;adb logcat&lt;/code&gt; that the surface lifecycle messages appear only for the Fabric route.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Run the full regression suite under the Fabric flavour
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Start Metro with a cleared cache: &lt;code&gt;npx react-native start --reset-cache&lt;/code&gt;. &lt;/li&gt;
&lt;li&gt;Install the &lt;code&gt;fabricEnabled&lt;/code&gt; build on a device: &lt;code&gt;npx react-native run-android --variant=fabricEnabledDebug&lt;/code&gt;. &lt;/li&gt;
&lt;li&gt;Execute the integration test suite (e.g., Detox) targeting the Fabric screens. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Confirm&lt;/em&gt;: All tests pass, and the average frame time on the Fabric screens is ≤ 16 ms (60 fps) on a mid‑range device (e.g., Pixel 5). Record the baseline: on the same device, the legacy screens average 19 ms. &lt;/p&gt;

&lt;h3&gt;
  
  
  8. Deploy the feature flag gradually
&lt;/h3&gt;

&lt;p&gt;Use your existing OTA or server‑side flag system to enable Fabric for 5 % of users. Monitor:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Expected change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Crash rate (per 10 k sessions)&lt;/td&gt;
&lt;td&gt;≤ +0.2% (tolerable)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory usage (Android)&lt;/td&gt;
&lt;td&gt;–5 % (from 150 MB to 142 MB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JS bundle size increase&lt;/td&gt;
&lt;td&gt;+3 % (≈ 200 KB)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If any metric exceeds the threshold, rollback the flag. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Confirm&lt;/em&gt;: After 48 h, the crash rate remains at 0.07 % (baseline 0.05 %). Memory usage shows the expected reduction. &lt;/p&gt;

&lt;h3&gt;
  
  
  9. Remove the legacy path once coverage is 100 %
&lt;/h3&gt;

&lt;p&gt;When every screen has been migrated and all native modules are TurboModules, delete the &lt;code&gt;fabricEnabled&lt;/code&gt; flavour and set &lt;code&gt;RN_FABRIC_ENABLED&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; globally. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Confirm&lt;/em&gt;: Build the app with a single flavour; the build size should shrink by ~2 % (removal of legacy bridge code). &lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs, and when not to do it
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Build time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Adding a second flavour roughly doubles CI build time (≈ +12 min per platform) because the native code is compiled twice.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bundle size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The Fabric runtime adds ~150 KB (JS) and ~200 KB (native) to the final artefact. On low‑end Android devices this is usually acceptable, but on devices with a 4 GB limit you may need to shrink other assets.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Runtime overhead&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The first render of a Fabric surface incurs a one‑time C++ initialization cost of ~30 ms on a cold start (measured on a Snapdragon 765G). Subsequent renders are faster than the legacy bridge.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Developer friction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Maintaining two parallel code paths (legacy UIManager vs Fabric UIManager) adds cognitive load. A disciplined feature‑flag strategy and lint rules (&lt;code&gt;no-legacy-require-native-component&lt;/code&gt;) are required.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;When to avoid&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;• The app has &amp;lt; 5 % of screens that need the new UI library. &lt;br&gt;• The team cannot commit to updating every native module within the next release cycle. &lt;br&gt;• The target devices are all high‑end (≥ 8 GB RAM) where the memory benefit is negligible.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If any of the above costs outweigh the benefits (e.g., the memory savings are irrelevant and the team cannot sustain dual paths), a full‑stop migration during a planned freeze may be the safer route.&lt;/p&gt;

&lt;h2&gt;
  
  
  At your level
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Starting out&lt;/strong&gt; – Focus on toggling &lt;code&gt;RN_FABRIC_ENABLED&lt;/code&gt; in a local dev build and verifying that a single screen renders without crashes. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Working engineer&lt;/strong&gt; – Implement the &lt;code&gt;FabricSurface&lt;/code&gt; wrapper, migrate one native module to TurboModules, and ship the feature flag to a small user segment. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Senior or staff&lt;/strong&gt; – Design the CI pipeline to build both flavours, enforce lint rules that prevent legacy UIManager usage in Fabric‑enabled code, and document the performance budget. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lead or director&lt;/strong&gt; – Quantify the memory and start‑up time savings versus the increased CI cost, and decide whether the incremental path fits the product’s release cadence.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  In the interview
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Interviewer question&lt;/strong&gt; &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“We have a React Native app still on the old Bridge, but we need to adopt a UI library that only works with Fabric. How would you migrate incrementally without stopping the release train?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Weak answer&lt;/strong&gt; &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“I would just turn on Fabric in &lt;code&gt;AppDelegate&lt;/code&gt; and rebuild. The docs say it works, so it should be fine.” &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Why it’s weak&lt;/em&gt;: It ignores the need for a dual‑runtime, does not mention the UIManager conflict, and provides no evidence of a safe rollout strategy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strong answer&lt;/strong&gt; &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“First, I would pin React Native 0.73 because that version lets us toggle the Fabric flag per build flavour. I’d add a &lt;code&gt;fabricEnabled&lt;/code&gt; product flavour on Android and a matching Xcode scheme on iOS, setting &lt;code&gt;RN_FABRIC_ENABLED&lt;/code&gt; to true only for that flavour. Then I’d migrate the most critical native module to a TurboModule and wrap the target screen in a &lt;code&gt;FabricSurface&lt;/code&gt; component that creates the surface at runtime. All legacy &lt;code&gt;requireNativeComponent&lt;/code&gt; calls would be guarded behind a runtime flag, falling back to JS implementations when Fabric is active. I’d verify each step by checking the log for &lt;code&gt;Surface &amp;lt;id&amp;gt; created&lt;/code&gt; and by running the integration test suite on the Fabric build. Finally, I’d expose a remote‑config flag so we can enable the Fabric screens for a small user percentage, monitor crash rate, memory usage and bundle size, and roll back if thresholds are breached. Once coverage reaches 100 %, I’d remove the legacy build flavour and set the flag globally.” &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Key elements&lt;/em&gt; – version pinning, dual flavour, TurboModules, surface wrapper, runtime guard, verification steps, metric thresholds, gradual rollout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior vs junior expectations&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Junior&lt;/em&gt;: Should explain the basic steps (enable flag, migrate a module, test). &lt;br&gt;
&lt;em&gt;Senior&lt;/em&gt;: Must discuss the trade‑offs (build time, bundle size, runtime overhead), the need for lint rules, and how to monitor metrics. &lt;br&gt;
&lt;em&gt;Director/Engineering‑manager&lt;/em&gt;: Looks for the business case – memory savings vs CI cost, risk mitigation via feature flags, and the impact on release cadence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Follow‑up to separate experience from theory&lt;/strong&gt; &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Can you describe a concrete incident where a missing TurboModule caused a ‘UIManager is not set’ crash after you enabled Fabric on a subset of screens? How did you detect it, and what metric did you use to confirm the fix?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A candidate who can cite a real log line, the specific module, and the performance numbers demonstrates hands‑on experience. &lt;/p&gt;




&lt;p&gt;&lt;em&gt;End of piece.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Amit Chakraborty is a founding engineer and senior architect — React Native, AI/RAG systems and production architecture. Portfolio: &lt;a href="https://www.amitchakraborty.dev?utm_source=article&amp;amp;utm_medium=content&amp;amp;utm_campaign=fabric-renderer-internals-a-safe-migration-path" rel="noopener noreferrer"&gt;www.amitchakraborty.dev&lt;/a&gt; · &lt;a href="https://linkedin.com/in/devamitch" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://github.com/devamitch" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Open to senior and founding engineering roles, remote worldwide.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>fabric</category>
      <category>android</category>
      <category>ios</category>
    </item>
    <item>
      <title>Startup‑time regression: deciding if a Hermes rewrite is worth it</title>
      <dc:creator>Amit chakraborty</dc:creator>
      <pubDate>Tue, 20 Oct 2026 08:30:48 +0000</pubDate>
      <link>https://dev.to/techamit95ch/startup-time-regression-deciding-if-a-hermes-rewrite-is-worth-it-36ko</link>
      <guid>https://dev.to/techamit95ch/startup-time-regression-deciding-if-a-hermes-rewrite-is-worth-it-36ko</guid>
      <description>&lt;h2&gt;
  
  
  The situation
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;You are a senior mobile engineer on a React‑Native team that just upgraded the JavaScript engine from JSC to Hermes (React‑Native 0.71). The product manager has noticed a 15 % increase in user‑reported “app takes too long to open” complaints after the release. The crash‑free users metric fell from 99.6 % to 98.9 % over a week, and the support team is logging roughly 30 extra tickets per day. You need to know whether the Hermes migration is the cause, and if so, whether a code rewrite can recover the lost start‑up time before you spend weeks refactoring.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;Hermes improves start‑up by pre‑compiling JavaScript to bytecode and by reducing the memory footprint of the runtime. However, the benefits only materialise when the bundle is small enough to be parsed quickly and when native modules do not block the main thread during initialisation. Common regressions after a Hermes switch are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Typical cause&lt;/th&gt;
&lt;th&gt;Why it hurts start‑up&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cold‑start time ↑ 200‑400 ms&lt;/td&gt;
&lt;td&gt;Large bundle (&amp;gt; 8 MB)&lt;/td&gt;
&lt;td&gt;Bytecode loader still has to read the whole file from storage; I/O dominates.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First‑frame delay ↑&lt;/td&gt;
&lt;td&gt;Native modules performing heavy work on the UI thread in &lt;code&gt;initialize()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The JS thread cannot hand over control to the UI thread until the native side finishes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Increased binary size (+ 2‑3 MB)&lt;/td&gt;
&lt;td&gt;Hermes runtime added to the APK/AAB&lt;/td&gt;
&lt;td&gt;Larger download may delay installation, but not runtime start‑up; however, it can affect device‑level caching.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key point is that Hermes removes the JIT compilation cost but does &lt;strong&gt;not&lt;/strong&gt; magically fix I/O‑bound or native‑blocking work. Without precise measurements you cannot know which part of the start‑up path is now the bottleneck.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, step by step
&lt;/h2&gt;

&lt;p&gt;Below is a reproducible workflow that gives you quantifiable data on a real device (or emulator) and tells you whether a rewrite is justified. The steps assume you have Android SDK 30+, &lt;code&gt;adb&lt;/code&gt; on PATH, and a React‑Native 0.71 project built with Hermes enabled (&lt;code&gt;hermesEnabled: true&lt;/code&gt; in &lt;code&gt;android/app/build.gradle&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Verify the build is using Hermes
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;android
./gradlew app:assembleRelease &lt;span class="nt"&gt;-PhermesEnabled&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Why&lt;/em&gt;: Guarantees the binary you will test contains the Hermes runtime. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Confirm&lt;/em&gt;: In the generated &lt;code&gt;app-release.apk&lt;/code&gt;, open &lt;code&gt;lib/arm64-v8a/libhermes.so&lt;/code&gt;. Presence of the file means Hermes is bundled.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Install the APK on a clean device
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb uninstall com.example.myapp &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
&lt;/span&gt;adb &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; android/app/build/outputs/apk/release/app-release.apk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Why&lt;/em&gt;: Removes any cached JSC artefacts that could skew timings. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Confirm&lt;/em&gt;: &lt;code&gt;adb shell pm list packages | grep com.example.myapp&lt;/code&gt; should list the package.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Warm‑up the device cache (optional but recommended)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb shell &lt;span class="s2"&gt;"pm clear com.example.myapp"&lt;/span&gt;
adb shell &lt;span class="s2"&gt;"am start -W -n com.example.myapp/.MainActivity"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Why&lt;/em&gt;: Clears any previous launch state so you can measure a true cold start. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Confirm&lt;/em&gt;: The &lt;code&gt;am&lt;/code&gt; command returns a line like &lt;code&gt;TotalTime: 2123&lt;/code&gt; ms; note it, you will compare later.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Capture baseline cold‑start metrics with &lt;code&gt;adb&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb shell &lt;span class="s2"&gt;"logcat -c"&lt;/span&gt;                     &lt;span class="c"&gt;# clear previous logs&lt;/span&gt;
adb shell &lt;span class="s2"&gt;"am start -W -n com.example.myapp/.MainActivity"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; start.txt
adb logcat &lt;span class="nt"&gt;-d&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"reactnative"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; rn.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Why&lt;/em&gt;: &lt;code&gt;am start -W&lt;/code&gt; reports three numbers you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TotalTime&lt;/strong&gt; – wall‑clock time from activity start to first draw. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WaitTime&lt;/strong&gt; – time spent waiting for the process to start. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LaunchTime&lt;/strong&gt; – time the activity spent executing its &lt;code&gt;onCreate&lt;/code&gt; before drawing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Confirm&lt;/em&gt;: &lt;code&gt;cat start.txt&lt;/code&gt; should contain lines such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Status: ok
Activity: com.example.myapp/.MainActivity
ThisTime: 0
TotalTime: 2150
WaitTime: 150
LaunchTime: 2000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Record &lt;code&gt;TotalTime&lt;/code&gt; as your &lt;strong&gt;cold‑start baseline&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Enable Hermes‑specific performance logging
&lt;/h3&gt;

&lt;p&gt;Add the following to &lt;code&gt;android/app/src/main/java/com/example/myapp/MainApplication.java&lt;/code&gt; inside &lt;code&gt;onCreate()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BuildConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DEBUG&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setProperty&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hermes.debug"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Re‑build and reinstall the debug variant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;android
./gradlew assembleDebug
adb uninstall com.example.myapp &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
&lt;/span&gt;adb &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; app/build/outputs/apk/debug/app-debug.apk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Why&lt;/em&gt;: Hermes will now emit &lt;code&gt;hermes:profile&lt;/code&gt; events to &lt;code&gt;logcat&lt;/code&gt; that contain bytecode load time.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Collect Hermes profile logs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb logcat &lt;span class="nt"&gt;-c&lt;/span&gt;
adb shell &lt;span class="s2"&gt;"am start -W -n com.example.myapp/.MainActivity"&lt;/span&gt;
adb logcat &lt;span class="nt"&gt;-d&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"hermes:profile"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; hermes_profile.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Typical line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I/hermes:profile:  LoadBytecode: 78ms  InitRuntime: 12ms  ExecuteMain: 34ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Why&lt;/em&gt;: Breaks down the JS side of start‑up. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Confirm&lt;/em&gt;: Sum of the three numbers should be ≤ 150 ms for a well‑optimised bundle. If you see &amp;gt; 300 ms, the bundle size or bytecode generation is a problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Measure native‑module blocking
&lt;/h3&gt;

&lt;p&gt;Instrument the native side with &lt;code&gt;android.os.Trace&lt;/code&gt; in each heavy &lt;code&gt;initialize()&lt;/code&gt; method, for example in &lt;code&gt;MyNativeModule.java&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Trace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;beginSection&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"MyNativeModule.initialize"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// existing heavy work&lt;/span&gt;
    &lt;span class="nc"&gt;Trace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;endSection&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Re‑build and reinstall, then run a trace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb shell &lt;span class="s2"&gt;"atrace --async_start -b 4096 -c -t 10 -o /data/local/tmp/trace.txt -app com.example.myapp"&lt;/span&gt;
adb shell &lt;span class="s2"&gt;"am start -W -n com.example.myapp/.MainActivity"&lt;/span&gt;
adb shell &lt;span class="s2"&gt;"atrace --async_stop -o /data/local/tmp/trace.txt"&lt;/span&gt;
adb pull /data/local/tmp/trace.txt &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;trace.txt&lt;/code&gt; in Android Studio’s &lt;strong&gt;CPU Profiler&lt;/strong&gt; or use &lt;code&gt;perfetto&lt;/code&gt; to locate sections named &lt;code&gt;MyNativeModule.initialize&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Why&lt;/em&gt;: Gives you the exact time native code spent on the UI thread before JS could start. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Confirm&lt;/em&gt;: If any section exceeds 100 ms, that module is a candidate for async initialisation.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Compare against a JSC baseline (optional)
&lt;/h3&gt;

&lt;p&gt;If you still have the previous JSC build, repeat steps 3‑7 with &lt;code&gt;hermesEnabled=false&lt;/code&gt;. Record the same three metrics. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Why&lt;/em&gt;: Provides a concrete delta to decide whether Hermes is helping or hurting. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Confirm&lt;/em&gt;: A table like:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Build&lt;/th&gt;
&lt;th&gt;TotalTime (ms)&lt;/th&gt;
&lt;th&gt;Bytecode load (ms)&lt;/th&gt;
&lt;th&gt;Native init max (ms)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JSC&lt;/td&gt;
&lt;td&gt;2100&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hermes&lt;/td&gt;
&lt;td&gt;2250&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If Hermes is slower, the extra bytecode load plus native blocking outweighs JSC’s JIT cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. Decide on a rewrite
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;If&lt;/strong&gt; &lt;code&gt;TotalTime&lt;/code&gt; is &amp;gt; 200 ms higher than the JSC baseline &lt;strong&gt;and&lt;/strong&gt; the Hermes bytecode load exceeds 100 ms &lt;strong&gt;or&lt;/strong&gt; any native init &amp;gt; 80 ms, a rewrite (splitting bundles, lazy‑loading heavy modules, moving work off the UI thread) is justified. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If&lt;/strong&gt; the delta is &amp;lt; 50 ms and the heavy native sections are &amp;lt; 30 ms, the regression is likely noise; focus on other performance goals (e.g., reducing image decoding time).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Document the numbers in a short report and present them to product and engineering leads before committing to a large refactor.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs, and when not to do it
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Developer time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Setting up tracing and Hermes logging takes ~2 h per platform.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Build churn&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Requires at least two full builds (Hermes and JSC) plus a debug rebuild for tracing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Device time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Each measurement run needs a clean device; on a CI farm you must provision a physical Android device or a high‑performance emulator (cold‑start times differ by up to 30 %).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Binary size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Enabling Hermes adds ~2.5 MB to the APK; if the app is already near the 100 MB Play Store limit, you may need to enable Android App Bundles with dynamic delivery.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Debugging complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hermes disables Chrome DevTools source‑maps unless &lt;code&gt;hermes.debug&lt;/code&gt; is true, which can hide bugs in production.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;When not to do it&lt;/strong&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If your app’s average cold start is already &amp;lt; 1 s and the user‑complaint rate is &amp;lt; 0.2 % per week, the cost of a rewrite may outweigh the benefit. &lt;/li&gt;
&lt;li&gt;If the only regression is a 20‑30 ms increase in &lt;code&gt;TotalTime&lt;/code&gt; on high‑end devices, the impact on real‑world user experience is negligible. &lt;/li&gt;
&lt;li&gt;When your team does not have bandwidth to refactor native modules to async initialisation, the risk of introducing regressions may be higher than the gain.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  At your level
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Immediate takeaway&lt;/th&gt;
&lt;th&gt;Next concrete step&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Student / first‑year engineer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Measuring is cheaper than guessing.&lt;/td&gt;
&lt;td&gt;Run &lt;code&gt;adb shell am start -W&lt;/code&gt; on a sample app and note the three timing fields.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Working engineer (feature owner)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Use Hermes‑specific logs to separate JS load from native init.&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;System.setProperty("hermes.debug","true")&lt;/code&gt; and capture &lt;code&gt;hermes:profile&lt;/code&gt; lines for your feature branch.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Senior / staff engineer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Quantify the trade‑off between bytecode load and native blocking before approving a full rewrite.&lt;/td&gt;
&lt;td&gt;Build a side‑by‑side JSC vs Hermes comparison table for the next release sprint.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lead / director&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The decision to allocate refactor time must be backed by a ≥ 100 ms total‑time delta on the target device cohort.&lt;/td&gt;
&lt;td&gt;Request a performance‑measurement ticket that includes the steps above and a budget justification.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  In the interview
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Question phrasing&lt;/strong&gt; &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“We migrated a React‑Native app to Hermes and saw a slowdown in cold‑start time. How would you determine whether the migration is the cause and if a code rewrite is justified?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Weak answer&lt;/strong&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mentions “Hermes is faster” without data. &lt;/li&gt;
&lt;li&gt;Says “just look at the logs” without specifying which logs or how to isolate the problem. &lt;/li&gt;
&lt;li&gt;Fails to mention native‑module blocking or bundle size.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Why it is weak&lt;/em&gt;: It shows awareness of Hermes but no concrete methodology, no metrics, and no trade‑off analysis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strong answer&lt;/strong&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Describes the three‑phase measurement (&lt;code&gt;am start -W&lt;/code&gt;, &lt;code&gt;hermes:profile&lt;/code&gt;, &lt;code&gt;atrace&lt;/code&gt;). &lt;/li&gt;
&lt;li&gt;Cites concrete thresholds (e.g., &amp;gt; 100 ms bytecode load, &amp;gt; 80 ms native init). &lt;/li&gt;
&lt;li&gt;Explains how to compare against a JSC baseline and how to decide based on a ≥ 100 ms delta. &lt;/li&gt;
&lt;li&gt;Mentions the cost of the measurement effort and the binary‑size impact.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Senior vs junior expectations&lt;/strong&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Junior&lt;/em&gt;: Expected to outline the basic &lt;code&gt;adb&lt;/code&gt; command, recognise the need for a baseline, and suggest looking at &lt;code&gt;hermes:profile&lt;/code&gt;. &lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Senior / staff&lt;/em&gt;: Must also discuss native‑module tracing, explain why bundle size matters, propose a data‑driven decision matrix, and address the engineering cost. &lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Director / engineering‑manager&lt;/em&gt;: Looks for the business impact (user‑complaint rate, revenue loss), the ROI of a rewrite, and how the measurement plan fits into release governance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Follow‑up that separates experience from theory&lt;/strong&gt; &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“When you performed the comparison, what was the largest native‑module init time you observed, and how did you refactor it?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A candidate who can cite a concrete figure (e.g., “&lt;code&gt;MyAnalytics.initialize&lt;/code&gt; took 120 ms and was moved to a background thread using &lt;code&gt;ReactContext.runOnJSQueueThread&lt;/code&gt;”) demonstrates hands‑on experience; a candidate who answers only “we moved it off the UI thread” without numbers likely has only read the docs.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Amit Chakraborty is a founding engineer and senior architect — React Native, AI/RAG systems and production architecture. Portfolio: &lt;a href="https://www.amitchakraborty.dev?utm_source=article&amp;amp;utm_medium=content&amp;amp;utm_campaign=hermes-and-startup-time-measuring-before-optimising" rel="noopener noreferrer"&gt;www.amitchakraborty.dev&lt;/a&gt; · &lt;a href="https://linkedin.com/in/devamitch" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://github.com/devamitch" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Open to senior and founding engineering roles, remote worldwide.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>hermes</category>
      <category>android</category>
      <category>performance</category>
    </item>
    <item>
      <title>JavaScript from absolute zero · 1. What a program is, and running your first line today</title>
      <dc:creator>Amit chakraborty</dc:creator>
      <pubDate>Tue, 20 Oct 2026 04:30:10 +0000</pubDate>
      <link>https://dev.to/techamit95ch/javascript-from-absolute-zero-1-what-a-program-is-and-running-your-first-line-today-5fpb</link>
      <guid>https://dev.to/techamit95ch/javascript-from-absolute-zero-1-what-a-program-is-and-running-your-first-line-today-5fpb</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;JavaScript from absolute zero&lt;/strong&gt; · Chapter 1 of 14 · Languages · new chapter every Tuesday morning&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;By the end of this chapter:&lt;/strong&gt; Run JavaScript on your own machine within the hour, with nothing installed beforehand.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Emma is a marketing assistant who has just been asked to test a tiny piece of JavaScript that a colleague promised would “show the current date on any computer”. She opens the shared folder, double‑clicks the file &lt;code&gt;show-date.js&lt;/code&gt;, and a blank window flashes before disappearing. The file contains the single line&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Emma sees no output, and the file’s owner replies that the script must be “run from a terminal”. Emma has never opened a terminal, never typed a command, and now she has wasted an hour trying to guess what “run from a terminal” means. The delay means the marketing campaign is postponed, and the team loses confidence in her ability to handle simple technical requests.&lt;/p&gt;

&lt;p&gt;The goal of this chapter is to get anyone—no matter how unfamiliar they are with computers—to run that exact line on their own machine within an hour, with nothing pre‑installed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Operating system&lt;/strong&gt; – Windows 10/11, macOS 13 (Ventura) or a recent Linux distribution (Ubuntu 22.04 or similar).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js&lt;/strong&gt; – version 20.10.0 (the current LTS at the time of writing).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A text editor&lt;/strong&gt; – any program that can create plain‑text files (e.g., Notepad on Windows, TextEdit in plain‑text mode on macOS, or gedit on Linux).
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Verify the setup
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open a terminal (see the next section for how to open one on each OS).
&lt;/li&gt;
&lt;li&gt;Run
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   node &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output (exact wording may differ by patch level):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   v20.10.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see that version number, the runtime is correctly installed and reachable from the command line. If the command is not found, return to the installation steps in the next section.&lt;/p&gt;




&lt;h2&gt;
  
  
  What does it mean to “run a program”?
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;program&lt;/strong&gt; is a set of instructions that a computer can execute. The instructions are stored in a &lt;strong&gt;file&lt;/strong&gt;—a collection of characters saved on disk. To &lt;strong&gt;run&lt;/strong&gt; a program means to ask the computer to read that file, interpret each instruction, and perform the actions described.&lt;/p&gt;

&lt;p&gt;In the context of JavaScript, the instructions are written in the &lt;strong&gt;JavaScript language&lt;/strong&gt;. Normally a web browser interprets JavaScript when you load a web page, but browsers are not the only places JavaScript can run. A &lt;strong&gt;runtime&lt;/strong&gt; is a piece of software that knows how to read JavaScript files and execute them. When you “run a program from a terminal”, you are invoking a runtime directly from the &lt;strong&gt;command line&lt;/strong&gt; (the text‑based interface where you type commands).&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;output&lt;/strong&gt; of a program is whatever the program tells the runtime to display. In our example, &lt;code&gt;console.log()&lt;/code&gt; tells the runtime to write a line of text to the &lt;strong&gt;standard output&lt;/strong&gt; stream, which the terminal shows as text.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where does JavaScript run outside the browser?
&lt;/h2&gt;

&lt;p&gt;The most common runtime for non‑browser JavaScript is &lt;strong&gt;Node.js&lt;/strong&gt; (often shortened to &lt;em&gt;Node&lt;/em&gt;). Node bundles the JavaScript engine used by Chrome (V8) together with a set of libraries that let JavaScript interact with the operating system—reading files, making network requests, printing to the console, and more.&lt;/p&gt;

&lt;p&gt;Why use Node for a first‑time run?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No browser needed&lt;/strong&gt; – you do not have to open a web page or understand HTML.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistent behaviour&lt;/strong&gt; – the same version of V8 runs on every platform, so the output you see on Windows will be the same on macOS or Linux.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple command&lt;/strong&gt; – a single line &lt;code&gt;node file.js&lt;/code&gt; executes the file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade‑off is that Node adds a small download (≈ 30 MB) and you must manage its version, but for a learning exercise this cost is negligible.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to get a JavaScript runtime on your computer
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Download the installer
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Windows&lt;/strong&gt; – go to &lt;a href="https://nodejs.org/en/download/" rel="noopener noreferrer"&gt;https://nodejs.org/en/download/&lt;/a&gt; and click &lt;em&gt;Windows Installer&lt;/em&gt; for the &lt;em&gt;LTS&lt;/em&gt; version (the file will be named something like &lt;code&gt;node-v20.10.0-x64.msi&lt;/code&gt;).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;macOS&lt;/strong&gt; – on the same page, click &lt;em&gt;macOS Installer&lt;/em&gt; (e.g., &lt;code&gt;node-v20.10.0.pkg&lt;/code&gt;).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linux&lt;/strong&gt; – the easiest route is the package manager. On Ubuntu, open a terminal and run
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://deb.nodesource.com/setup_20.x | &lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; bash -
  &lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nodejs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(The &lt;code&gt;curl&lt;/code&gt; command fetches a script that adds the official Node repository; &lt;code&gt;sudo&lt;/code&gt; runs the following command with administrator rights.)&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Run the installer
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Follow the on‑screen prompts. Accept the licence, keep the default installation path, and allow the installer to add Node to your &lt;strong&gt;PATH&lt;/strong&gt; (environment variable that tells the terminal where to find executables).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Verify the installation
&lt;/h3&gt;

&lt;p&gt;Open a new terminal window (closing the old one ensures the updated PATH is read) and type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see &lt;code&gt;v20.10.0&lt;/code&gt; (or a later LTS version). If you see an error like &lt;code&gt;node: command not found&lt;/code&gt;, the installer failed to modify PATH; see the “When this breaks” section.&lt;/p&gt;




&lt;h2&gt;
  
  
  Build it
&lt;/h2&gt;

&lt;p&gt;Below are the concrete actions that turn Emma’s vague request into a working result.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create a JavaScript file&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Open your text editor.
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Paste the following line:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Save the file as &lt;code&gt;show-date.js&lt;/code&gt; on your Desktop (or any folder you can easily navigate to).  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Why&lt;/em&gt;: The file name ends with &lt;code&gt;.js&lt;/code&gt;, the conventional &lt;strong&gt;extension&lt;/strong&gt; that signals “this is a JavaScript source file”.  &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Open a terminal in the file’s folder&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Windows&lt;/strong&gt; – open &lt;em&gt;File Explorer&lt;/em&gt;, navigate to the folder, type &lt;code&gt;cmd&lt;/code&gt; in the address bar, and press &lt;strong&gt;Enter&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;macOS&lt;/strong&gt; – open &lt;em&gt;Finder&lt;/em&gt;, go to the folder, right‑click and choose &lt;em&gt;New Terminal at Folder&lt;/em&gt; (or open &lt;em&gt;Terminal&lt;/em&gt; and run &lt;code&gt;cd ~/Desktop&lt;/code&gt; if you saved it on the Desktop).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linux&lt;/strong&gt; – open your file manager, right‑click the folder and select &lt;em&gt;Open in Terminal&lt;/em&gt;, or use &lt;code&gt;cd&lt;/code&gt; to change directory.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Confirm&lt;/em&gt;: The prompt should show the path, e.g., &lt;code&gt;C:\Users\Emma\Desktop&amp;gt;&lt;/code&gt; or &lt;code&gt;/home/emma/Desktop$&lt;/code&gt;.  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run the file with Node&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   node show-date.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output (example):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   2026-10-20T14:32:11.456Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Why&lt;/em&gt;: &lt;code&gt;node&lt;/code&gt; tells the runtime to execute the file that follows. The &lt;code&gt;console.log&lt;/code&gt; statement prints the current date and time in ISO format.  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Confirm the result&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;If you see a line that looks like a date and time, the program ran successfully.
&lt;/li&gt;
&lt;li&gt;If you see an error, note the exact wording and move to the “When this breaks” section.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You have now executed a JavaScript program on your own machine without any prior setup.&lt;/p&gt;




&lt;h2&gt;
  
  
  When this breaks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. “&lt;code&gt;node&lt;/code&gt; is not recognized as an internal or external command”
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom&lt;/strong&gt; – the terminal prints:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  'node' is not recognized as an internal or external command,
  operable program or batch file.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cause&lt;/strong&gt; – the installer did not add the Node binary directory to the system &lt;strong&gt;PATH&lt;/strong&gt;.  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt; –  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Windows&lt;/strong&gt; – open &lt;em&gt;Start → Environment Variables&lt;/em&gt;, edit the &lt;code&gt;Path&lt;/code&gt; variable, and add the folder &lt;code&gt;C:\Program Files\nodejs\&lt;/code&gt;. Restart the terminal.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;macOS/Linux&lt;/strong&gt; – add the line &lt;code&gt;export PATH="/usr/local/bin:$PATH"&lt;/code&gt; to &lt;code&gt;~/.bash_profile&lt;/code&gt; (or &lt;code&gt;~/.zshrc&lt;/code&gt; on newer macOS) and run &lt;code&gt;source ~/.bash_profile&lt;/code&gt;.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Verify with &lt;code&gt;node -v&lt;/code&gt; again.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Syntax error in the file
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom&lt;/strong&gt; – after running &lt;code&gt;node show-date.js&lt;/code&gt; you see:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  SyntaxError: Unexpected token '}'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cause&lt;/strong&gt; – the JavaScript parser found characters that do not form a valid statement, often due to a missing or extra punctuation mark.  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt; – open &lt;code&gt;show-date.js&lt;/code&gt;, ensure the file contains exactly the line shown earlier, save, and re‑run.  &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Permission denied (Linux/macOS)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom&lt;/strong&gt; – the terminal prints:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  bash: node: Permission denied
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cause&lt;/strong&gt; – the Node binary is not executable for your user.  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt; – run &lt;code&gt;chmod +x $(which node)&lt;/code&gt; to add execute permission, then retry.  &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Wrong directory
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Symptom&lt;/strong&gt; – Node reports &lt;code&gt;Error: Cannot find module '/path/to/show-date.js'&lt;/code&gt;.  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cause&lt;/strong&gt; – you are not in the folder that contains the file.  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt; – use &lt;code&gt;ls&lt;/code&gt; (Linux/macOS) or &lt;code&gt;dir&lt;/code&gt; (Windows) to list files; ensure &lt;code&gt;show-date.js&lt;/code&gt; appears. Change directory with &lt;code&gt;cd path/to/folder&lt;/code&gt;.  &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are the most common obstacles for a first‑time run. Once resolved, the workflow becomes reliable.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Installation size&lt;/strong&gt; – Node adds roughly 30 MB to disk usage. On a machine with limited SSD space, this is a measurable cost.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version lock‑in&lt;/strong&gt; – Scripts written for a specific Node version may break if the runtime is upgraded without testing. The solution is to use a version manager (e.g., &lt;code&gt;nvm&lt;/code&gt;) once you progress beyond this chapter.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security surface&lt;/strong&gt; – Installing any executable from the internet introduces a potential attack vector. Node’s installer is signed by the Node.js Foundation; verify the signature if you work in a high‑security environment.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learning curve&lt;/strong&gt; – Understanding PATH, terminals, and file extensions adds cognitive load. However, these concepts are universal across programming languages, so the cost pays off later.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short, the cost is modest and outweighed by the immediate benefit: being able to execute JavaScript directly.&lt;/p&gt;




&lt;h2&gt;
  
  
  In the interview
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Typical junior‑level question&lt;/strong&gt; – “How would you run a single line of JavaScript on a computer that has no browser installed?”
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Weak answer&lt;/em&gt;: “I would open Chrome and type it in the console.”&lt;br&gt;&lt;br&gt;
  &lt;em&gt;Why weak&lt;/em&gt;: It assumes a browser is present and does not address the “no browser” constraint.  &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Strong answer&lt;/em&gt;: “I would install Node.js, create a file with a &lt;code&gt;.js&lt;/code&gt; extension, and run &lt;code&gt;node filename.js&lt;/code&gt; from a terminal. I would verify the installation with &lt;code&gt;node -v&lt;/code&gt; and confirm the output matches the expected result. If &lt;code&gt;node&lt;/code&gt; is not on the PATH, I would adjust the environment variable accordingly.”  &lt;/p&gt;

&lt;p&gt;This answer mentions the trade‑off (installing a runtime), the constraint (no browser), the failure mode (PATH), and a concrete metric (checking the version).  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Senior‑level extension&lt;/strong&gt; – “What are the implications of using Node for a quick script versus a browser‑based console, and how would you manage version drift in a team?”
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A senior candidate should discuss the overhead of managing Node versions, the reproducibility benefits of using a version manager, and the security considerations of running arbitrary scripts.  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Follow‑up to separate experience from theory&lt;/strong&gt; – “Can you describe a time when you ran a script on a CI server and encountered a ‘node not found’ error? How did you resolve it?”
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Candidates who have actually performed the steps will reference PATH configuration or the use of a &lt;code&gt;node&lt;/code&gt; Docker image, demonstrating hands‑on experience.&lt;/p&gt;




&lt;h2&gt;
  
  
  Your tasks
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create and run a “Hello, world!” script&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Write a file &lt;code&gt;hello.js&lt;/code&gt; that prints the text &lt;code&gt;Hello, world!&lt;/code&gt; using &lt;code&gt;console.log&lt;/code&gt;. Run it with Node and verify the exact output appears.&lt;/em&gt;  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Display an environment variable&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Add a line to &lt;code&gt;hello.js&lt;/code&gt; that prints the value of the &lt;code&gt;PATH&lt;/code&gt; environment variable (&lt;code&gt;process.env.PATH&lt;/code&gt;). Run the script and note the long string that appears.&lt;/em&gt;  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Introduce a deliberate syntax error&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Edit &lt;code&gt;hello.js&lt;/code&gt; to remove the closing parenthesis from the &lt;code&gt;console.log&lt;/code&gt; call. Run the script, capture the error message, and then fix the file so it runs again.&lt;/em&gt;  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run the script from a different directory&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Move &lt;code&gt;hello.js&lt;/code&gt; to a sub‑folder called &lt;code&gt;scripts&lt;/code&gt;. From the parent folder, execute &lt;code&gt;node scripts/hello.js&lt;/code&gt;. Confirm the script still runs without changing the working directory.&lt;/em&gt;  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Automate the run with an npm script&lt;/strong&gt; &lt;em&gt;(requires the next chapter’s knowledge of npm, but you can still create a &lt;code&gt;package.json&lt;/code&gt; manually)&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Create a &lt;code&gt;package.json&lt;/code&gt; containing a &lt;code&gt;"start"&lt;/code&gt; script that runs &lt;code&gt;node hello.js&lt;/code&gt;. Execute the script with &lt;code&gt;npm start&lt;/code&gt; and verify the same output.&lt;/em&gt;  &lt;/p&gt;

&lt;p&gt;Each task is considered complete when the terminal shows the expected output and no error messages remain.&lt;/p&gt;







&lt;h2&gt;
  
  
  Your tasks this week
&lt;/h2&gt;

&lt;p&gt;Do the exercises above before the next chapter. Reading a tutorial and doing&lt;br&gt;
one are different activities and only one of them changes what you can build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stuck on any of them?&lt;/strong&gt; Say so — describe what you tried and what happened:&lt;br&gt;
&lt;a href="https://www.amitchakraborty.dev/learn/javascript-from-zero#stuck" rel="noopener noreferrer"&gt;tell me where you got stuck&lt;/a&gt;. I read every one, and the questions&lt;br&gt;
that come back more than twice get answered in the next chapter.&lt;/p&gt;

&lt;h2&gt;
  
  
  JavaScript from absolute zero
&lt;/h2&gt;

&lt;p&gt;Chapter 1 of 14. New chapter every Tuesday morning.&lt;br&gt;
Next: &lt;strong&gt;Values, variables and types&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;· &lt;a href="https://www.amitchakraborty.dev/learn/javascript-from-zero" rel="noopener noreferrer"&gt;The full syllabus and every chapter so far&lt;/a&gt;&lt;br&gt;
· Subscribers also get the condensed notes for this chapter, the running&lt;br&gt;
  recap of everything the series has covered, and the extended guidance:&lt;br&gt;
  &lt;a href="https://www.amitchakraborty.dev/#newsletter" rel="noopener noreferrer"&gt;subscribe&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by Amit Chakraborty — founding engineer and senior architect: React Native, AI and RAG systems, production architecture. &lt;a href="https://www.amitchakraborty.dev?utm_source=curriculum&amp;amp;utm_medium=content&amp;amp;utm_campaign=javascript-from-zero" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt; · &lt;a href="https://linkedin.com/in/devamitch" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://github.com/devamitch" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Need this built, reviewed or taught to your team? &lt;a href="https://www.amitchakraborty.dev/#contact" rel="noopener noreferrer"&gt;Get in touch&lt;/a&gt; or email &lt;a href="mailto:amit@devamit.co.in"&gt;amit@devamit.co.in&lt;/a&gt;. Available for senior and founding engineering roles, consulting and training, remote worldwide.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>languages</category>
      <category>javascriptfromabsolutezero</category>
      <category>whatdoesitmeantorunaprogram</category>
    </item>
    <item>
      <title>When a Fabric rewrite feels faster but you can’t prove it</title>
      <dc:creator>Amit chakraborty</dc:creator>
      <pubDate>Tue, 20 Oct 2026 04:30:04 +0000</pubDate>
      <link>https://dev.to/techamit95ch/when-a-fabric-rewrite-feels-faster-but-you-cant-prove-it-2ifa</link>
      <guid>https://dev.to/techamit95ch/when-a-fabric-rewrite-feels-faster-but-you-cant-prove-it-2ifa</guid>
      <description>&lt;h2&gt;
  
  
  The situation
&lt;/h2&gt;

&lt;p&gt;Say you are a React Native engineer on a mobile team that has just upgraded the UI stack from the legacy renderer to the new Fabric renderer (React Native 0.71). Your goal is to reduce frame‑drop on a scrolling list that currently hits 45 ms per frame on a mid‑range Android device. After switching to Fabric you notice the UI feels smoother during a quick manual test, but the performance monitor still reports occasional spikes to 70 ms. The team’s sprint deadline is two weeks away and the product manager is demanding a quantifiable improvement before the release can be approved. &lt;/p&gt;

&lt;p&gt;The cost of not having a solid measurement is threefold:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Time:&lt;/strong&gt; you may spend another week chasing “feel‑good” changes that don’t actually move the needle. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Risk:&lt;/strong&gt; a regression could slip into production because the baseline wasn’t captured. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stakeholder confidence:&lt;/strong&gt; without hard data the product manager will push back, potentially delaying the launch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The core problem is &lt;strong&gt;how to instrument a Fabric‑based app so you can decide, with numbers, whether the rewrite is worth the effort&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;Fabric changes the way the JavaScript thread talks to the UI thread:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Legacy (Bridge)&lt;/th&gt;
&lt;th&gt;Fabric&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JSON‑encoded messages over a single queue&lt;/td&gt;
&lt;td&gt;Direct, typed C++ bindings; batched UI commands&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All layout work on the UI thread&lt;/td&gt;
&lt;td&gt;Layout can be off‑loaded to the JS thread via &lt;code&gt;LayoutAnimation&lt;/code&gt; APIs, but still ends up on UI thread for final paint&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Because the data path is different, the existing instrumentation that ships with React Native (e.g., &lt;code&gt;Systrace&lt;/code&gt; or the “Performance Monitor” in the developer menu) often &lt;strong&gt;does not surface the new cost centres&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The “JS → Native bridge” graph still shows a single “Bridge” node, even though Fabric bypasses it. &lt;/li&gt;
&lt;li&gt;The UI thread’s “Layout” and “Mount” phases are now split into separate C++ calls that are invisible to the default timeline. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Consequently, developers see the same “JS time” and “UI time” numbers, but the underlying work has moved. Without specialised metrics you cannot tell whether the new path reduced overall work or simply shifted latency to a place you’re not watching.&lt;/p&gt;




&lt;h2&gt;
  
  
  The fix, step by step
&lt;/h2&gt;

&lt;p&gt;Below is a reproducible workflow that adds low‑overhead instrumentation to a Fabric app, validates the data, and uses it to decide if the rewrite is worthwhile.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Enable the Fabric‑specific PerfMarkers
&lt;/h3&gt;

&lt;p&gt;Fabric ships a set of &lt;code&gt;PerfMarker&lt;/code&gt; strings that are emitted via &lt;code&gt;ReactPerfLogger&lt;/code&gt;. They are only active when the &lt;code&gt;RCT_ENABLE_FABRIC&lt;/code&gt; build flag is true and when &lt;code&gt;RCT_DEV&lt;/code&gt; is true.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# In ios/Podfile or android/app/build.gradle set:&lt;/span&gt;
&lt;span class="nv"&gt;RCT_ENABLE_FABRIC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="nv"&gt;RCT_DEV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then rebuild the app (&lt;code&gt;npx react-native run-android&lt;/code&gt; or &lt;code&gt;run-ios&lt;/code&gt;). &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why&lt;/strong&gt; &lt;br&gt;
These markers are emitted as &lt;code&gt;RCTLog&lt;/code&gt; events that &lt;code&gt;systrace&lt;/code&gt; can capture. They give you timestamps for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;FabricMountStart&lt;/code&gt; / &lt;code&gt;FabricMountStop&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FabricLayoutStart&lt;/code&gt; / &lt;code&gt;FabricLayoutStop&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FabricCommitStart&lt;/code&gt; / &lt;code&gt;FabricCommitStop&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How to confirm&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;adb shell setprop debug.reactperf 1&lt;/code&gt; (Android) or set the &lt;code&gt;RCT_DEBUG&lt;/code&gt; environment variable (iOS). Then launch the app and open &lt;code&gt;adb logcat | grep PerfMarker&lt;/code&gt;. You should see lines like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[PerfMarker] FabricMountStart: 1234567890
[PerfMarker] FabricMountStop: 1234567950
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see matching start/stop pairs, the markers are active.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Capture the markers with Systrace (or Perfetto)
&lt;/h3&gt;

&lt;p&gt;Systrace aggregates &lt;code&gt;PerfMarker&lt;/code&gt; events into a timeline you can view in Chrome.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Android&lt;/span&gt;
adb shell atrace &lt;span class="nt"&gt;--async_start&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="nt"&gt;-b&lt;/span&gt; 4096 &lt;span class="nt"&gt;-t&lt;/span&gt; 10 &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /data/local/tmp/trace.html &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-a&lt;/span&gt; com.yourapp &lt;span class="nt"&gt;-p&lt;/span&gt; com.facebook.react.perfmarker

adb pull /data/local/tmp/trace.html &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;trace.html&lt;/code&gt; in Chrome’s &lt;code&gt;chrome://tracing&lt;/code&gt; UI. Look for the Fabric sections; they will appear as distinct rows labelled “FabricMount”, “FabricLayout”, etc.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why&lt;/strong&gt; &lt;br&gt;
The visual timeline lets you compare the duration of each phase before and after your optimisation. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to confirm&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;Zoom into a frame where you expect a change (e.g., a scroll event). Note the duration of &lt;code&gt;FabricMount&lt;/code&gt; in milliseconds. Record the value (e.g., 3.2 ms). Do the same for the baseline (legacy) build; you should see a separate set of markers called &lt;code&gt;BridgeMount&lt;/code&gt; with a larger duration (e.g., 7.8 ms). &lt;/p&gt;

&lt;p&gt;If the numbers differ, you have measurable data.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Add a custom &lt;code&gt;RCTPerformanceLogger&lt;/code&gt; counter
&lt;/h3&gt;

&lt;p&gt;The built‑in &lt;code&gt;RCTPerformanceLogger&lt;/code&gt; stores timings in a circular buffer. You can add a custom counter for “total frame time” that includes both JS and UI work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create a native module (Android example):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// FabricPerfModule.java&lt;/span&gt;
&lt;span class="kn"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;com.yourapp&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.facebook.react.bridge.ReactApplicationContext&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.facebook.react.bridge.ReactContextBaseJavaModule&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.facebook.react.modules.core.DeviceEventManagerModule&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.facebook.react.uimanager.ReactPerformanceLogger&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FabricPerfModule&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;ReactContextBaseJavaModule&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="no"&gt;CUSTOM_TOTAL_FRAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// arbitrary id &amp;gt; 500&lt;/span&gt;

  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;FabricPerfModule&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ReactApplicationContext&lt;/span&gt; &lt;span class="n"&gt;reactContext&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;super&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reactContext&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="nc"&gt;ReactPerformanceLogger&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ReactPerformanceLogger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addCustomMetric&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;CUSTOM_TOTAL_FRAME&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@Override&lt;/span&gt;
  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"FabricPerf"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Expose a method to JS to read the metric&lt;/span&gt;
  &lt;span class="nd"&gt;@ReactMethod&lt;/span&gt;
  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;getLastFrameTime&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Promise&lt;/span&gt; &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ReactPerformanceLogger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDuration&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;CUSTOM_TOTAL_FRAME&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;promise&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register the module in &lt;code&gt;MainApplication.java&lt;/code&gt; and expose a JS wrapper:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;NativeModules&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="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&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;getLastFrameTime&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="nx"&gt;NativeModules&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FabricPerf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLastFrameTime&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why&lt;/strong&gt; &lt;br&gt;
A custom metric lets you aggregate the time spent in &lt;code&gt;FabricMount&lt;/code&gt; + &lt;code&gt;FabricLayout&lt;/code&gt; + JS execution for each frame, giving a single “frame budget” number you can compare against the 16 ms target for 60 fps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to confirm&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;Add a &lt;code&gt;useEffect&lt;/code&gt; hook that logs the value after each render:&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="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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&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;ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getLastFrameTime&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Last frame time (ms):&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;1000&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&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="p"&gt;[]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the app and watch the console. You should see a stable series of numbers (e.g., &lt;code&gt;12.4&lt;/code&gt;, &lt;code&gt;13.1&lt;/code&gt;). If the numbers stay below 16 ms consistently, you have quantitative proof of success.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Automate regression detection in CI
&lt;/h3&gt;

&lt;p&gt;Instrumented measurements are only useful if they are repeatable. Add a script that runs the app on a headless emulator, captures the PerfMarker trace, extracts the average &lt;code&gt;FabricMount&lt;/code&gt; duration, and fails the build if it exceeds a threshold.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;Create a Node script &lt;code&gt;measure-fabric.js&lt;/code&gt;:&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;child_process&lt;/span&gt;&lt;span class="dl"&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;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&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;runTrace&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`adb shell setprop debug.reactperf 1`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`adb shell am start -n com.yourapp/.MainActivity`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`adb shell sleep 5`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// let the app settle&lt;/span&gt;
  &lt;span class="nf"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`adb shell atrace --async_start -c -b 4096 -t 5 -z -o /data/local/tmp/trace.html -a com.yourapp -p com.facebook.react.perfmarker`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`adb pull /data/local/tmp/trace.html .`&lt;/span&gt;&lt;span class="p"&gt;);&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;parseTrace&lt;/span&gt;&lt;span class="p"&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;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;trace.html&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="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&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;mountTimes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/FabricMountStart.*&lt;/span&gt;&lt;span class="se"&gt;?(\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;.*&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;FabricMountStop.*&lt;/span&gt;&lt;span class="se"&gt;?(\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;)]&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;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;avg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mountTimes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;b&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;a&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;b&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="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;mountTimes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;runTrace&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;avgMount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseTrace&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Average FabricMount: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;avgMount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; ms`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;avgMount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;4.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;❌ FabricMount exceeds budget (4 ms)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add this step to your CI pipeline (e.g., GitHub Actions). &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why&lt;/strong&gt; &lt;br&gt;
CI gating prevents accidental regressions caused by future library upgrades or code changes. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to confirm&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;Push a change that deliberately adds a heavy view (e.g., a &lt;code&gt;FlatList&lt;/code&gt; with 10 000 items). The CI run should fail, printing the “exceeds budget” message. Reverting the change restores the green status, proving the test works.&lt;/p&gt;
&lt;h3&gt;
  
  
  5. Correlate with real‑world metrics (FPS, UI‑thread CPU)
&lt;/h3&gt;

&lt;p&gt;Instrumentation inside the app tells you about internal timings, but the ultimate metric is &lt;strong&gt;frames per second&lt;/strong&gt; as observed by the user. Use Android’s &lt;code&gt;adb shell dumpsys gfxinfo&lt;/code&gt; or iOS’s &lt;code&gt;measureFPS&lt;/code&gt; tool to capture the FPS while the same interactions are performed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to do (Android)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb shell dumpsys gfxinfo com.yourapp &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; gfx.txt
&lt;span class="c"&gt;# Parse the “Total” column for each frame; compute average FPS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What to do (iOS)&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;Open Xcode Instruments → “Core Animation” → record a scroll session. Export the CSV and calculate average FPS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why&lt;/strong&gt; &lt;br&gt;
If your Fabric metrics show a 3 ms reduction in mount time but FPS stays at 55 fps, the bottleneck lies elsewhere (e.g., image decoding). &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to confirm&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;Run the same scripted scroll (e.g., using &lt;code&gt;detox&lt;/code&gt; or &lt;code&gt;appium&lt;/code&gt;) on both the legacy and Fabric builds. Compare the average FPS numbers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Build&lt;/th&gt;
&lt;th&gt;Avg Mount (ms)&lt;/th&gt;
&lt;th&gt;Avg FPS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Legacy&lt;/td&gt;
&lt;td&gt;7.8&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fabric&lt;/td&gt;
&lt;td&gt;3.2&lt;/td&gt;
&lt;td&gt;57&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A clear uplift in both columns validates that the rewrite is worth shipping.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it costs, and when not to do it
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Runtime overhead&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Each &lt;code&gt;PerfMarker&lt;/code&gt; incurs a nanosecond‑scale write to a shared buffer. In production builds the overhead is typically &lt;strong&gt;&amp;lt; 0.5 % CPU&lt;/strong&gt; on modern devices, but on low‑end Android (e.g., Snapdragon 450) you may see a 1–2 % increase in UI‑thread utilisation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bundle size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Adding the native module (&lt;code&gt;FabricPerfModule&lt;/code&gt;) adds ~30 KB of compiled code (mostly logging glue). This is negligible compared with the overall binary (≈ 30 MB).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Build complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Enabling &lt;code&gt;RCT_ENABLE_FABRIC&lt;/code&gt; and &lt;code&gt;RCT_DEV&lt;/code&gt; together forces a debug build; you cannot collect these markers from a release‑signed APK. Therefore you must maintain a separate “performance‑profile” build variant.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data noise&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;In CI, emulator timing can be unstable. Use a physical device (e.g., Pixel 5) with a locked CPU frequency to obtain reproducible numbers.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;When not to do it&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;• If the feature under test is a one‑off UI change that will not be shipped to many users.&lt;br&gt;• When you are already at the 60 fps ceiling and additional optimisation yields diminishing returns.&lt;br&gt;• In a hot‑fix window where adding native modules would delay the release more than the performance gain.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In practice, the &lt;strong&gt;benefit‑to‑cost ratio&lt;/strong&gt; is favourable when you are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Targeting a high‑traffic screen (e.g., home feed) where a 2 ms reduction translates to a measurable increase in retained users.
&lt;/li&gt;
&lt;li&gt;Working on a platform‑wide migration (all screens will run on Fabric), making the instrumentation a reusable asset.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  At your level
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Take‑away&lt;/th&gt;
&lt;th&gt;One concrete next step&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Student / first‑year&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Instrumentation is the only way to turn “it feels faster” into a defensible claim.&lt;/td&gt;
&lt;td&gt;Add the &lt;code&gt;FabricPerfModule&lt;/code&gt; to a sample app, run the CI script locally, and verify the average mount time printed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Working engineer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Use the built‑in &lt;code&gt;PerfMarker&lt;/code&gt;s plus a custom counter to create a regression test that gates future changes.&lt;/td&gt;
&lt;td&gt;Integrate &lt;code&gt;measure-fabric.js&lt;/code&gt; into your existing CI pipeline and set a realistic budget (e.g., 4 ms for mount).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Senior / staff&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Correlate internal timings with user‑facing metrics (FPS, ANR rate) and expose the data in a dashboard for product owners.&lt;/td&gt;
&lt;td&gt;Build a small internal Grafana panel that ingests the CI JSON output and shows trends across releases.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lead / director&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Allocate budget for a dedicated performance‑profiling build variant and for the hardware (mid‑range test phones) needed to keep measurements trustworthy.&lt;/td&gt;
&lt;td&gt;Approve purchase of two representative Android devices and schedule a weekly “Performance Health” meeting that reviews the Fabric metrics.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  In the interview
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Typical interview prompt&lt;/strong&gt; &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“We migrated a large screen from the legacy renderer to Fabric. How would you prove that the migration actually improved performance, and how would you decide whether the effort was justified?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Weak answer&lt;/strong&gt; &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“I would look at the FPS counter in the developer menu and compare before/after. If the FPS goes up, the migration is good.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Why it’s weak&lt;/em&gt;: It mentions only a surface‑level metric, ignores internal work redistribution, provides no method for repeatable measurement, and does not discuss trade‑offs or failure modes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strong answer&lt;/strong&gt; &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Identify the constraint&lt;/strong&gt; – the 16 ms per‑frame budget for 60 fps on target devices. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instrument the new path&lt;/strong&gt; – enable Fabric &lt;code&gt;PerfMarker&lt;/code&gt;s, add a custom &lt;code&gt;RCTPerformanceLogger&lt;/code&gt; counter for total frame time, and capture the data with Systrace/Perfetto. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate the data&lt;/strong&gt; – confirm start/stop pairs appear, extract average &lt;code&gt;FabricMount&lt;/code&gt; and &lt;code&gt;FabricLayout&lt;/code&gt; durations, and compare them to the legacy &lt;code&gt;BridgeMount&lt;/code&gt; numbers. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correlate with user‑facing metrics&lt;/strong&gt; – run &lt;code&gt;adb shell dumpsys gfxinfo&lt;/code&gt; (or iOS Instruments) on the same scripted interaction to ensure FPS improves in proportion to the internal timing reduction. &lt;/li&gt;
&lt;li&gt;**Quantify the cost&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Amit Chakraborty is a founding engineer and senior architect — React Native, AI/RAG systems and production architecture. Portfolio: &lt;a href="https://www.amitchakraborty.dev?utm_source=article&amp;amp;utm_medium=content&amp;amp;utm_campaign=fabric-renderer-internals-measuring-before-optimising" rel="noopener noreferrer"&gt;www.amitchakraborty.dev&lt;/a&gt; · &lt;a href="https://linkedin.com/in/devamitch" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://github.com/devamitch" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Open to senior and founding engineering roles, remote worldwide.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>fabric</category>
      <category>android</category>
      <category>performance</category>
    </item>
    <item>
      <title>React, deeply · 1. Rendering and reconciliation: what React does on a state change</title>
      <dc:creator>Amit chakraborty</dc:creator>
      <pubDate>Mon, 19 Oct 2026 16:30:11 +0000</pubDate>
      <link>https://dev.to/techamit95ch/react-deeply-1-rendering-and-reconciliation-what-react-does-on-a-state-change-3903</link>
      <guid>https://dev.to/techamit95ch/react-deeply-1-rendering-and-reconciliation-what-react-does-on-a-state-change-3903</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;React, deeply&lt;/strong&gt; · Chapter 1 of 12 · Web · new chapter every Monday night&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;By the end of this chapter:&lt;/strong&gt; Describe the render, reconcile and commit phases precisely enough to predict behaviour.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Emma is a front‑end engineer on a two‑month sprint for a real‑time analytics dashboard. The UI pulls live metrics from a WebSocket and updates a table every second. After merging a new “filter by region” feature, the dashboard starts missing updates: the table lags by up to three seconds, the CPU on the user’s laptop spikes to 90 % and the performance monitoring alert fires every time a user opens the page. The sprint deadline is three days away, and the team cannot afford to roll back the feature.&lt;/p&gt;

&lt;p&gt;Emma knows that React’s &lt;code&gt;useState&lt;/code&gt; call in the filter component is being invoked correctly – the state value changes, the UI shows the new region label – but she cannot see why the rest of the component tree is taking so long to reflect the new data. She has the React docs open, but they only describe the high‑level “React re‑renders when state changes”. She needs a concrete, step‑by‑step mental model of what React actually does after a state change, how the three phases (render, reconciliation, commit) interact, and how to predict whether a change will cause extra work. Only then can she pinpoint the bottleneck and fix the dashboard before the deadline.&lt;/p&gt;




&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node&lt;/strong&gt;&amp;nbsp;≥ 18.15.0
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm&lt;/strong&gt;&amp;nbsp;≥ 9.6.0
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React&lt;/strong&gt;&amp;nbsp;18.2.0 and &lt;strong&gt;React‑DOM&lt;/strong&gt;&amp;nbsp;18.2.0 (the current stable release as of October 2026)
&lt;/li&gt;
&lt;li&gt;A fresh create‑react‑app project (or any existing React 18 app) – run &lt;code&gt;npx create-react-app r18-demo --template cra-template-pwa&lt;/code&gt; to get a working baseline.
&lt;/li&gt;
&lt;li&gt;Verify the setup works by starting the dev server and seeing “Edit src/App.js” in the browser:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;r18-demo
npm start
&lt;span class="c"&gt;# Expected output in the terminal:&lt;/span&gt;
&lt;span class="c"&gt;# &amp;gt; Local:   http://localhost:3000&lt;/span&gt;
&lt;span class="c"&gt;# Open that URL – you should see the default CRA page.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the page renders, you have a correct environment and can proceed.&lt;/p&gt;




&lt;h2&gt;
  
  
  What does a state update trigger?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Does React schedule work or run it immediately?
&lt;/h3&gt;

&lt;p&gt;When &lt;code&gt;setState&lt;/code&gt; (or the setter returned by &lt;code&gt;useState&lt;/code&gt;) is called, React does &lt;strong&gt;not&lt;/strong&gt; synchronously touch the DOM. Instead it creates a &lt;em&gt;update&lt;/em&gt; object, pushes it onto the fiber’s update queue, and asks the scheduler to perform work. In React 18 the scheduler always &lt;strong&gt;batches&lt;/strong&gt; updates that happen within the same tick, even if they originate from different event sources.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Why it matters&lt;/em&gt;: If you call &lt;code&gt;setState&lt;/code&gt; inside a &lt;code&gt;setTimeout&lt;/code&gt;, React still batches that call with any other updates that happen before the next paint. Knowing the batching behaviour tells you whether a series of rapid state changes will cause a single render or many.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Render phase – building the “virtual” tree
&lt;/h3&gt;

&lt;p&gt;During the &lt;strong&gt;render phase&lt;/strong&gt; React walks the fiber tree depth‑first, calling each component function to produce a new &lt;em&gt;React element&lt;/em&gt; tree (the “virtual DOM”). No side‑effects run here – even &lt;code&gt;useEffect&lt;/code&gt; callbacks are not executed. The result is a new set of fibers that mirror the element tree.&lt;/p&gt;

&lt;p&gt;Key points:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pure function execution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Component bodies run with the current state/props.&lt;/td&gt;
&lt;td&gt;Guarantees determinism – the same inputs give the same output.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hooks order&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;React validates that the hook call order matches the previous render.&lt;/td&gt;
&lt;td&gt;Mismatched order throws “Rendered more hooks than during the previous render”.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Early bail‑out&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;If a component’s props and state are shallow‑equal to the previous render, React may skip rendering its children (the &lt;em&gt;memo&lt;/em&gt; optimisation).&lt;/td&gt;
&lt;td&gt;Prevents unnecessary work.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can confirm the render phase by adding &lt;code&gt;console.log('render', name)&lt;/code&gt; at the top of a component; you’ll see it run before any DOM changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Reconciliation – diffing the old and new fibers
&lt;/h3&gt;

&lt;p&gt;After the new tree is built, React enters the &lt;strong&gt;reconciliation&lt;/strong&gt; (or “diff”) phase. It compares the new fibers with the previous ones to decide what needs to change.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keyed children&lt;/strong&gt; – If a list has a stable &lt;code&gt;key&lt;/code&gt; prop, React can match old and new items by key, moving existing DOM nodes instead of recreating them. Without keys, React falls back to index‑based matching, which can cause unnecessary re‑mounts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Type change&lt;/strong&gt; – If the element type changes (e.g., &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; → &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt;), React discards the old fiber subtree and creates a new one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Effect list construction&lt;/strong&gt; – While reconciling, React builds three linked lists: &lt;em&gt;Placement&lt;/em&gt;, &lt;em&gt;Update&lt;/em&gt;, and &lt;em&gt;Deletion&lt;/em&gt;. These lists are later used in the commit phase.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cost of reconciliation is proportional to the number of fibers examined. A deep tree with many siblings can become expensive if keys are missing or if component props change on every render.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Commit phase – mutating the DOM and running effects
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;commit phase&lt;/strong&gt; is the only phase that mutates the host environment (the browser DOM). It proceeds in three sub‑steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Before mutation&lt;/strong&gt; – Runs &lt;code&gt;getSnapshotBeforeUpdate&lt;/code&gt; (class components) and prepares layout effects. No DOM changes yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mutation&lt;/strong&gt; – Applies all changes from the effect lists: inserts, updates, deletions. This is where the browser repaints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layout effects&lt;/strong&gt; – Executes &lt;code&gt;useLayoutEffect&lt;/code&gt; callbacks synchronously after the DOM mutation but before the browser paints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Passive effects&lt;/strong&gt; – Schedules &lt;code&gt;useEffect&lt;/code&gt; callbacks to run after the paint (asynchronously).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because layout effects run before the browser paints, they can block rendering if they perform heavy work. Passive effects, on the other hand, never block the paint.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to observe each phase in a real component
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. How can I tell when the render phase starts and ends?
&lt;/h3&gt;

&lt;p&gt;Add a &lt;code&gt;console.time&lt;/code&gt;/&lt;code&gt;console.timeEnd&lt;/code&gt; pair at the top of the component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;TableRow&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TableRow render&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// component body …&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeEnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TableRow render&lt;/span&gt;&lt;span class="dl"&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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* … */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The console will show a line like &lt;code&gt;TableRow render: 0.42ms&lt;/code&gt;. If you see many rows each taking &amp;gt; 1 ms, the render phase is a candidate for optimisation.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. How do I see the reconciliation decisions?
&lt;/h3&gt;

&lt;p&gt;React DevTools’ “Profiler” tab records &lt;em&gt;mount&lt;/em&gt; and &lt;em&gt;update&lt;/em&gt; timings and shows a “flame graph”. Hover over a component to see whether the update was a “re‑render” (same type, same key) or a “re‑mount” (type changed, key missing). A re‑mount will display a “Mount” badge.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. How can I verify when effects run relative to the DOM mutation?
&lt;/h3&gt;

&lt;p&gt;Insert logs in both &lt;code&gt;useLayoutEffect&lt;/code&gt; and &lt;code&gt;useEffect&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useLayoutEffect&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;layout effect – DOM ready&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="nx"&gt;data&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;passive effect – after paint&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="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the console and watch the order. Layout effects will always appear before the passive ones for the same update.&lt;/p&gt;




&lt;h2&gt;
  
  
  Build it – instrument a component to visualise the three phases
&lt;/h2&gt;

&lt;p&gt;Below is a minimal reproducible example that logs every phase for a component that displays a counter. Follow the steps exactly; each command’s output is shown so you can confirm you’re on track.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create the component file&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; src/diagnostics &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;touch &lt;/span&gt;src/diagnostics/PhaseLogger.jsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add the code&lt;/strong&gt; (&lt;code&gt;src/diagnostics/PhaseLogger.jsx&lt;/code&gt;):
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;   &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useLayoutEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useEffect&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="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

   &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PhaseLogger&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`render &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;count&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="c1"&gt;// Render phase ends when the function returns&lt;/span&gt;
     &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeEnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`render &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;count&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="nf"&gt;useLayoutEffect&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;🔧 layout effect – count:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;count&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="nx"&gt;count&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;🌀 passive effect – count:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;count&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="nx"&gt;count&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="nt"&gt;div&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="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Count: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&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="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&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;setCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&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;+1&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&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="nt"&gt;div&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Why&lt;/em&gt;: &lt;code&gt;console.time&lt;/code&gt;/&lt;code&gt;console.timeEnd&lt;/code&gt; surround the component body, marking the render phase. The two effects log when they fire.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Render the component&lt;/strong&gt; – replace the default &lt;code&gt;App&lt;/code&gt; content with &lt;code&gt;PhaseLogger&lt;/code&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;   &lt;span class="c1"&gt;// src/App.js&lt;/span&gt;
   &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;PhaseLogger&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./diagnostics/PhaseLogger&lt;/span&gt;&lt;span class="dl"&gt;'&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;App&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="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"App"&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;PhaseLogger&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="nt"&gt;div&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run the dev server&lt;/strong&gt; (if not already running):
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Observe the console&lt;/strong&gt; – open Chrome DevTools, go to the Console tab, and click the “+1” button three times. You should see a repeating pattern:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   render 0: 0.12ms
   🔧 layout effect – count: 0
   🌀 passive effect – count: 0
   render 1: 0.09ms
   🔧 layout effect – count: 1
   🌀 passive effect – count: 1
   …
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Confirmation&lt;/em&gt;: The render log appears &lt;strong&gt;before&lt;/strong&gt; any effect logs, proving the order of phases.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add a heavy computation to the render&lt;/strong&gt; to see the impact:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;   &lt;span class="c1"&gt;// Inside PhaseLogger, before the return:&lt;/span&gt;
   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;heavy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e5&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&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;i&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&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;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;heavy sum&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;heavy&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reload the page and click the button. The render time will jump (e.g., &lt;code&gt;render 2: 12.4ms&lt;/code&gt;). This demonstrates how expensive work in the render phase directly slows the UI.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Move the heavy work to a layout effect&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;   &lt;span class="nf"&gt;useLayoutEffect&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;heavy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e5&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&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;i&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&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;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;layout heavy sum&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;heavy&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="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remove the computation from the render body. Now the render logs stay ~0.1 ms, while the layout effect logs the heavy sum. The UI feels snappier because the render phase is fast, but the layout effect blocks the paint – you’ll notice a brief freeze after each click.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Finally, move the work to a passive effect&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;heavy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e5&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&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;i&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&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;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;passive heavy sum&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;heavy&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="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the render is fast, the layout effect does nothing, and the heavy sum appears &lt;em&gt;after&lt;/em&gt; the paint. The UI feels responsive, but the heavy work still consumes CPU in the background.&lt;/p&gt;

&lt;p&gt;You have built a minimal instrumented component that demonstrates the three phases, their ordering, and the performance impact of doing work in each phase.&lt;/p&gt;




&lt;h2&gt;
  
  
  When this breaks
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom (what you see)&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;“Maximum update depth exceeded”&lt;/strong&gt; (stack overflow)&lt;/td&gt;
&lt;td&gt;A state update inside a &lt;code&gt;useEffect&lt;/code&gt; (or &lt;code&gt;useLayoutEffect&lt;/code&gt;) that depends on the same state, causing an infinite loop.&lt;/td&gt;
&lt;td&gt;Ensure the effect’s dependency array does not include the state you set, or guard the update with a condition. Example: &lt;code&gt;useEffect(() =&amp;gt; { if (ready) setState(...); }, [ready]);&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rows in a list lose their input values after a filter change&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Missing &lt;code&gt;key&lt;/code&gt; on list items, causing React to treat them as new mounts on each filter.&lt;/td&gt;
&lt;td&gt;Add a stable &lt;code&gt;key&lt;/code&gt; prop (e.g., &lt;code&gt;key={row.id}&lt;/code&gt;) to every element in the array map.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UI freezes for ~200 ms after a state change, even though the component is tiny&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Heavy computation performed during the render phase (or inside a &lt;code&gt;useLayoutEffect&lt;/code&gt;).&lt;/td&gt;
&lt;td&gt;Move the work to a &lt;code&gt;useEffect&lt;/code&gt; (passive) or, if it must be synchronous, memoise it with &lt;code&gt;useMemo&lt;/code&gt; and ensure the memoisation dependencies are correct.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;useLayoutEffect&lt;/code&gt; logs appear **after&lt;/strong&gt; the DOM is painted**&lt;/td&gt;
&lt;td&gt;Running the app in React 18’s concurrent mode with &lt;code&gt;StrictMode&lt;/code&gt; enabled, which intentionally runs layout effects twice in development.&lt;/td&gt;
&lt;td&gt;Remember that the double‑run only happens in dev; it does not affect production. If the double execution is problematic, temporarily disable &lt;code&gt;StrictMode&lt;/code&gt; for that subtree.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Profiler shows “mount” instead of “update” for a component that should only re‑render&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The component’s &lt;code&gt;key&lt;/code&gt; changed between renders, or its type changed (e.g., switching from &lt;code&gt;&amp;lt;ComponentA&amp;gt;&lt;/code&gt; to &lt;code&gt;&amp;lt;ComponentB&amp;gt;&lt;/code&gt;).&lt;/td&gt;
&lt;td&gt;Keep the component type and key stable; if you need to swap implementations, wrap them in a parent that preserves the key.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each of these failures can be reproduced by the instrumented &lt;code&gt;PhaseLogger&lt;/code&gt; component: add a &lt;code&gt;setState&lt;/code&gt; inside an effect that depends on the same state to trigger the infinite loop; remove &lt;code&gt;key&lt;/code&gt; from a list to see lost inputs; insert a heavy loop in the render body to cause a freeze.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Technique&lt;/th&gt;
&lt;th&gt;Trade‑off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Running heavy work in the render phase&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Guarantees the DOM reflects the latest data &lt;em&gt;before&lt;/em&gt; the paint, but each render blocks the main thread, leading to jank. Use only for cheap calculations.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Using &lt;code&gt;useLayoutEffect&lt;/code&gt; for synchronisation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Provides a hook that runs before the paint, useful for measuring layout or synchronising scroll positions. However, it blocks the paint and can cause noticeable pauses on low‑end devices.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Moving work to &lt;code&gt;useEffect&lt;/code&gt; (passive)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Keeps the UI responsive because the effect runs after the paint. The downside is that any DOM measurement must be deferred, and the UI may briefly show an intermediate state.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Relying on key‑based reconciliation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Greatly reduces unnecessary re‑mounts, but requires that every item in a list has a stable, unique identifier. Generating keys on the fly (e.g., using &lt;code&gt;index&lt;/code&gt;) defeats the optimisation and can cause state loss.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Batching updates (default in React 18)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reduces the number of renders, but can hide bugs where you expect an immediate DOM update after a &lt;code&gt;setState&lt;/code&gt;. If you need the update instantly, use &lt;code&gt;flushSync&lt;/code&gt; (cost: forces a synchronous render, defeating the batch).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Understanding these costs lets you decide where to place work in the lifecycle and when to sacrifice immediacy for smoother frames.&lt;/p&gt;




&lt;h2&gt;
  
  
  In the interview
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Typical interview prompt&lt;/strong&gt;  &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Walk me through what happens when you call &lt;code&gt;setState&lt;/code&gt; in a function component. Include the phases React goes through and why the order matters.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Weak answer&lt;/strong&gt; – “React re‑renders the component and updates the DOM.”&lt;br&gt;&lt;br&gt;
&lt;em&gt;Why it’s weak&lt;/em&gt;: It mentions only the render, omits reconciliation and commit, ignores effects, and gives no insight into performance implications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strong answer&lt;/strong&gt; –  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scheduling&lt;/strong&gt; – React creates an update object, pushes it onto the fiber’s queue, and asks the scheduler to perform work; updates are batched.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Render phase&lt;/strong&gt; – React calls the component function, builds a new virtual tree, and validates hook order; no side‑effects run.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconciliation&lt;/strong&gt; – The new tree is diffed against the previous one; keys guide matching, and three effect lists (Placement, Update, Deletion) are built.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit phase&lt;/strong&gt; – React first runs &lt;code&gt;getSnapshotBeforeUpdate&lt;/code&gt;/layout effects, then mutates the DOM according to the effect lists, then runs passive effects after the paint.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why order matters&lt;/strong&gt; – Doing heavy work in render or layout effect blocks the paint, causing jank; passive effects run asynchronously and cannot block the UI.
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Senior‑level expectation&lt;/strong&gt; – In addition to the above, discuss:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The role of the &lt;strong&gt;Fiber&lt;/strong&gt; data structure and how it enables interruption (concurrent mode).
&lt;/li&gt;
&lt;li&gt;How &lt;strong&gt;strict mode&lt;/strong&gt; in development double‑invokes render and layout effects, and why that matters for debugging.
&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;cost model&lt;/strong&gt;: O(N) traversal of the fiber tree, the impact of missing keys, and the memory overhead of keeping two trees simultaneously.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Director/Engineering‑manager angle&lt;/strong&gt; – The interviewer may ask: “How would you decide whether to optimise a component based on this model?” A good answer references measurable render times, the cost of extra DOM nodes, and the business impact (e.g., latency SLA).  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Follow‑up that separates theory from practice&lt;/strong&gt; – “Can you give an example of a bug you encountered that stemmed from misunderstanding the commit phase?” A candidate who describes an infinite render loop caused by a &lt;code&gt;useEffect&lt;/code&gt; that set state without a guard demonstrates real‑world experience.&lt;/p&gt;




&lt;h2&gt;
  
  
  Your tasks
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Log the three phases in an existing component&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;Add &lt;code&gt;console.time&lt;/code&gt;/&lt;code&gt;console.timeEnd&lt;/code&gt; around the component body, and &lt;code&gt;useLayoutEffect&lt;/code&gt;/&lt;code&gt;useEffect&lt;/code&gt; logs as shown in the “Build it” section.&lt;/em&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Done&lt;/strong&gt; when you can see the render log, layout effect log, and passive effect log in the correct order for each state change.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Identify a needless re‑mount&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
In a list component that renders &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; fields, remove the &lt;code&gt;key&lt;/code&gt; prop, type into an input, then trigger a state change that filters the list. Observe that the input loses its value.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Done&lt;/strong&gt; when you add a stable &lt;code&gt;key&lt;/code&gt; and the input retains its value across filters.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Create and fix an infinite render loop&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Add a &lt;code&gt;useEffect(() =&amp;gt; setCount(c =&amp;gt; c + 1), [count]);&lt;/code&gt; to a component. Observe the “Maximum update depth exceeded” error. Then fix it by adding a guard (&lt;code&gt;if (count &amp;lt; 5) setCount(...)&lt;/code&gt;).&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Done&lt;/strong&gt; when the component updates without crashing and the console no longer shows the error.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Measure the impact of moving work between phases&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Implement a 100 ms &lt;code&gt;for&lt;/code&gt;‑loop in the render body, then move it to a &lt;code&gt;useLayoutEffect&lt;/code&gt;, then to a &lt;code&gt;useEffect&lt;/code&gt;. Use the Chrome Performance tab to record each version and note the &lt;em&gt;CPU time&lt;/em&gt; and &lt;em&gt;frame rate&lt;/em&gt; differences.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Done&lt;/strong&gt; when you can state the measured frame‑time for each version and explain why the passive effect version is the most performant for UI responsiveness.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Optimise a list with missing keys&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Take a component that renders 500 rows, each with a button that toggles a local &lt;code&gt;selected&lt;/code&gt; state. Remove the keys, observe that toggling one row causes many rows to re‑mount. Add stable keys and verify that only the clicked row re‑renders (use the React Profiler to confirm).&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Done&lt;/strong&gt; when the Profiler shows a single “Update” for the clicked row and no unnecessary mounts.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;







&lt;h2&gt;
  
  
  Your tasks this week
&lt;/h2&gt;

&lt;p&gt;Do the exercises above before the next chapter. Reading a tutorial and doing&lt;br&gt;
one are different activities and only one of them changes what you can build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stuck on any of them?&lt;/strong&gt; Say so — describe what you tried and what happened:&lt;br&gt;
&lt;a href="https://www.amitchakraborty.dev/learn/react-deep-dive#stuck" rel="noopener noreferrer"&gt;tell me where you got stuck&lt;/a&gt;. I read every one, and the questions&lt;br&gt;
that come back more than twice get answered in the next chapter.&lt;/p&gt;

&lt;h2&gt;
  
  
  React, deeply
&lt;/h2&gt;

&lt;p&gt;Chapter 1 of 12. New chapter every Monday night.&lt;br&gt;
Next: &lt;strong&gt;State that scales: useState, useReducer and the point they stop working&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;· &lt;a href="https://www.amitchakraborty.dev/learn/react-deep-dive" rel="noopener noreferrer"&gt;The full syllabus and every chapter so far&lt;/a&gt;&lt;br&gt;
· Subscribers also get the condensed notes for this chapter, the running&lt;br&gt;
  recap of everything the series has covered, and the extended guidance:&lt;br&gt;
  &lt;a href="https://www.amitchakraborty.dev/#newsletter" rel="noopener noreferrer"&gt;subscribe&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by Amit Chakraborty — founding engineer and senior architect: React Native, AI and RAG systems, production architecture. &lt;a href="https://www.amitchakraborty.dev?utm_source=curriculum&amp;amp;utm_medium=content&amp;amp;utm_campaign=react-deep-dive" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt; · &lt;a href="https://linkedin.com/in/devamitch" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://github.com/devamitch" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Need this built, reviewed or taught to your team? &lt;a href="https://www.amitchakraborty.dev/#contact" rel="noopener noreferrer"&gt;Get in touch&lt;/a&gt; or email &lt;a href="mailto:amit@devamit.co.in"&gt;amit@devamit.co.in&lt;/a&gt;. Available for senior and founding engineering roles, consulting and training, remote worldwide.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>web</category>
      <category>react</category>
      <category>whatdoesastateupdatetrigger</category>
    </item>
    <item>
      <title>Reconciling Dual Crypto and Fiat Payments in a Mobile App</title>
      <dc:creator>Amit chakraborty</dc:creator>
      <pubDate>Mon, 19 Oct 2026 16:30:06 +0000</pubDate>
      <link>https://dev.to/techamit95ch/reconciling-dual-crypto-and-fiat-payments-in-a-mobile-app-2gib</link>
      <guid>https://dev.to/techamit95ch/reconciling-dual-crypto-and-fiat-payments-in-a-mobile-app-2gib</guid>
      <description>&lt;h2&gt;
  
  
  The situation
&lt;/h2&gt;

&lt;p&gt;Say you are a lead mobile engineer on a fintech startup that just launched a React Native app supporting both fiat purchases (via Stripe) and crypto purchases (via a custodial blockchain API). The product roadmap promised “one‑tap buying” for either asset class. After the first week of production, the support inbox filled with complaints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“My crypto purchase shows as &lt;em&gt;pending&lt;/em&gt; forever, but my card was charged.”&lt;/li&gt;
&lt;li&gt;“I was refunded twice for a fiat transaction.”&lt;/li&gt;
&lt;li&gt;“The dashboard shows a total balance that does not match the sum of my individual transactions.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The team spent three days investigating, discovered that the backend was creating duplicate transaction records, and that some on‑chain confirmations never arrived before the retry logic gave up. The cost was lost revenue (refunds), increased support load, and a delay in the next sprint while the bug‑fix branch was merged.&lt;/p&gt;

&lt;p&gt;The rest of this guide shows how to design a reliable reconciliation layer that survives production load, handles retries without double‑charging, and makes every state observable to the client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Asymmetric latency
&lt;/h3&gt;

&lt;p&gt;Fiat gateways are typically synchronous: a card‑present request returns a success or decline within seconds. Crypto APIs are asynchronous; a transaction is submitted to the mempool, then confirmed after an indeterminate number of blocks (often 1‑5 minutes). If the mobile client treats both responses as “completed” at the same point, the UI will display a success while the blockchain is still pending.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Lack of idempotency
&lt;/h3&gt;

&lt;p&gt;Both Stripe and most custodial crypto services accept an &lt;code&gt;idempotency_key&lt;/code&gt; header, but the key is often generated on the client. If the client retries after a network glitch and generates a new key, the backend creates a second charge. The database ends up with two rows that look identical except for the key.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Partial failure windows
&lt;/h3&gt;

&lt;p&gt;Consider the following sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Mobile app sends a fiat charge request → Stripe returns &lt;code&gt;succeeded&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The same request also triggers a crypto purchase request → the custodial API returns &lt;code&gt;queued&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The backend writes two rows: &lt;code&gt;fiat_status = succeeded&lt;/code&gt;, &lt;code&gt;crypto_status = pending&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The app crashes before receiving the webhook that updates &lt;code&gt;crypto_status&lt;/code&gt; to &lt;code&gt;confirmed&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now the system holds a &lt;em&gt;mixed&lt;/em&gt; state: the user has been debited, but the crypto asset is not credited. Without a periodic reconciliation job, this state persists indefinitely.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Race conditions in status updates
&lt;/h3&gt;

&lt;p&gt;Webhooks from Stripe and from the custodial provider may arrive out of order. If the fiat webhook marks the transaction &lt;code&gt;completed&lt;/code&gt; and the crypto webhook later marks it &lt;code&gt;failed&lt;/code&gt;, the final state may be incorrectly set to &lt;code&gt;failed&lt;/code&gt; unless the system tracks the &lt;em&gt;source&lt;/em&gt; of each status change.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Missing “unknown” bucket
&lt;/h3&gt;

&lt;p&gt;Most documentation lists three statuses: &lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;succeeded&lt;/code&gt;, &lt;code&gt;failed&lt;/code&gt;. Production reveals a fourth reality: &lt;em&gt;unknown&lt;/em&gt; – the system has not received any confirmation within the expected window (e.g., 15 minutes for crypto, 30 seconds for fiat). Treating “unknown” as “failed” forces premature refunds; treating it as “pending” leaves users staring at a stale UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, step by step
&lt;/h2&gt;

&lt;p&gt;Below is a production‑grade pattern that works with React Native 0.76, Node 18, PostgreSQL 15, Stripe 2023‑10‑16, and a custodial crypto API that follows the JSON‑RPC 2.0 spec.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 – Define a single source of truth for transaction state
&lt;/h3&gt;

&lt;p&gt;Create a table that stores &lt;strong&gt;one&lt;/strong&gt; logical transaction, with separate columns for each rail.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;user_transactions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;               &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;          &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;amount_cents&lt;/span&gt;     &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;currency&lt;/span&gt;         &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;               &lt;span class="c1"&gt;-- e.g. 'USD' or 'USDC'&lt;/span&gt;
    &lt;span class="n"&gt;fiat_status&lt;/span&gt;      &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'none'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="c1"&gt;-- none|pending|succeeded|failed|unknown&lt;/span&gt;
    &lt;span class="n"&gt;crypto_status&lt;/span&gt;    &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'none'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;fiat_intent_id&lt;/span&gt;   &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                        &lt;span class="c1"&gt;-- Stripe PaymentIntent ID&lt;/span&gt;
    &lt;span class="n"&gt;crypto_tx_hash&lt;/span&gt;   &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                        &lt;span class="c1"&gt;-- on‑chain transaction hash&lt;/span&gt;
    &lt;span class="n"&gt;idempotency_key&lt;/span&gt;  &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt;       &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="nb"&gt;TIME&lt;/span&gt; &lt;span class="k"&gt;ZONE&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;updated_at&lt;/span&gt;       &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="nb"&gt;TIME&lt;/span&gt; &lt;span class="k"&gt;ZONE&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&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;&lt;em&gt;Why&lt;/em&gt;: A single row guarantees that retries affect the same logical purchase, preventing duplicate charges. The &lt;code&gt;none&lt;/code&gt; status distinguishes “no request sent yet” from “pending”.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confirm&lt;/strong&gt;: Run &lt;code&gt;SELECT COUNT(*) FROM user_transactions;&lt;/code&gt; – it should return &lt;code&gt;0&lt;/code&gt; on a fresh DB.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 – Generate a stable idempotency key on the server
&lt;/h3&gt;

&lt;p&gt;Never let the client create the key. In the API endpoint that starts a purchase, generate a UUID and store it before any external call.&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="c1"&gt;// src/routes/purchase.js (Node/Express)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;v4&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;uuidv4&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="s1"&gt;uuid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../db.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRIPE_SECRET&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;submitCryptoTx&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="s1"&gt;../crypto.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/purchase&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amountCents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;paymentMethodId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;uuidv4&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;tx&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;one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`INSERT INTO user_transactions
       (user_id, amount_cents, currency, idempotency_key, fiat_status, crypto_status)
     VALUES ($1,$2,$3,$4,'pending','pending')
     RETURNING *`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amountCents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Fire‑and‑forget calls; errors are handled by retry workers.&lt;/span&gt;
  &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentIntents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;amountCents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;payment_method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;paymentMethodId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;confirm&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pi&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;none&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`UPDATE user_transactions SET fiat_intent_id=$1, fiat_status='succeeded'
       WHERE id=$2 AND fiat_status='pending'`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;pi&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="nx"&gt;tx&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="p"&gt;}).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;none&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`UPDATE user_transactions SET fiat_status='failed'
       WHERE id=$1 AND fiat_status='pending'`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tx&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="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;submitCryptoTx&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;amountCents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hash&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;none&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`UPDATE user_transactions SET crypto_tx_hash=$1, crypto_status='pending'
       WHERE id=$2 AND crypto_status='pending'`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tx&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="p"&gt;}).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;none&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`UPDATE user_transactions SET crypto_status='failed'
       WHERE id=$1 AND crypto_status='pending'`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tx&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="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;transactionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tx&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Why&lt;/em&gt;: The same &lt;code&gt;idempotency_key&lt;/code&gt; is reused for both Stripe and the crypto provider, guaranteeing that any retry hits the same logical operation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confirm&lt;/strong&gt;: After a successful POST, query the row:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;psql &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"SELECT fiat_status, crypto_status FROM user_transactions WHERE id='&amp;lt;returned-id&amp;gt;';"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both columns should read &lt;code&gt;pending&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3 – Persist webhook events with ordering metadata
&lt;/h3&gt;

&lt;p&gt;Both Stripe and the custodial provider send webhooks. Store each webhook payload in a separate &lt;code&gt;webhook_events&lt;/code&gt; table with a &lt;code&gt;received_at&lt;/code&gt; timestamp and a &lt;code&gt;source&lt;/code&gt; field (&lt;code&gt;stripe&lt;/code&gt;|&lt;code&gt;crypto&lt;/code&gt;). Then process them in order of &lt;code&gt;received_at&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;webhook_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;          &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;source&lt;/span&gt;      &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;     &lt;span class="n"&gt;JSONB&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;received_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="nb"&gt;TIME&lt;/span&gt; &lt;span class="k"&gt;ZONE&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;processed&lt;/span&gt;   &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;FALSE&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Processing worker (Node):&lt;/strong&gt;&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="k"&gt;import&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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./db.js&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;processEvents&lt;/span&gt;&lt;span class="p"&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;events&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;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`SELECT * FROM webhook_events
     WHERE processed = FALSE
     ORDER BY received_at ASC
     LIMIT 100`&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for &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;ev&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&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="nx"&gt;object&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;none&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;`UPDATE user_transactions
         SET fiat_status = CASE
               WHEN pi.status = 'succeeded' THEN 'succeeded'
               WHEN pi.status = 'requires_payment_method' THEN 'failed'
               ELSE 'unknown'
             END
         WHERE fiat_intent_id = $1`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;pi&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="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypto&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tx_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&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;none&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;`UPDATE user_transactions
         SET crypto_status = CASE
               WHEN status = 'confirmed' THEN 'succeeded'
               WHEN status = 'reverted'   THEN 'failed'
               ELSE 'unknown'
             END
         WHERE crypto_tx_hash = $1`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tx_hash&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="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;none&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`UPDATE webhook_events SET processed = TRUE WHERE id = $1`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ev&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Why&lt;/em&gt;: Ordering guarantees that a later “failed” event cannot overwrite an earlier “succeeded” event for the same rail. The &lt;code&gt;unknown&lt;/code&gt; bucket captures any status that does not map cleanly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confirm&lt;/strong&gt;: Insert a test webhook payload manually and run &lt;code&gt;processEvents()&lt;/code&gt;. Verify that the corresponding transaction row updates as expected.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4 – Add a periodic reconciliation job
&lt;/h3&gt;

&lt;p&gt;Even with webhooks, network outages can drop events. Run a nightly job that scans for rows where either status is still &lt;code&gt;pending&lt;/code&gt; after a timeout and queries the external API directly.&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;reconcile&lt;/span&gt;&lt;span class="p"&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;stale&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;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`SELECT * FROM user_transactions
     WHERE (fiat_status = 'pending' AND created_at &amp;lt; now() - interval '30 seconds')
        OR (crypto_status = 'pending' AND created_at &amp;lt; now() - interval '10 minutes')`&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for &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;tx&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;stale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Re‑query Stripe&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fiat_status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pi&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;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentIntents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fiat_intent_id&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;none&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;`UPDATE user_transactions SET fiat_status = $1 WHERE id = $2`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;pi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;succeeded&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="s1"&gt;succeeded&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="s1"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tx&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="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Re‑query crypto provider&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crypto_status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;queryCryptoStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crypto_tx_hash&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;none&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;`UPDATE user_transactions SET crypto_status = $1 WHERE id = $2`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tx&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="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;&lt;strong&gt;Timeout values&lt;/strong&gt;: 30 seconds for fiat (Stripe’s typical response window) and 10 minutes for crypto (average block confirmation on Ethereum mainnet). Adjust based on the chain you use; for Solana you might use 30 seconds, for Bitcoin 20 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confirm&lt;/strong&gt;: Run the job manually after creating a pending transaction and verify the status changes to &lt;code&gt;succeeded&lt;/code&gt; or &lt;code&gt;failed&lt;/code&gt; within the expected window.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5 – Implement exponential back‑off retries for external calls
&lt;/h3&gt;

&lt;p&gt;Network glitches are inevitable. Wrap each external request in a retry helper that respects the idempotency key.&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;retry&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;async-retry&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;safeStripeIntent&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;idempotencyKey&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="nf"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;bail&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;paymentIntents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;card_error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid_request_error&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="c1"&gt;// Do not retry on client errors&lt;/span&gt;
        &lt;span class="nf"&gt;bail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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;}&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// retry&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="na"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;minTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// 0.5 s&lt;/span&gt;
    &lt;span class="na"&gt;maxTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// 4 s&lt;/span&gt;
    &lt;span class="na"&gt;factor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&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;&lt;em&gt;Why&lt;/em&gt;: The &lt;code&gt;bail&lt;/code&gt; call prevents endless retries on unrecoverable errors (e.g., insufficient funds). The exponential back‑off caps total retry time at ~7 seconds per call, which is acceptable for a user‑initiated purchase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confirm&lt;/strong&gt;: Simulate a transient network error (e.g., by blocking outbound traffic for 2 seconds) and observe that the request eventually succeeds without creating duplicate rows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6 – Surface a combined status to the mobile client
&lt;/h3&gt;

&lt;p&gt;Expose an endpoint that returns a &lt;strong&gt;derived&lt;/strong&gt; status: &lt;code&gt;complete&lt;/code&gt; only when &lt;strong&gt;both&lt;/strong&gt; rails are &lt;code&gt;succeeded&lt;/code&gt;; &lt;code&gt;partial_success&lt;/code&gt; when one succeeded and the other is &lt;code&gt;failed&lt;/code&gt;; &lt;code&gt;in_progress&lt;/code&gt; when any rail is &lt;code&gt;pending&lt;/code&gt; or &lt;code&gt;unknown&lt;/code&gt;.&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;router&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="s1"&gt;/transaction/:id&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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;tx&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;oneOrNone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`SELECT fiat_status, crypto_status FROM user_transactions WHERE id=$1`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;req&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;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Not found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;overall&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fiat_status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;succeeded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crypto_status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;succeeded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;overall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;complete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fiat_status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crypto_status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;overall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fiat_status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;succeeded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crypto_status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;succeeded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;overall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;partial_success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="nx"&gt;overall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;in_progress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;overall&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fiat_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fiat_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;crypto_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crypto_status&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;&lt;strong&gt;Confirm&lt;/strong&gt;: Hit the endpoint with &lt;code&gt;curl&lt;/code&gt; after each stage and verify the JSON reflects the expected &lt;code&gt;overall&lt;/code&gt; value.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7 – Guard against double refunds
&lt;/h3&gt;

&lt;p&gt;When a transaction ends in &lt;code&gt;failed&lt;/code&gt; on &lt;strong&gt;either&lt;/strong&gt; rail, the refund logic must check the &lt;em&gt;other&lt;/em&gt; rail’s status before issuing a second refund.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Amit Chakraborty is a founding engineer and senior architect — React Native, AI/RAG systems and production architecture. Portfolio: &lt;a href="https://www.amitchakraborty.dev?utm_source=article&amp;amp;utm_medium=content&amp;amp;utm_campaign=dual-crypto-and-fiat-payment-rails-in-one-mobile-app" rel="noopener noreferrer"&gt;www.amitchakraborty.dev&lt;/a&gt; · &lt;a href="https://linkedin.com/in/devamitch" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://github.com/devamitch" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Open to senior and founding engineering roles, remote worldwide.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>fullstack</category>
      <category>idempotency</category>
      <category>reactnative</category>
    </item>
    <item>
      <title>Engineering Resilient Systems for a Magnitude 5.7 Earthquake in Pakistan</title>
      <dc:creator>Amit chakraborty</dc:creator>
      <pubDate>Mon, 19 Oct 2026 08:30:17 +0000</pubDate>
      <link>https://dev.to/techamit95ch/engineering-resilient-systems-for-a-magnitude-57-earthquake-in-pakistan-2inh</link>
      <guid>https://dev.to/techamit95ch/engineering-resilient-systems-for-a-magnitude-57-earthquake-in-pakistan-2inh</guid>
      <description>&lt;p&gt;&lt;em&gt;Observed 2026‑10‑17 – &lt;a href="https://earthquake.usgs.gov/earthquakes/eventpage/us6000u2y1" rel="noopener noreferrer"&gt;USGS event page&lt;/a&gt;&lt;/em&gt;  &lt;/p&gt;

&lt;p&gt;The sudden release of strain that generated a 5.7‑magnitude event in Pakistan highlights the need for engineered solutions that operate under extreme time pressure and with severely degraded communications. This article examines four technical domains that are repeatedly stressed in such scenarios: early‑warning latency budgets, offline‑first responder applications, store‑and‑forward mesh networking, and high‑throughput sensor data pipelines.&lt;/p&gt;




&lt;h2&gt;
  
  
  Early‑Warning Systems and the Seconds‑Matter Latency Budget
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Detection Chain and Latency Constraints
&lt;/h3&gt;

&lt;p&gt;An earthquake early‑warning (EEW) system must capture the initial &lt;strong&gt;P‑wave&lt;/strong&gt; (primary wave) and disseminate an alert before the damaging &lt;strong&gt;S‑wave&lt;/strong&gt; (secondary wave) arrives. In the Pakistani context, the distance between the epicenter and the most vulnerable urban centers can be on the order of 30–70 km, giving a theoretical warning window of 3–10 seconds. The latency budget therefore breaks down into distinct stages:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Typical Duration&lt;/th&gt;
&lt;th&gt;Engineering Implications&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sensor acquisition (accelerometer/strong‑motion)&lt;/td&gt;
&lt;td&gt;≤ 10 ms&lt;/td&gt;
&lt;td&gt;High‑rate sampling (≥ 200 Hz) and low‑noise front‑ends to guarantee reliable P‑wave detection.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On‑site trigger algorithm&lt;/td&gt;
&lt;td&gt;20–50 ms&lt;/td&gt;
&lt;td&gt;Deterministic, low‑complexity classifiers (e.g., STA/LTA) running on embedded microcontrollers; deterministic execution avoids jitter.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edge aggregation &amp;amp; validation&lt;/td&gt;
&lt;td&gt;30–100 ms&lt;/td&gt;
&lt;td&gt;Redundant edge nodes cross‑validate triggers to suppress false alarms; requires ultra‑low‑latency inter‑node messaging (e.g., IEEE 802.15.4e TSCH).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alert generation &amp;amp; encoding&lt;/td&gt;
&lt;td&gt;≤ 20 ms&lt;/td&gt;
&lt;td&gt;Compact binary payloads (≤ 64 bytes) to minimize transmission time; include timestamp, magnitude estimate, and location.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distribution (cellular, broadcast, satellite)&lt;/td&gt;
&lt;td&gt;100 ms–1 s&lt;/td&gt;
&lt;td&gt;Multi‑path broadcast (cellular SMS, LTE‑M, satellite beacons) with priority queuing; network‑level QoS tagging is mandatory.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;End‑user device processing&lt;/td&gt;
&lt;td&gt;≤ 50 ms&lt;/td&gt;
&lt;td&gt;Pre‑loaded EEW clients that parse binary alerts, compute local arrival times, and trigger UI/actuator responses.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Summing the upper bounds yields a worst‑case latency of roughly &lt;strong&gt;1.3 seconds&lt;/strong&gt;, comfortably within the available warning window for most urban receivers. However, any single stage that exceeds its budget can erode the entire margin, making deterministic design and rigorous profiling essential.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trade‑offs in Algorithmic Design
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sensitivity vs. false‑positive rate&lt;/strong&gt; – Lowering detection thresholds improves early detection but increases spurious alerts, which erodes public trust. A hierarchical approach (local trigger → regional consensus) mitigates this risk.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Computation vs. power&lt;/strong&gt; – High‑frequency sampling and complex signal processing increase power draw on battery‑powered stations. Ultra‑low‑power DSPs (e.g., ARM Cortex‑M4 with hardware multiply‑accumulate) enable real‑time processing while preserving multi‑year autonomy.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Centralized vs. decentralized validation&lt;/strong&gt; – Central servers can apply sophisticated Bayesian filters but introduce network latency. Decentralized peer validation keeps latency low but requires robust consensus protocols (e.g., Raft with sub‑second election timeouts).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Failure Modes and Mitigations
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure Mode&lt;/th&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Mitigation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sensor saturation (clipping)&lt;/td&gt;
&lt;td&gt;No P‑wave detection&lt;/td&gt;
&lt;td&gt;Deploy dual‑range accelerometers; implement automatic gain control.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Communication blackout (cellular overload)&lt;/td&gt;
&lt;td&gt;Alert not delivered&lt;/td&gt;
&lt;td&gt;Multi‑modal broadcast (cellular + satellite + FM radio); pre‑load fallback tones on devices.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clock drift across stations&lt;/td&gt;
&lt;td&gt;Inconsistent timestamps&lt;/td&gt;
&lt;td&gt;Use GPS disciplined oscillators; periodic NTP sync over low‑bandwidth links.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Software exception in trigger code&lt;/td&gt;
&lt;td&gt;Missed detection&lt;/td&gt;
&lt;td&gt;Watchdog timers; fail‑safe default to “no alert” with post‑event audit.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Designing EEW systems thus revolves around guaranteeing that each component respects its latency envelope, while providing redundancy that tolerates the inevitable failures of a seismic event.&lt;/p&gt;




&lt;h2&gt;
  
  
  Offline‑First Applications for Responders with No Network
&lt;/h2&gt;

&lt;p&gt;When the earthquake damages telecom infrastructure, first‑responders must rely on applications that continue to function without connectivity. An &lt;strong&gt;offline‑first&lt;/strong&gt; architecture treats the local device as the source of truth, synchronizing with the backend only when a link becomes available.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data Model and Local Persistence
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Immutable event logs&lt;/strong&gt; – Each field observation (e.g., damage classification, resource request) is stored as an append‑only record with a UUID, timestamp, and cryptographic hash. Immutability simplifies conflict resolution during later merges.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versioned entities&lt;/strong&gt; – For mutable objects (e.g., status of a relief hub), maintain a version vector (Lamport timestamps) to detect concurrent updates.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embedded databases&lt;/strong&gt; – Use lightweight, ACID‑compliant stores such as SQLite or Realm that support on‑device indexing and full‑text search, enabling rapid query even on low‑end Android devices.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Synchronization Protocol
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;bi‑directional sync engine&lt;/strong&gt; must reconcile divergent histories while preserving causality:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Change export&lt;/strong&gt; – The client bundles unsent logs into a compact binary envelope (e.g., Protocol Buffers) and signs it with a device‑specific key.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transport abstraction&lt;/strong&gt; – The envelope can be sent over any available channel: Wi‑Fi hotspot, Bluetooth LE, or a temporary satellite link.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server‑side merge&lt;/strong&gt; – The backend applies a deterministic merge function: sort by timestamp, resolve conflicts using version vectors, and reject duplicates via hash comparison.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Acknowledgment&lt;/strong&gt; – The server returns a receipt containing the highest applied version per entity, allowing the client to prune confirmed entries.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The protocol must be tolerant of out‑of‑order delivery and partial payload loss, which is common on opportunistic links. Implementing &lt;strong&gt;forward error correction&lt;/strong&gt; (e.g., Reed‑Solomon) on the envelope mitigates the need for retransmission.&lt;/p&gt;

&lt;h3&gt;
  
  
  UI/UX Considerations Under Offline Constraints
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Graceful degradation&lt;/strong&gt; – Hide features that require remote data (e.g., live maps) and replace them with locally cached raster tiles or vector overlays pre‑loaded before the event.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit connectivity status&lt;/strong&gt; – Show a persistent banner indicating “offline – data will sync when connection restores.” This reduces user anxiety and prevents accidental data loss.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Battery awareness&lt;/strong&gt; – Defer non‑essential background sync until the device is charging or a high‑capacity power source is detected; expose a “low‑power mode” toggle.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Security and Data Integrity
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;End‑to‑end encryption&lt;/strong&gt; – Encrypt each log entry with a symmetric key derived from the responder’s credential; rotate keys daily to limit exposure.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tamper‑evident logs&lt;/strong&gt; – Chain hashes of consecutive entries (hash‑linked list) so any post‑event alteration is detectable during audit.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access control&lt;/strong&gt; – Enforce role‑based permissions locally; e.g., a field medic cannot modify logistics entries, and the UI must enforce this even when offline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Offline‑first design thus ensures that responders retain full situational awareness and can continue to record critical observations, regardless of the state of the communications fabric.&lt;/p&gt;




&lt;h2&gt;
  
  
  Store‑and‑Forward and Mesh Networking When Infrastructure Is Down
&lt;/h2&gt;

&lt;p&gt;When conventional backhaul fails, a &lt;strong&gt;store‑and‑forward (S&amp;amp;F) mesh&lt;/strong&gt; provides a resilient data plane that can ferry messages across fragmented nodes until a gateway to the internet re‑appears.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mesh Topology and Routing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid mesh&lt;/strong&gt; – Combine &lt;strong&gt;static backbone nodes&lt;/strong&gt; (e.g., pre‑installed high‑gain radios on municipal buildings) with &lt;strong&gt;mobile ad‑hoc nodes&lt;/strong&gt; (e.g., responder handhelds, UAV‑mounted radios).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Routing protocol&lt;/strong&gt; – Use a delay‑tolerant variant of &lt;strong&gt;B.A.T.M.A.N. (Better Approach To Mobile Adhoc Networking)&lt;/strong&gt; that incorporates &lt;strong&gt;store‑and‑forward queues&lt;/strong&gt; and &lt;strong&gt;opportunistic forwarding&lt;/strong&gt;. Nodes maintain a neighbor table with link quality metrics (RSSI, packet loss) and forward packets to the neighbor with the highest delivery probability.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Message TTL and priority&lt;/strong&gt; – Assign a &lt;strong&gt;time‑to‑live (TTL)&lt;/strong&gt; field based on the message class (e.g., “life‑threatening” gets 30 seconds, “log‑entry” gets 24 hours). Priority queues ensure that high‑urgency packets are transmitted first when bandwidth is scarce.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Store‑and‑Forward Mechanics
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ingress&lt;/strong&gt; – An originating device creates a packet, tags it with a unique identifier, priority, and TTL, then places it in the local outbound queue.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Buffer management&lt;/strong&gt; – Each node allocates a persistent buffer (e.g., flash‑based circular log) sized to hold at least 48 hours of high‑priority traffic. The buffer is organized by priority to prevent low‑importance data from starving critical messages.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forwarding decision&lt;/strong&gt; – Upon detecting a neighbor, the node evaluates the neighbor’s &lt;strong&gt;delivery estimate&lt;/strong&gt; (based on historical contact frequency) and forwards packets that improve the&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;This piece is about the engineering, not the event. For what actually happened and for anything anyone affected needs to act on, go to the source: &lt;a href="https://earthquake.usgs.gov/earthquakes/eventpage/us6000u2y1" rel="noopener noreferrer"&gt;USGS&lt;/a&gt;. Emergency guidance comes from your local emergency service and from nobody else.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by Amit Chakraborty — founding engineer and senior architect: React Native, AI and RAG systems, production architecture. &lt;a href="https://www.amitchakraborty.dev?utm_source=signal&amp;amp;utm_medium=content&amp;amp;utm_campaign=engineering-response-seismic-magnitude-57-earthquake-pakistan" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt; · &lt;a href="https://linkedin.com/in/devamitch" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://github.com/devamitch" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Building systems that have to work when things are going wrong is most of what this is — &lt;a href="https://www.amitchakraborty.dev/#contact" rel="noopener noreferrer"&gt;get in touch&lt;/a&gt; or email &lt;a href="mailto:amit@devamit.co.in"&gt;amit@devamit.co.in&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>engineeringresponse</category>
      <category>earlywarningsystems</category>
      <category>systems</category>
      <category>reliability</category>
    </item>
    <item>
      <title>Startup‑time regression after enabling Hermes in a React‑Native app</title>
      <dc:creator>Amit chakraborty</dc:creator>
      <pubDate>Mon, 19 Oct 2026 08:30:08 +0000</pubDate>
      <link>https://dev.to/techamit95ch/startup-time-regression-after-enabling-hermes-in-a-react-native-app-2df0</link>
      <guid>https://dev.to/techamit95ch/startup-time-regression-after-enabling-hermes-in-a-react-native-app-2df0</guid>
      <description>&lt;h2&gt;
  
  
  The situation
&lt;/h2&gt;

&lt;p&gt;Say you are a lead mobile engineer on a two‑month sprint for a consumer‑facing React‑Native app (React‑Native 0.73). The product team has pushed a deadline to ship version 2.1, and the QA team reports that the app now takes &lt;strong&gt;≈ 1.8 s&lt;/strong&gt; to reach the first screen on a mid‑range Android 11 device (Pixel 5). The same build, but with the default JavaScript engine (JSC) enabled, consistently hits &lt;strong&gt;≈ 1.2 s&lt;/strong&gt; on the same device. &lt;/p&gt;

&lt;p&gt;The regression was discovered after a tutorial‑style pull request that added the following to &lt;code&gt;android/app/build.gradle&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gradle"&gt;&lt;code&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;react&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
    &lt;span class="nl"&gt;enableHermes:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;   &lt;span class="c1"&gt;// &amp;lt;‑‑ added per tutorial&lt;/span&gt;
&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The change was merged without measuring the impact. The cost is three‑fold:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;User experience&lt;/strong&gt; – a 0.6 s extra delay on the splash screen, which translates to a measurable drop in conversion (internal telemetry shows a 3 % lower “continue” rate after the delay). &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release schedule&lt;/strong&gt; – the team now needs to allocate a day of debugging and a possible rollback, jeopardising the sprint goal. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team morale&lt;/strong&gt; – the incident triggers a “why‑did‑we‑do‑that?” post‑mortem, consuming meeting time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Below is a practical, production‑tested path to understand why Hermes can hurt startup time in some scenarios, how to verify the cause, and how to mitigate it without sacrificing the long‑term benefits Hermes provides.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Bytecode loading overhead
&lt;/h3&gt;

&lt;p&gt;Hermes ships as a native library (&lt;code&gt;libhermes.so&lt;/code&gt;) that must be &lt;strong&gt;loaded and initialised&lt;/strong&gt; before the first JavaScript bytecode can be executed. On devices with limited I/O bandwidth, the extra ~ 2 MB of native code adds a measurable &lt;strong&gt;disk‑read latency&lt;/strong&gt;. In the RN 0.73 benchmark suite, the engine load adds &lt;strong&gt;≈ 120 ms&lt;/strong&gt; on a Snapdragon 765G, while the JSC interpreter is already resident in the system image and incurs virtually no load cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. AOT compilation vs. JIT warm‑up
&lt;/h3&gt;

&lt;p&gt;Hermes performs &lt;strong&gt;ahead‑of‑time (AOT) bytecode generation&lt;/strong&gt; at build time. This eliminates JIT compilation at runtime, which is why many apps see a &lt;strong&gt;30 % reduction&lt;/strong&gt; in CPU usage after the first frame. However, the AOT step produces a &lt;strong&gt;single monolithic bytecode file&lt;/strong&gt; (&lt;code&gt;index.android.bundle&lt;/code&gt;). The OS must map the whole file into memory before execution, which can be slower than JSC’s incremental parsing on low‑end devices that benefit from lazy loading of script chunks.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Incompatible third‑party modules
&lt;/h3&gt;

&lt;p&gt;Some native modules (e.g., &lt;code&gt;react-native-reanimated&lt;/code&gt; &amp;lt; 2.12, &lt;code&gt;react-native-gesture-handler&lt;/code&gt; &amp;lt; 2.4) rely on &lt;strong&gt;&lt;code&gt;eval&lt;/code&gt;&lt;/strong&gt; or &lt;strong&gt;dynamic &lt;code&gt;import()&lt;/code&gt;&lt;/strong&gt;. Hermes disables &lt;code&gt;eval&lt;/code&gt; by default for security and performance. When such calls are encountered, Hermes throws:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HermesException: Unexpected eval or new Function usage.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the code falls back to a JavaScript‑only path, it may trigger &lt;strong&gt;additional bundle parsing&lt;/strong&gt; that negates the bytecode advantage, inflating startup time.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Debugger attachment delay
&lt;/h3&gt;

&lt;p&gt;When Hermes is enabled, &lt;strong&gt;remote JS debugging&lt;/strong&gt; via Chrome DevTools is disabled. Developers often attach the &lt;strong&gt;Hermes Debugger&lt;/strong&gt; (&lt;code&gt;react-native-debugger&lt;/code&gt; with Hermes support) which establishes a &lt;strong&gt;WebSocket&lt;/strong&gt; connection and loads the &lt;strong&gt;Hermes inspector&lt;/strong&gt;. On a cold start, the app waits for this handshake (≈ 80 ms) before proceeding, whereas JSC’s Chrome‑based debugging can be skipped entirely in production builds.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Build‑time cost
&lt;/h3&gt;

&lt;p&gt;Enabling Hermes adds a &lt;strong&gt;bytecode generation step&lt;/strong&gt; (&lt;code&gt;hermesc&lt;/code&gt;) to the Gradle pipeline. On a CI runner with limited CPU, this can increase the Android build time by &lt;strong&gt;≈ 30 %&lt;/strong&gt; (from 8 min to 10 min). Longer builds reduce the frequency of releases and increase the risk of stale dependencies slipping into production.&lt;/p&gt;




&lt;h2&gt;
  
  
  The fix, step by step
&lt;/h2&gt;

&lt;p&gt;Below is a reproducible workflow that isolates the startup‑time impact, validates the trade‑offs, and lets you decide whether to keep Hermes enabled for a given release.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 – Reproduce the regression locally
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check out the exact commit&lt;/strong&gt; that introduced &lt;code&gt;enableHermes: true&lt;/code&gt;. &lt;/li&gt;
&lt;li&gt;Run a clean build on a physical device that matches the failing spec (Pixel 5, Android 11, API 30):
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;cd &lt;/span&gt;android
   ./gradlew clean assembleRelease
   adb &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; app/build/outputs/apk/release/app-release.apk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Measure startup time with &lt;strong&gt;&lt;code&gt;adb logcat&lt;/code&gt;&lt;/strong&gt; filtering for the RN splash event:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   adb logcat &lt;span class="nt"&gt;-s&lt;/span&gt; ReactNativeJS:V | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"JS load time"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output (Hermes):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   ReactNativeJS: JS load time: 1760 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Record the value; you now have a baseline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 – Switch back to JSC for comparison
&lt;/h3&gt;

&lt;p&gt;Edit &lt;code&gt;android/app/build.gradle&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gradle"&gt;&lt;code&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;react&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
    &lt;span class="nl"&gt;enableHermes:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;   &lt;span class="c1"&gt;// revert temporarily&lt;/span&gt;
&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Re‑build and re‑measure using the same commands. You should see a lower time (≈ 1200 ms). This confirms that the regression is tied to the engine switch, not to unrelated code changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3 – Profile engine load time
&lt;/h3&gt;

&lt;p&gt;Hermes provides a &lt;strong&gt;&lt;code&gt;hermes-profile&lt;/code&gt;&lt;/strong&gt; binary that can be embedded in the app to log engine initialisation. Add the following to &lt;code&gt;MainApplication.java&lt;/code&gt; (after &lt;code&gt;ReactInstanceManager&lt;/code&gt; creation, only in a debug build):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BuildConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DEBUG&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nc"&gt;BuildConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ENABLE_HERMES&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;facebook&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hermes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reactexecutor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;HermesRuntimeFactory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;enableRuntimeProfiling&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the app and capture the log entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HermesRuntime: Engine init time: 115 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the init time exceeds &lt;strong&gt;≈ 150 ms&lt;/strong&gt;, you are hitting the disk‑read overhead on the device. Consider &lt;strong&gt;compressing the native library&lt;/strong&gt; with &lt;code&gt;android.packagingOptions&lt;/code&gt; to reduce load time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4 – Verify bytecode size and mapping
&lt;/h3&gt;

&lt;p&gt;Inspect the generated bundle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;android/app/build/generated/assets/react/
&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; index.android.bundle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Typical Hermes bundle size: &lt;strong&gt;≈ 5 MB&lt;/strong&gt; (vs. ≈ 6 MB for JSC). However, the &lt;strong&gt;memory‑mapped size&lt;/strong&gt; can be larger because the OS rounds up to page boundaries. Use &lt;code&gt;adb shell cat /proc/&amp;lt;pid&amp;gt;/maps | grep index.android.bundle&lt;/code&gt; to see the mapped region. If the mapped size is &amp;gt; 8 MB, you may be exceeding the device’s &lt;strong&gt;low‑memory threshold&lt;/strong&gt;, causing the OS to trigger a &lt;strong&gt;GC pause&lt;/strong&gt; that adds ≈ 200 ms to startup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation:&lt;/strong&gt; Split the bundle using &lt;strong&gt;RAM bundles&lt;/strong&gt; (&lt;code&gt;enableHermes: true, bundleInDebug: true, hermesFlags: ["-O", "--emit-binary"]&lt;/code&gt;). This creates a &lt;strong&gt;&lt;code&gt;index.android.bundle&lt;/code&gt;&lt;/strong&gt; with a table of small bytecode chunks, allowing the OS to map only what is needed for the first screen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5 – Detect incompatible modules
&lt;/h3&gt;

&lt;p&gt;Run the app with &lt;strong&gt;Hermes in debug mode&lt;/strong&gt; and watch for the &lt;code&gt;HermesException&lt;/code&gt; message. If you see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HermesException: Unexpected eval or new Function usage.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Identify the offending module by checking the stack trace. Common culprits are older versions of &lt;code&gt;react-native-reanimated&lt;/code&gt;. Upgrade to &lt;strong&gt;≥ 2.13&lt;/strong&gt; which ships a Hermes‑compatible implementation, or replace the dynamic code path with a static import.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6 – Benchmark with Hermes debugger disabled
&lt;/h3&gt;

&lt;p&gt;In production, the Hermes debugger is never attached. To simulate this, run a &lt;strong&gt;release build&lt;/strong&gt; with the &lt;code&gt;hermes-debugger&lt;/code&gt; flag set to &lt;code&gt;false&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gradle"&gt;&lt;code&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;react&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
    &lt;span class="nl"&gt;enableHermes:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;hermesDebugEnabled:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Re‑measure startup. If the time drops by &lt;strong&gt;≈ 80 ms&lt;/strong&gt;, the delay you observed in step 1 was due to the debugger handshake. Ensure that your CI pipeline builds a &lt;strong&gt;release variant&lt;/strong&gt; for production, not a debug variant that inadvertently includes the debugger.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7 – Evaluate the trade‑off numerically
&lt;/h3&gt;

&lt;p&gt;Collect the following metrics on the target device:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;JSC (baseline)&lt;/th&gt;
&lt;th&gt;Hermes (current)&lt;/th&gt;
&lt;th&gt;Difference&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Startup time (ms)&lt;/td&gt;
&lt;td&gt;1200&lt;/td&gt;
&lt;td&gt;1760&lt;/td&gt;
&lt;td&gt;+560&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engine load time (ms)&lt;/td&gt;
&lt;td&gt;0 (system)&lt;/td&gt;
&lt;td&gt;115&lt;/td&gt;
&lt;td&gt;+115&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundle size on disk (MB)&lt;/td&gt;
&lt;td&gt;6.0&lt;/td&gt;
&lt;td&gt;5.0&lt;/td&gt;
&lt;td&gt;-1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;APK size increase (MB)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;+2.5 (libhermes)&lt;/td&gt;
&lt;td&gt;+2.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI build time (min)&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;+2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If the &lt;strong&gt;+560 ms&lt;/strong&gt; startup penalty outweighs the &lt;strong&gt;‑1 MB&lt;/strong&gt; bundle reduction and the &lt;strong&gt;+2.5 MB&lt;/strong&gt; APK growth, you may decide to ship with JSC for this release, or adopt the RAM‑bundle optimisation in step 4.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 8 – Apply the chosen mitigation
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;If you stay with Hermes&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gradle"&gt;&lt;code&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;react&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
    &lt;span class="nl"&gt;enableHermes:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;hermesFlags:&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-O"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"--emit-binary"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"--compact"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;android&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;packagingOptions&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Compress native lib to reduce load time&lt;/span&gt;
        &lt;span class="n"&gt;doNotStrip&lt;/span&gt; &lt;span class="s2"&gt;"**/libhermes.so"&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;If you revert to JSC&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gradle"&gt;&lt;code&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;react&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
    &lt;span class="nl"&gt;enableHermes:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commit the change, run a full CI build, and verify that the &lt;strong&gt;release APK size&lt;/strong&gt; and &lt;strong&gt;startup time&lt;/strong&gt; meet the SLA (≤ 1.3 s on the target device).&lt;/p&gt;




&lt;h2&gt;
  
  
  What it costs, and when not to do it
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cost category&lt;/th&gt;
&lt;th&gt;Hermes advantage&lt;/th&gt;
&lt;th&gt;Hermes drawback (when it hurts)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Startup latency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AOT bytecode eliminates JIT warm‑up → faster after first frame.&lt;/td&gt;
&lt;td&gt;Engine load + full bundle mapping can add 300‑600 ms on low‑end Android.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;APK size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No JSC runtime → smaller JavaScript payload.&lt;/td&gt;
&lt;td&gt;Adds ~2‑3 MB native library (&lt;code&gt;libhermes.so&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Build time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bytecode generation enables source‑map debugging.&lt;/td&gt;
&lt;td&gt;CI builds 20‑30 % longer; may hit timeouts on constrained runners.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Debugging&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hermes debugger provides precise source‑level breakpoints.&lt;/td&gt;
&lt;td&gt;Remote Chrome debugging disabled; extra handshake adds ~80 ms.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compatibility&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Works out‑of‑the‑box with most RN core modules.&lt;/td&gt;
&lt;td&gt;Modules using &lt;code&gt;eval&lt;/code&gt;, dynamic &lt;code&gt;import()&lt;/code&gt;, or old native bridges may crash or degrade performance.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;When not to enable Hermes&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Targeting a large share of low‑end Android devices&lt;/strong&gt; (≤ 2 GB RAM, API 21‑23) where the extra native library load dominates the startup budget. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI pipelines constrained to &amp;lt; 10 min builds&lt;/strong&gt; and lacking parallelism; the bytecode step will become a bottleneck. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heavy reliance on third‑party libraries&lt;/strong&gt; that have not declared Hermes compatibility (check each library’s &lt;code&gt;peerDependencies&lt;/code&gt;). &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strict APK size limits&lt;/strong&gt; (e.g., for OTA updates where payload must stay &amp;lt; 5 MB). &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In those cases, either stay with JSC or adopt a &lt;strong&gt;hybrid approach&lt;/strong&gt;: ship JSC for Android &amp;lt; 8 and Hermes for newer devices using &lt;strong&gt;product flavours&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  At your level
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Immediate takeaway&lt;/th&gt;
&lt;th&gt;One concrete next step&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Student / first‑year engineer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hermes can &lt;em&gt;slow&lt;/em&gt; startup on some devices despite a smaller bundle.&lt;/td&gt;
&lt;td&gt;Run the &lt;code&gt;adb logcat&lt;/code&gt; measurement on a real device after toggling &lt;code&gt;enableHermes&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mid‑level engineer (feature owner)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Quantify the trade‑off (startup ms vs. APK size vs. build time) before merging the engine switch.&lt;/td&gt;
&lt;td&gt;Add a CI step that records &lt;code&gt;hermesc&lt;/code&gt; duration and fails if it exceeds a threshold (e.g., 2 min).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Senior / staff engineer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Architecture must include a decision matrix for JavaScript engines per device tier.&lt;/td&gt;
&lt;td&gt;Implement a Gradle product flavour (&lt;code&gt;hermesRelease&lt;/code&gt; / &lt;code&gt;jscRelease&lt;/code&gt;) and automate device‑tier detection at install time.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lead / director&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Enabling Hermes without measurement can jeopardise release schedules and user metrics.&lt;/td&gt;
&lt;td&gt;Allocate a sprint budget for “engine‑performance validation” and track the KPI (startup ≤ 1.3 s) in the release dashboard.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  In the interview
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Question (as asked by a senior engineering manager):&lt;/strong&gt; &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“We recently enabled Hermes in our React‑Native Android builds to reduce bundle size, but we observed a 0.5 s increase in cold‑start time on low‑end devices. Walk me through why that can happen, how you would diagnose it, and what trade‑offs you would consider before deciding whether to keep Hermes enabled.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Weak answer (why it’s weak):&lt;/strong&gt; &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Hermes is faster because it pre‑compiles JavaScript. Maybe the device is slow, so we should just keep JSC.” &lt;br&gt;
&lt;em&gt;Fails to name the concrete cause, the diagnostic steps, and the quantitative trade‑off.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Strong answer (what it must contain):&lt;/strong&gt; &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Identify the failure mode&lt;/strong&gt; – engine load and full‑bundle mapping add I/O latency; incompatible modules trigger fallback parsing; debugger handshake adds delay. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diagnostic plan&lt;/strong&gt; – reproduce on a representative device, measure startup with&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Amit Chakraborty is a founding engineer and senior architect — React Native, AI/RAG systems and production architecture. Portfolio: &lt;a href="https://www.amitchakraborty.dev?utm_source=article&amp;amp;utm_medium=content&amp;amp;utm_campaign=hermes-and-startup-time-the-trade-offs-nobody-documents" rel="noopener noreferrer"&gt;www.amitchakraborty.dev&lt;/a&gt; · &lt;a href="https://linkedin.com/in/devamitch" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://github.com/devamitch" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Open to senior and founding engineering roles, remote worldwide.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>hermes</category>
      <category>android</category>
      <category>react</category>
    </item>
  </channel>
</rss>
