[h] home·[n] notes·[w] work·[a] about
INA·
← notes
The Bug That Only Happened on iPhone
 
DebuggingPerformanceNext.jsReactiOSWeb Performance

The Bug That Only Happened on iPhone

How a storefront crash led us to discover that the real problem wasn't our code—it was our images.

Introduction

One of the most challenging production issues I've worked on wasn't caused by a JavaScript exception or a failed API request.

It happened only on iPhones.

Users reported that our storefront would become increasingly slow while browsing products. After scrolling for some time, the browser would suddenly freeze or refresh itself.

The strange part?

Everything worked perfectly on desktop browsers and Android devices.

Even stranger, the issue happened across Safari, Chrome, and Edge on iOS.

At first glance, it looked like an iOS browser bug.

It wasn't.


The Symptoms

Users consistently described the same experience:

  • The storefront loaded normally.
  • Products appeared without any issues.
  • Scrolling initially felt smooth.
  • Performance gradually degraded while browsing.
  • Eventually, the browser refreshed or crashed.

From the application's perspective, everything appeared healthy.

No failed API requests.

No React errors.

No console exceptions.

Nothing obvious pointed to the cause.


The First Investigation

Our first instinct was to collect as much diagnostic information as possible.

We integrated Sentry into the application, expecting it to capture runtime exceptions or browser crashes.

Unfortunately, it didn't.

No JavaScript errors were reported.

No React exceptions appeared.

No unhandled promise rejections.

Nothing.

At first, this was frustrating.

But looking back, it was actually an important clue.


What Sentry Couldn't Tell Us

Sentry is excellent at capturing JavaScript errors.

However, this issue wasn't a JavaScript failure.

The browser itself was terminating the page.

Once WebKit decided to kill the process because of memory pressure, there was no JavaScript left running to report anything.

From Sentry's perspective, nothing had crashed.

The application simply disappeared.

This completely changed the direction of our investigation.


Looking Beyond JavaScript

Instead of searching for exceptions, we started profiling the application.

Using Safari Web Inspector, we monitored:

  • CPU usage
  • Memory usage
  • Rendering performance
  • Network activity

CPU usage looked normal.

Rendering wasn't unusually expensive.

API requests completed successfully.

But memory usage kept growing as users continued scrolling.

Eventually, iOS terminated the page.

That finally explained why neither the browser console nor Sentry reported any errors.


Understanding iOS

One important detail is that every browser on iOS uses WebKit under the hood.

Whether users opened the storefront using Safari, Chrome, or Edge, they all shared the same rendering engine.

Unlike desktop browsers, WebKit on iOS aggressively limits memory usage.

When a webpage exceeds its available memory budget, the operating system may terminate it without throwing a JavaScript exception.

That perfectly matched our observations.


Finding the Root Cause

Once we shifted our attention to memory usage, the answer became much clearer.

The issue wasn't React.

It wasn't Next.js.

It wasn't our API.

It was the images.

Our storefront displayed a large collection of products, each with relatively large product images.

As users continued scrolling, more images were decoded and retained in memory.

Desktop browsers had enough available memory to handle this workload.

iPhones did not.

Eventually, memory usage crossed the threshold enforced by WebKit, causing the browser to terminate the page.


The Solution

Instead of optimizing React components, we optimized the assets.

Compress Product Images

We significantly reduced the size of our product images while maintaining acceptable visual quality.

This immediately reduced both bandwidth usage and memory consumption.


Deliver Images at the Right Size

Some source images were much larger than necessary for product cards.

Instead of downloading large images and shrinking them with CSS, we served images closer to their actual display size.


Keep Lazy Loading

Lazy loading was already in place, but smaller image assets made it considerably more effective.

Each newly loaded product consumed much less memory than before.


The Results

After deploying the optimized images:

  • No more crashes on iPhone.
  • Memory usage became much more stable.
  • Scrolling performance improved noticeably.
  • Faster page loads.
  • Reduced bandwidth usage.
  • Better Core Web Vitals.

The fix benefited every user, not just those on iOS.


Lessons Learned

This incident taught me several valuable lessons.

Not every production issue generates a stack trace.

Sometimes the browser itself is the one failing.

Monitoring tools have limits.

Sentry is invaluable for JavaScript exceptions, but it cannot report crashes after the browser process has already been terminated by the operating system.

Always validate your assumptions.

We initially spent time looking for React bugs because that's where we expected the problem to be.

The real bottleneck was memory usage caused by oversized image assets.

Performance isn't just about code.

Optimizing JavaScript is important, but shipping smaller assets can have an even greater impact on real-world performance.


Final Thoughts

This bug reminded me that debugging production issues is often about eliminating incorrect assumptions.

I expected Sentry to point directly to the problem.

Instead, the absence of errors became the clue.

Once we stopped focusing solely on JavaScript and started measuring what the browser was actually doing, the root cause became clear.

The solution wasn't rewriting components or introducing complex optimizations.

It was simply reducing the amount of data we were asking users' devices to process.

Sometimes, the best engineering solution isn't writing better code.

It's shipping fewer bytes