{"id":"l-rn-5-1","courseId":"react-native-apps","lang":"en","kind":"lesson","summary":"Find and fix Trailhead’s bugs and slow screens with LogBox, React Native DevTools, the Performance Monitor and the Profiler, measuring in production mode before changing code.","html":"<p>Something is wrong with Trailhead: the log scrolls in jerks, a button sometimes does nothing, and once a week a user’s app closes on its own. Debugging on mobile follows the same habits as on the web: reproduce, observe, form a hypothesis, measure. The tools are different, and one of them, the release build, matters more than people expect.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">The tools you have</h2>\n<p><strong>LogBox</strong> is the in-app overlay. A red full-screen error is an uncaught JavaScript exception, with a stack trace that points into your source. Warnings show as a small banner at the bottom. Fix warnings as they appear; a good team habit is to treat a new warning like a failing test, because the ones you ignore are the ones that turn into crashes.</p>\n<p><strong><code dir=\"ltr\">console.log</code></strong> output appears in the terminal running Metro and in the DevTools console. Fine for quick checks; remove them from hot paths like <code dir=\"ltr\">renderItem</code>, where logging every row slows the list itself.</p>\n<p><strong>React Native DevTools</strong> is the built-in debugger, based on Chrome DevTools. Press <code dir=\"ltr\">j</code> in the Metro terminal, or choose Open DevTools from the dev menu (<code dir=\"ltr\">m</code> in the terminal, or shake the device). It has:</p>\n<ul>\n<li>a <strong>Console</strong> connected to your running app,</li>\n<li><strong>Sources</strong> with breakpoints, stepping and watch expressions (<code dir=\"ltr\">Cmd+P</code> / <code dir=\"ltr\">Ctrl+P</code> to find a file),</li>\n<li>the <strong>Components</strong> inspector, to see the component tree, props, state and hooks,</li>\n<li>the <strong>Profiler</strong>, to record renders and see what rendered and why.</li>\n</ul>\n<p>A breakpoint beats ten <code dir=\"ltr\">console.log</code> calls when you don’t know where the bug is. Set one in the handler of the button that “sometimes does nothing”, tap it, and inspect the state you actually have.</p>\n<p><strong>Native crashes</strong>, where the app closes without a red screen, come from native code (a library, a misconfigured permission, running out of memory). The JavaScript tools can’t see them. Read the device logs: Xcode’s console for iOS, Logcat in Android Studio, or the terminal output of <code dir=\"ltr\">npx expo run:android</code>. In production, an error-reporting service such as Sentry collects both JavaScript and native crashes from real users, with stack traces.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Measure in production mode</h2>\n<p>Development mode runs extra checks: prop validation, warnings, the dev menu, Fast Refresh. It can make JavaScript several times slower. Judging performance there is like timing a runner wearing a backpack. Before you change any code for speed, run without dev mode:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">npx</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> expo</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> start</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --no-dev</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --minify</span></span></code></pre></div>\n<p>or install a release build (you’ll make one with EAS later in this section). Test on a mid-range Android phone; if it’s smooth there, it’s smooth almost everywhere.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">JS thread or UI thread?</h2>\n<p>Open the dev menu and turn on the <strong>Performance Monitor</strong>. It shows two frame rates, and they map to the two threads from the very first lesson:</p>\n<div class=\"table-wrap\" role=\"region\" aria-label=\"Scrollable table\" tabindex=\"0\"><table><thead>\n<tr>\n<th>Symptom</th>\n<th>Likely cause</th>\n<th>Where to look</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>JS FPS drops, UI FPS fine</td>\n<td>too much JavaScript: re-renders, heavy work in render, big JSON parsing</td>\n<td>Profiler, your components</td>\n</tr>\n<tr>\n<td>UI FPS drops</td>\n<td>too much native work: deep view trees, huge images, many shadows</td>\n<td>view hierarchy, image sizes</td>\n</tr>\n<tr>\n<td>Taps feel delayed</td>\n<td>JS thread busy when the touch arrives</td>\n<td>Profiler during the interaction</td>\n</tr>\n</tbody>\n</table></div><p>Most problems in React Native apps are on the JS side, and most of those are <strong>re-rendering too much</strong>. Record a Profiler session while you reproduce the slowness, then look for components that render far more often than they should. The Profiler can record why each component rendered (changed props, state, context or parent), which turns a guess into a fix.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Fixes, in order of payoff</h2>\n<p>Revisit the list lesson’s checklist first: memoized rows, stable props, <code dir=\"ltr\">keyExtractor</code>, right-sized images. Beyond lists:</p>\n<ul>\n<li><strong>Move work out of render.</strong> Sorting 600 hikes or formatting dates in every render adds up. Compute once with <code dir=\"ltr\">useMemo</code>, or when the data changes.</li>\n<li><strong>Don’t re-render the world on a timer.</strong> A ticking clock or a live GPS position stored in the state of a big screen re-renders everything under it. Put fast-changing state in the smallest component that shows it.</li>\n<li><strong>Animate on the UI thread.</strong> The <code dir=\"ltr\">Animated</code> API with <code dir=\"ltr\">useNativeDriver: true</code>, or the Reanimated library, runs animations natively, so a busy JS thread can’t make them stutter. Gestures paired with animations (swipe to delete, bottom sheets) belong there too.</li>\n<li><strong>Let the compiler memoize.</strong> Expo supports the React Compiler, which inserts memoization automatically. It removes a whole class of hand-written <code dir=\"ltr\">memo</code> and <code dir=\"ltr\">useCallback</code>, but it doesn’t fix heavy work in render.</li>\n</ul>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Optimizing what you haven’t measured</span></p>\n<p>Sprinkling <code dir=\"ltr\">useCallback</code> and <code dir=\"ltr\">memo</code> across the codebase “for performance” makes code harder to read and often changes nothing, because the real cost was somewhere else: a 4000-pixel image, a timer re-rendering the screen, or simply development mode. Measure in production mode, find the hot spot, fix that, measure again.</p>\n</aside>\n<h2 id=\"sec-5\" tabindex=\"-1\">A debugging routine that works</h2>\n<p>When a bug report arrives, write down the exact steps, device and OS version. Reproduce it on a similar device. Decide which side it’s on (JavaScript error, native crash, or performance). Use the matching tool. And when you’ve fixed it, ask whether a test could have caught it, which is the next lesson.</p>\n","toc":[{"id":"sec-1","text":"The tools you have","level":2},{"id":"sec-2","text":"Measure in production mode","level":2},{"id":"sec-3","text":"JS thread or UI thread?","level":2},{"id":"sec-4","text":"Fixes, in order of payoff","level":2},{"id":"sec-5","text":"A debugging routine that works","level":2}],"takeaways":["Press <code dir=\"ltr\">j</code> in the Metro terminal (or use the dev menu) to open React Native DevTools, with a console, breakpoints, a Components inspector and a Profiler.","Development mode is much slower than a release build, so judge performance with <code dir=\"ltr\">npx expo start --no-dev --minify</code> or a real build.","The Performance Monitor shows JS and UI frame rates separately; which one drops tells you where the problem is.","Most JavaScript slowness in React Native is re-rendering too much, too often; the Profiler shows which components and why.","Run animations on the UI thread (Reanimated, or Animated with the native driver) so a busy JS thread can’t make them stutter."],"further":[{"title":"React Native DevTools","url":"https://reactnative.dev/docs/react-native-devtools"},{"title":"Debugging Basics","url":"https://reactnative.dev/docs/debugging"},{"title":"Performance Overview","url":"https://reactnative.dev/docs/performance"}],"quiz":[{"q":"<p>Trailhead’s hike list scrolls badly in development on your phone. What should you do before optimizing anything?</p>","options":[{"text":"Wrap every component in <code dir=\"ltr\">memo</code>.","why":"Memoizing blindly adds complexity and may not touch the real problem. Measure first."},{"text":"Switch the list from FlatList to ScrollView.","why":"That renders every row up front, which makes long lists slower, not faster."},{"text":"Upgrade to the newest phone available and test there.","why":"Your users don’t have the newest phone. Test on a mid-range device, in production mode."},{"text":"Run the JavaScript in production mode (<code dir=\"ltr\">npx expo start --no-dev --minify</code> or a release build) and check whether it’s still slow.","why":"Correct. Development mode adds checks and warnings that slow everything down; many “performance bugs” disappear in production mode."}],"answer":3},{"q":"<p>The Performance Monitor shows the UI thread at 60 FPS and the JS thread dropping to 15 FPS while you scroll. Where is the problem?</p>","options":[{"text":"In JavaScript, for example components re-rendering on every scroll event or heavy work in render.","why":"Correct. The native side keeps up; your JavaScript can’t. The Profiler will show which components are busy."},{"text":"In the native views, for example too many nested Views or oversized images.","why":"That would show up as a UI thread drop. Here the UI thread is fine."},{"text":"In the network, because requests block the JS thread.","why":"Network requests are asynchronous; waiting on them doesn’t consume JS thread frames."}],"answer":0},{"q":"<p>A fade-in animation on the hike detail screen stutters while the screen loads its data. What’s the most robust fix?</p>","options":[{"text":"Delay loading the data until the animation finishes.","why":"That slows the screen down to hide the stutter, and any other JS work can still cause it."},{"text":"Run the animation on the UI thread, with Reanimated or <code dir=\"ltr\">Animated</code> using <code dir=\"ltr\">useNativeDriver: true</code>.","why":"Correct. Animations driven on the UI thread don’t depend on the busy JS thread, so they stay smooth."},{"text":"Lower the animation’s duration so the stutter is shorter.","why":"A shorter stutter is still a stutter; the cause is the JS thread being busy."}],"answer":1}],"exercise":{"kind":"spot-bug","language":"jsx","code":"import { memo, useEffect, useState } from 'react';\nimport { FlatList, Text, View } from 'react-native';\nimport { useHikes } from '../state/hikes';\n\nconst HikeRow = memo(function HikeRow({ hike }) {\n  return (\n    <View style={{ padding: 12 }}>\n      <Text>{hike.name}</Text>\n      <Text>{hike.distanceKm} km</Text>\n    </View>\n  );\n});\n\nexport default function HikeLog() {\n  const hikes = useHikes();\n  const [now, setNow] = useState(Date.now());\n\n  useEffect(() => {\n    const id = setInterval(() => setNow(Date.now()), 16);\n    return () => clearInterval(id);\n  }, []);\n\n  return (\n    <View style={{ flex: 1 }}>\n      <Text>Updated {new Date(now).toLocaleTimeString()}</Text>\n      <FlatList\n        data={hikes}\n        keyExtractor={(item, index) => String(index)}\n        renderItem={({ item }) => <HikeRow hike={{ ...item }} />}\n      />\n    </View>\n  );\n}","bugLines":[19,28,29],"title":"Why does the hike log stutter?","prompt":"<p>Trailhead’s log stutters on a mid-range Android phone, even in production mode. The Performance Monitor shows the <strong>JS thread</strong> dropping frames while the UI thread is fine, and the Profiler shows every <code dir=\"ltr\">HikeRow</code> re-rendering constantly. Select the <strong>three</strong> lines responsible.</p>\n","hints":["Something makes the whole screen re-render many times a second. How often does 16 ms fire?","<code dir=\"ltr\">memo</code> skips a re-render only when every prop is the same object as last time.","Look at how rows are identified, too: what happens to keys when a hike is inserted at the top?"],"explanation":"<p>Line 19 updates state every 16 ms, re-rendering the whole screen about 60 times a second just to show a clock with one-second precision; use a much longer interval, and move the clock into its own small component so only it re-renders. Line 29 passes <code dir=\"ltr\">{ ...item }</code>, a brand-new object on every render, so <code dir=\"ltr\">memo(HikeRow)</code> never gets to skip a row; pass <code dir=\"ltr\">hike={item}</code>. Line 28 keys rows by index, so inserting a hike at the top shifts every key, and every row re-mounts; use <code dir=\"ltr\">keyExtractor={(hike) =&gt; hike.id}</code>. The inline <code dir=\"ltr\">style</code> objects inside <code dir=\"ltr\">HikeRow</code> are not the problem: they’re created only when the row actually renders.</p>\n","checkLabels":{}},"words":833,"readMinutes":4}