Opening a player profile hard-crashed the app. No JavaScript exception, no error boundary triggered, no stack trace worth reading — the process simply went away.

This is from GameLynx, a React Native app with a live map at the centre of the product. The bug is worth writing up because the debugging approach mattered far more than the eventual fix, and because the instinct it corrects is a common one.

The wrong instinct

A screen crashes when you open it. The natural response is to read the screen’s code. What is it rendering, what data is it fetching, what could be undefined.

Days went into that, and it found nothing — because there was nothing there to find. The profile screen’s logic was fine. The crash was not caused by what the screen did; it was caused by what the screen cost.

The tell, in hindsight: a genuine JavaScript error produces a genuine JavaScript error. Silence means something below the JavaScript layer decided the process should stop.

What the profiler showed

Instrumenting the running app and watching the native heap made it immediately legible:

  • Baseline sat around 185 MB
  • Opening a profile pushed it past 390 MB
  • Garbage collection then stalled for 24 seconds
  • Android resolved this the way it always does — by killing the process

An app unresponsive for 24 seconds is, from the OS’s point of view, an app that is never coming back.

The actual cause

The profile screen showed a small map thumbnail of the player’s home area. Reasonable feature, three lines of code.

But the profile was pushed onto a navigation stack, and the discovery map underneath stayed mounted. That is normal and correct behaviour for a navigation stack — going back should not rebuild the previous screen.

The consequence is that a second live Mapbox GL instance now existed alongside the first. Map instances hold GPU textures, tile caches, and style resources. Two of them is not twice the memory of one, because the second arrives when the first has already warmed its caches. Together they crossed a line the device would not tolerate.

Each piece was individually reasonable. The interaction was not.

The fix

Two rules, both narrow:

  1. Non-interactive maps are not live maps. The profile thumbnail became a Mapbox Static Images API render — an image. It looks identical at that size, because at 120 pixels square nobody pans a map.
  2. Only one live map instance may be mounted at any time. Enforced structurally rather than by convention, so a future screen cannot reintroduce this by accident.

Memory settled at a stable 217 MB across repeated navigation. Not 185 — the app legitimately does more now — but stable, which is the property that matters. A number that does not grow with navigation is a number that will not kill the process.

What transfers

Silence in the JavaScript layer means look below it. An exception is a message from your own code. Its absence is a message from something else.

Resource cost is a feature’s cost. Reviewing what a component does misses what it holds. A map, a video player, a camera preview, or a large canvas each carry a native cost that no amount of reading the render function reveals.

Navigation stacks keep screens alive. That is the point of them. It also means “how many of these can exist at once” is a real question with a real answer, and the answer is often not one.

Profile earlier than feels justified. Days went into reading code that was not broken. An afternoon with a profiler found it. The reluctance to reach for the profiler is usually about it feeling like a heavyweight tool for a small bug — but the size of a bug is not knowable in advance, which is rather the point.

Why this ends up on a services site

Because this is what a lot of the work actually is. Feature lists are easy to write and easy to copy. The difference between an app that ships and one that stalls in beta is usually a handful of problems like this one, and how quickly they get diagnosed.

The GameLynx case study documents five more, including a keyboard that closed the instant it opened — which turned out to be a focus style recreating the native view under React Native’s Fabric renderer.

If you are maintaining an app with problems in this category, ongoing support is where that work lives.