RORK LABJP
TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the roundTOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Articles/Dev Tools
Dev Tools/2026-05-25Advanced

iOS Memory Pressure on Rork Apps — A 5-Tier Release Architecture from 12 Years of Wallpaper Apps

A 5-tier memory release architecture for Rork-built iOS apps, refined while running wallpaper apps to a cumulative 50 million downloads. Includes the Instruments + MetricKit measurement flow that brought OOM rate from 4.3% to 0.36%.

Rork515iOS109Memory ManagementOOMPerformance23Instruments2Wallpaper Apps3Indie Developer11

Premium Article

This morning's Crashlytics alert had the same pattern at the top again. "Out of Memory (Background)" was the leading group, and the reproducing devices were iPhone SE (3rd gen) and iPad mini 6. The former ships with 4GB of physical RAM, and while the iPad mini 6 also has 4GB, iPadOS multitasking is strict enough that the OS terminates the process within seconds of a memory warning.

When you run wallpaper apps, memory issues never really go away. Users keep tapping "next wallpaper, next wallpaper," cycling through high-resolution images one after another. With original source material around 3,000 x 5,000 pixels, a single decoded bitmap reserves over 60MB of memory, and rendering five or six in a list view is enough to start brushing against physical memory limits.

I've been building iOS and Android apps as an indie developer since 2014. Cumulative downloads have crossed 50 million, with most of that coming from wallpaper, healing, and law-of-attraction style image-centric apps. Even after I moved the recent lineup to a React Native base via Rork, the memory characteristics of "images are heavy" haven't changed. If anything, the JS bridge adds another layer, so without a release strategy that coordinates with native code, you find yourself unable to react quickly enough once a memory warning fires.

What I've sharpened over these 12 years is a design I call the 5-tier release architecture. The push toward 1 million yen per month in AdMob revenue made OOM crashes painfully tangible — each one directly chipped at LTV. This article walks through the actual implementation I run in Rork-generated Expo projects, on both the Swift and TypeScript sides, along with the measurement flow that took my OOM rate from 4.3% to 0.36%. I'm Masaki Hirokawa, an artist and personal developer shipping on both App Store and Google Play for the last 12 years, and I've kept the operational realism as close to my actual notebook as I can.

Why staged release matters

iOS memory warnings come in two waves. First, a light warning is delivered as a UIApplication.didReceiveMemoryWarning notification. Ignore it, and within seconds jetsam will kill the process — with an even stricter threshold if you're in the background. Even in foreground, if you keep allocating after the warning, the app dies without applicationWillTerminate ever firing.

The naive response is "clear the entire image cache on warning." That does lower OOM rate, but the side effect crushes user experience. The moment a warning fires mid-scroll, on-screen images vanish, a flash occurs, and reload spinners appear. For a wallpaper app, that registers as "the whole app froze" to users.

That's why I introduced a design that splits the release target into 5 tiers and lets the app shed memory in stages depending on warning level and pressure on remaining RAM. The tiers are ordered by how much they hurt user experience, and only when things get genuinely tight does the app reach into the deeper tiers.

  1. Tier 1: Disk cache and prefetched remote data — invisible to users, re-fetchable
  2. Tier 2: Decoded bitmaps for off-screen views — image cache outside the viewport
  3. Tier 3: Intermediate buffers for animations and transitions — visible to feel, but restorable
  4. Tier 4: Thumbnails and adjacent prefetch beyond the visible range — supports scrolling feel, but rebuildable
  5. Tier 5: High-resolution variants of currently visible images — last resort; swap in low-res placeholders

Releasing in this order minimizes perceived degradation while substantially reducing the probability of forced termination by jetsam. Across my apps, this design brought OOM rate from 4.3% to 0.36% (trailing 30 days, iPhone SE 3rd gen).

Receive on the native side — a Swift memory pressure observer

Even on Expo projects generated by Rork, adding a single native module lets you forward iOS memory warnings up to React Native. Start with the Swift listener.

import Foundation
import UIKit
import os
 
/// Singleton responsible for the 5-tier release strategy.
@objc(MemoryPressureManager)
class MemoryPressureManager: NSObject {
 
    enum Tier: Int {
        case disk = 1       // Tier 1: disk cache
        case offscreen = 2  // Tier 2: off-screen views
        case animation = 3  // Tier 3: animation buffers
        case adjacent = 4   // Tier 4: adjacent prefetch
        case visibleHi = 5  // Tier 5: visible high-res variants
    }
 
    static let shared = MemoryPressureManager()
    private let log = Logger(subsystem: "design.dolice.memory", category: "pressure")
    private var lastReleasedTier: Tier = .disk
    private var lastReleasedAt: Date = .distantPast
 
    override init() {
        super.init()
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(didReceiveMemoryWarning),
            name: UIApplication.didReceiveMemoryWarningNotification,
            object: nil
        )
    }
 
    @objc private func didReceiveMemoryWarning() {
        let footprint = currentFootprintMB()
        log.warning("memory warning at \(footprint, format: .fixed(precision: 1)) MB")
        escalateRelease(currentFootprintMB: footprint)
    }
 
    /// Decide the next tier to release based on remaining RAM
    /// and recent release history.
    private func escalateRelease(currentFootprintMB: Double) {
        let now = Date()
        let secondsSinceLast = now.timeIntervalSince(lastReleasedAt)
 
        // If we just released the same tier within 3 seconds, escalate one step.
        let nextRaw: Int
        if secondsSinceLast < 3.0 {
            nextRaw = min(lastReleasedTier.rawValue + 1, Tier.visibleHi.rawValue)
        } else if currentFootprintMB > 600 {
            // Past 600MB, start straight from Tier 3.
            nextRaw = max(lastReleasedTier.rawValue, Tier.animation.rawValue)
        } else {
            nextRaw = Tier.disk.rawValue
        }
 
        let tier = Tier(rawValue: nextRaw) ?? .disk
        notifyJS(tier: tier, footprint: currentFootprintMB)
        lastReleasedTier = tier
        lastReleasedAt = now
    }
}

The interesting part is that escalateRelease considers both "what tier did we last release" and "the current physical footprint." When warnings come in rapid succession, starting from Tier 1 every time can never catch up. Conversely, reaching straight to Tier 5 on a long-overdue first warning breaks UX, so a time-based reset is built in.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
A concrete implementation of staged release across 5 priority tiers triggered by iOS didReceiveMemoryWarning
The Instruments Allocations + MetricKit measurement flow that took OOM rate from 4.3% to 0.36%
Tier prioritization heuristics from running wallpaper apps to 50 million cumulative downloads, applicable to Rork-built React Native projects
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

Dev Tools2026-07-10
Adding React Compiler to Expo Let Me Delete 41 Hand-Written memo Calls
I enabled React Compiler on Rork-generated React Native screens and measured the rerender counts with Profiler. Here is how I decided which memo and useCallback calls were safe to delete, how to find the components the compiler bailed out on, and how to catch regressions in CI.
Dev Tools2026-07-10
The Termination That Never Shows Up as a Crash — Reading JetsamEvent in Rork Apps
Crashlytics is silent, yet reviewers write that the app closes by itself. Most of the time the OS killed it for exceeding its memory limit. Here is how to read JetsamEvent reports and design an image-heavy app's memory budget from measured values.
Dev Tools2026-07-06
Migrating Rork Max SwiftUI to @Observable: Narrowing the Re-renders ObservableObject Was Spreading
Rork Max tends to generate SwiftUI apps built on ObservableObject and @Published, where a single state change re-evaluates every subscribing view. Moving to the Observation framework's @Observable narrows invalidation to the property level. Here is the migration path, plus the view-body execution counts I measured in Instruments before and after.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →