Why iOS Safari Reloads Your Tab Under Memory Pressure

Users on iPhones report that your web app “randomly refreshes” and loses their work, or Safari shows “A problem repeatedly occurred” and gives up — while nothing is wrong on desktop. This guide from Remote Debugging Memory on Mobile Browsers, in Browser DevTools & Performance Profiling Workflows, explains how iOS memory limits terminate WebKit content processes, how to see which memory categories push you over, and how to make the page survive when it happens anyway.

Symptom Root Cause Immediate Action Measurable Impact
Page reloads by itself while in use Web content process exceeded its memory limit and was killed Record Web Inspector’s Memory timeline during the flow Identifies which category crosses the limit
Page reloads when returning to the tab Background process killed to free memory for other apps Save state on visibilitychange/pagehide; restore on load Users keep their work across reloads
“A problem repeatedly occurred” banner The page is killed again right after each reload Reduce peak memory on initial load Page loads successfully on low-memory devices
Images category dominates the timeline Large decoded images and canvases Serve right-sized images; release canvases Often hundreds of MB saved on image-heavy pages
JavaScript category climbs steadily A leak that desktop users never notice Apply standard leak detection in desktop Chrome and Safari Stops the gradual climb to the kill threshold

Root Cause: No Swap, Hard Limits, and Jetsam

iOS does not page memory out to disk the way desktop operating systems do. When memory runs short, it asks processes to release caches, and then the kernel’s memory-pressure mechanism — commonly called jetsam — terminates processes, background ones first. Each process also has a limit on how much memory it may use at all, and exceeding it gets the process terminated even in the foreground.

Safari, and every other iOS browser because they all use WebKit, renders pages in separate WebContent processes. When a WebContent process is terminated, Safari notices and reloads the page, which users experience as a spontaneous refresh. If the reloaded page crosses the limit again quickly, Safari stops retrying and shows its error banner. Nothing is written to your JavaScript error logs, because from the page’s point of view it simply stopped existing.

The limits are not published, vary by device model and iOS version, and are shared with everything else the device is doing, so an iPhone with a few gigabytes of RAM can terminate a page using far less than a desktop tab happily holds. What counts is the whole process footprint, not just the JavaScript heap: decoded images, canvas backing stores, WebGL textures, compositing layers for transformed and animated elements, DOM and layout structures, and the JavaScript heap together. Image-heavy and canvas-heavy pages hit the limit first, which is why the fixes in canvas and ImageBitmap memory in long-running apps matter disproportionately on iOS.

Web Inspector’s Timelines → Memory instrument, available when you connect an iPhone to a Mac as described in profiling Safari and iOS memory with Web Inspector, breaks the page’s memory into categories — JavaScript, Images, Layers, Page — and marks memory pressure events, which is exactly the view you need to find out what pushes you over.

Memory categories climbing to the kill line A cumulative line chart over time shows four categories: Page and DOM at the bottom, JavaScript added above it, then Layers, and Images on top as the total. As the user scrolls a photo gallery, the Images line grows until the total reaches a dashed device limit line. At that moment the WebContent process is terminated and memory drops to zero, followed by a reload that starts the climb again. device limit (unpublished, varies) process killed → Safari reloads reload: state lost Images Layers JavaScript Page / DOM scrolling a photo gallery →

Step-by-Step Fix

  1. Connect Web Inspector to the device. On the iPhone enable Settings → Safari → Advanced → Web Inspector; on the Mac enable Safari → Settings → Advanced → Show features for web developers, connect the phone, and choose Develop → [device] → [page]. Verification: a Web Inspector window opens for the page on the phone.
  2. Record the Memory timeline during the flow. Open Timelines, make sure Memory is enabled in the instruments list, start recording, and perform the flow that precedes reloads. Verification: the Memory track shows categories and a peak value, and any memory pressure events are marked.
  3. Identify the dominant category at the peak. Select the peak region and read the breakdown. Verification: you know whether Images, Layers, JavaScript or Page dominates, and by how many MB.
  4. Apply the matching reduction. For Images: serve responsive sizes with srcset, lazy-load off-screen images, and drop references to large Image/ImageBitmap objects. For Layers: remove unnecessary will-change and 3D transforms on many elements. For JavaScript: run leak detection. For Page: virtualize long lists. Verification: re-recording shows a lower peak for that category.
  5. Preserve state before the kill can happen. Save in-progress work to sessionStorage or IndexedDB on visibilitychange (hidden) and pagehide, and restore it on load. Verification: backgrounding Safari, opening several heavy apps and returning restores the user’s work even if the page reloads.
  6. Test on the smallest supported device. Repeat the flow on the oldest, lowest-memory iPhone you support. Verification: the flow completes without reloads and the peak stays well below the level at which kills were observed.
Peak memory by category, before and after Before: images 610 megabytes, layers 140, JavaScript 95, page 60, total 905 megabytes and the tab reloads. After serving right-sized images, releasing off-screen bitmaps and virtualizing the grid: images 150, layers 40, JavaScript 70, page 25, total 285 megabytes with no reload. Peak during gallery scroll (MB) Before (reloads) 905 MB total After (stable) 285 MB total Images Layers JavaScript Page

Command and Code Reference

Use case: persist in-progress work so a reload loses nothing. pagehide and visibilitychange are the last reliable moments before a background kill.

// draft-persistence.js
const KEY = 'draft:v1';

function saveDraft() {
  const draft = collectFormState();                  // small, serialisable object
  try { sessionStorage.setItem(KEY, JSON.stringify(draft)); } catch { /* quota */ }
}

// Save whenever the page might be about to disappear
document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') saveDraft();
});
addEventListener('pagehide', saveDraft);

// Restore on load, including after a jetsam-triggered reload
const saved = sessionStorage.getItem(KEY);
if (saved) restoreFormState(JSON.parse(saved));

Use case: release decoded image memory in a gallery. Dropping references to off-screen bitmaps and using right-sized sources keeps the Images category bounded.

// Keep only bitmaps for tiles near the viewport; close the rest
const bitmaps = new Map();                            // tileId → ImageBitmap

async function showTile(tile) {
  if (!bitmaps.has(tile.id)) {
    const blob = await (await fetch(tile.urlForWidth(tile.cssWidth * devicePixelRatio))).blob();
    bitmaps.set(tile.id, await createImageBitmap(blob)); // decoded once, right-sized
  }
  drawTile(tile, bitmaps.get(tile.id));
}

function hideTile(tile) {
  bitmaps.get(tile.id)?.close();                      // frees decoded pixels now
  bitmaps.delete(tile.id);
}

Verification and Regression Prevention

The problem is solved when the flow that used to trigger reloads completes on your lowest-memory supported iPhone with the Memory timeline’s peak comfortably below the level at which kills occurred, and when forcing a background kill (open the page, switch to several heavy apps, return) restores the user’s state even if the page reloads. Keep your recorded timelines: comparing category peaks between releases is the fastest way to catch regressions.

Because the limit is unpublished, set your own budget from observation — for example “peak under 300 MB on an iPhone with 3 GB of RAM” — and test against it on every release. Lab automation cannot drive real iOS memory limits, so pair a manual device check with automated desktop checks of the categories you can measure there: JavaScript heap via automated leak detection in CI, and image weight via bundle and asset budgets.

Testing reload recovery on iPhone Run the flow that used to trigger reloads on the lowest-memory supported iPhone and check the Web Inspector Memory timeline peak is comfortably below the observed kill level. Then open the page, switch to several heavy apps, return, and confirm the user’s state is restored even if the page reloaded. Lowest-RAM iPhone the flow that reloaded Memory timeline peak well below kill level Background kill switch to heavy apps Return state restored after reload

Edge Cases and Gotchas

All iOS browsers behave this way

Chrome, Firefox and Edge on iOS all use WebKit, so they inherit the same process model and limits. A reload problem reported by an iOS Chrome user is the same problem; test with Safari’s Web Inspector.

Back/forward cache restores are not reloads

A page restored from the back/forward cache fires pageshow with event.persisted === true and keeps its JavaScript state. A jetsam reload runs your scripts from scratch. Log the distinction in your analytics so you can count real memory-related reloads.

Installed web apps get similar treatment

Home-screen web apps run in WebKit processes too and are terminated under the same pressure, often more aggressively when in the background. The same state-persistence pattern applies.

Memory pressure events are hints

Web Inspector marks memory pressure events, but pages receive no JavaScript event for them. Design for the worst case — assume the page can disappear whenever it is hidden — rather than trying to react at the last moment.

Frequently Asked Questions

What is the memory limit for a Safari tab on iPhone?

Apple does not publish it, and it varies with device RAM, iOS version and what else is running. Treat it as unknown and measure your own peak with the Memory timeline on the lowest-memory device you support, keeping a generous margin below the point where you observe kills.

Can JavaScript detect that the page was killed?

Not while it happens. After the reload you can infer it: store a flag in sessionStorage on pagehide or visibilitychange, and if the next load finds the flag without a matching orderly navigation, count it as an unexpected reload in your telemetry.

Why does the same page work on desktop Safari?

Desktop macOS compresses and swaps memory and gives processes far more headroom, so a page using 900 MB merely feels heavy. On iOS the same footprint crosses the per-process limit and the WebContent process is terminated.