January 5, 2026
If your users are experiencing page crashes on iOS - particularly on older iPhones - you likely have a memory problem. And unlike CPU performance issues, memory problems in WebKit are poorly understood, difficult to diagnose, and can create cascading failures that make things exponentially worse.
This guide explains how WebKit manages memory on iOS, why pages crash, and what you can do about it.
Most web performance discussions focus on CPU - how long scripts take to execute, main thread blocking, interaction latency. But on mobile devices, particularly iOS, RAM is often the silent killer.
When a page exceeds available memory, it doesn't slow down gracefully. It crashes. The tab reloads. If it happens repeatedly, the tab gets marked as crashed. Users see a blank page or an error message. They leave.
This is especially problematic for content-heavy sites with significant JavaScript, ad tech, and third-party integrations. The combination of large bundles, hydration overhead, and dynamic ad content can push memory usage past iOS limits surprisingly quickly.
Here's the part that catches most developers off guard: iOS devices have far less available heap memory than you'd expect.
| Device | Approximate Heap Limit |
|---|---|
| iPhone 6s / SE (1st gen) | ~200-250MB |
| iPhone 8 / X | ~300-350MB |
| iPhone 11 / 12 | ~350-400MB |
| iPhone 13 / 14 | ~400-450MB |
| iPhone 15+ | ~1GB+ |
These numbers vary based on system load, background apps, and iOS version. The newer devices have significantly more headroom, but a huge portion of your mobile traffic is likely still on older devices with much tighter limits.
For comparison, your development MacBook probably has 16-32GB of RAM. Chrome on desktop can happily consume gigabytes. When you're testing locally, memory issues are invisible.
This is why pages that work perfectly in development crash repeatedly for real users on mobile Safari.
On iOS, two memory management systems work together (and sometimes against each other):
WebKit MemoryPressureHandler - WebKit's internal memory management system. The source code is available in the WebKit repository, so we can see exactly how it works.
iOS Jetsam subsystem - Apple's system-wide memory management. Applies to all apps, including Safari. More aggressive than WebKit's own handling. Source code is not publicly available.
The critical point: the lower of the two limits always applies. If Jetsam decides your page is using too much memory, it can force WebKit to act - or kill Safari entirely.
The base threshold is calculated as:
baseThreshold = min(3 GB, min(physical_RAM, jetsam_limit))
(Source: AvailableMemory.cpp)
In practice, for web pages on iOS, this typically lands in the 300-450MB range for most devices currently in use.
WebKit defines three memory pressure levels, each triggering increasingly aggressive cleanup:
Triggered when memory usage exceeds half the available limit. WebKit checks this every 30 seconds, or immediately if Jetsam sends an event.
What gets cleared:
Render caches (layout line caches, text breaking caches)
Dead resources (unused cached images, CSS, JS)
Glyph caches (font rendering caches)
Triggers garbage collection (non-blocking)
case MemoryUsagePolicyConservative:
releaseMemory(CriticalNo, Synchronous::No);
(Source: WTF/wtf/MemoryPressureHandler.cpp)
This is relatively gentle - pruning caches that can be rebuilt if needed.
Triggered at 65% of the limit. This is where things get serious.
What gets cleared:
Back/forward cache (destroys cached pages)
Decoded image data (keeps compressed data only)
CSS value pools
Audio HRTF cache (Web Audio)
Cookie cache
Query selector results
Font caches (emptied entirely)
Media buffers (video/audio buffered data)
All JIT-compiled JavaScript code
case MemoryUsagePolicyStrict:
releaseMemory(CriticalYes, Synchronous::No);
(Source: WTF/wtf/MemoryPressureHandler.cpp)
That last one is critical. WebKit deletes all the compiled JavaScript, meaning it will need to be recompiled when next executed. For sites with large JavaScript bundles, this creates significant performance degradation as the JIT compiler has to redo all its work.
Same cleanup as Strict mode, but runs synchronously - blocking everything until complete.
After cleanup, memory is measured again:
The relevant WebKit source code:
void MemoryPressureHandler::shrinkOrDie(size_t killThreshold)
{
RELEASE_LOG(MemoryPressure, "Process is above the memory kill threshold. Trying to shrink down.");
releaseMemory(Critical::Yes, Synchronous::Yes);
size_t footprint = memoryFootprint();
RELEASE_LOG(MemoryPressure, "New memory footprint: %zu MB", footprint / MB);
if (footprint < killThreshold) {
RELEASE_LOG(MemoryPressure, "Shrank below memory kill threshold. Process gets to live.");
return;
}
WTFLogAlways("Unable to shrink memory footprint (%zu MB) below kill threshold (%zu MB). Killed\n",
footprint / MB, killThreshold / MB);
m_memoryKillCallback();
}
Download our service guide to understand how we can help you optimise your site speed
(Source: WTF/wtf/MemoryPressureHandler.cpp - note that WTF stands for "Web Template Framework")
Jetsam is iOS's system-wide memory pressure handler. It monitors all running processes and can send events to WebKit or kill apps directly.
When Jetsam sends a memory pressure event, WebKit responds based on severity:
dispatch_source_set_event_handler(memoryPressureEventSource().get(), ^{
auto status = dispatch_source_get_data(memoryPressureEventSource().get());
switch (status) {
// VM pressure events
case DISPATCH_MEMORYPRESSURE_WARN:
respondToMemoryPressure(Critical::No);
break;
case DISPATCH_MEMORYPRESSURE_CRITICAL:
respondToMemoryPressure(Critical::Yes);
break;
// Process memory limit events
case DISPATCH_MEMORYPRESSURE_PROC_LIMIT_WARN:
didExceedProcessMemoryLimit(ProcessMemoryLimit::Warning);
respondToMemoryPressure(Critical::No);
break;
case DISPATCH_MEMORYPRESSURE_PROC_LIMIT_CRITICAL:
didExceedProcessMemoryLimit(ProcessMemoryLimit::Critical);
respondToMemoryPressure(Critical::Yes);
break;
}
});
(Source: MemoryPressureHandlerCocoa.mm)
The source code for Jetsam itself is not publicly available, but we know it can be more aggressive than WebKit's 30-second check interval. In extreme cases, Jetsam will kill Safari entirely if memory pressure is too high and WebKit isn't responding fast enough.
Based on our experience auditing sites with iOS crash problems, these are the most common contributors:
JavaScript frameworks - Modern JavaScript frameworks ship significant amounts of code that needs to be parsed, compiled, and held in memory. The bigger the bundle, the more memory pressure you create.
Third-party scripts - Ad tech, analytics, comment systems, social widgets, verification vendors, and other third-party integrations all consume memory. The cumulative impact of multiple vendors can be substantial, and memory usage often grows over time as dynamic content loads.
Here's the challenge: iOS provides very little in the way of memory diagnostics. Safari's memory profiler is notoriously unreliable on sites with high memory usage - ironically, it tends to break on exactly the sites you need to diagnose.
Chrome's memory profiler is significantly more reliable. While it won't match Safari's behaviour exactly, it can help you understand your codebase's memory characteristics and identify obvious problems.
To profile in Chrome:
Caveats:
To identify which scripts are causing problems, test the page with different scripts blocked and compare memory usage. This helps identify whether the problem is your own code or third-party integrations.
Important: diagnostic runs vary substantially between runs due to differences in ad creative, user-generated content, and other dynamic elements. Focus on overall trends rather than absolute values.
For long pages with dynamic content, destroy off-screen content rather than just hiding it:
The goal is to keep memory usage roughly constant over time, rather than growing linearly with user engagement.
Every kilobyte of JavaScript that doesn't ship is memory that doesn't get used. Focus on:
Remember: the JIT death spiral means that bundle size has an outsized impact on iOS memory stability. A 20% reduction in JavaScript might result in a 50%+ reduction in crashes.
Don't load everything upfront:
But be careful: lazy loading that doesn't clean up after itself can make memory problems worse over time.
For sites with significant mobile traffic, a server-rendered approach often makes more sense than a single-page application:
This applies to content-focused sites but also to any application where mobile represents a significant portion of traffic. E-commerce sites, dashboards accessed on mobile, internal tools used on tablets - if users are hitting your app on iOS devices, the architectural choice has real consequences.
This is a significant architectural decision, but if you're fighting constant iOS crashes on a site running a heavy JavaScript framework, it may be worth considering. The engineering effort to optimise a large SPA for iOS memory limits can exceed the effort of rebuilding with a simpler architecture.
Experiencing iOS crashes on your site? We specialise in diagnosing and fixing web performance issues.Get in touch- we'd love to help.
Download our service guide to understand how
we can help you optimise your site speed