RORK LABJP
MAX — Rork Max builds native Swift apps rather than React Native, spanning iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessageAPIS — It reaches native Apple APIs including SwiftUI, ARKit, HealthKit, HomeKit, Core ML, and Metal, so AR/LiDAR scanning and on-device ML are in scopeCLOUD — Rork Max writes Swift, compiles it on cloud-hosted Macs, and streams a live simulator that accepts real touch inputSUBMIT — From there it prepares the build for device install or App Store submissionSEED — Rork raised a $15M seed led by Left Lane Capital in April, joined by Peak XV and a16z SpeedrunPRICE — It's free to start at roughly 5 prompts a week, paid plans begin at $25/month, and Rork Max runs $200/monthMAX — Rork Max builds native Swift apps rather than React Native, spanning iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessageAPIS — It reaches native Apple APIs including SwiftUI, ARKit, HealthKit, HomeKit, Core ML, and Metal, so AR/LiDAR scanning and on-device ML are in scopeCLOUD — Rork Max writes Swift, compiles it on cloud-hosted Macs, and streams a live simulator that accepts real touch inputSUBMIT — From there it prepares the build for device install or App Store submissionSEED — Rork raised a $15M seed led by Left Lane Capital in April, joined by Peak XV and a16z SpeedrunPRICE — It's free to start at roughly 5 prompts a week, paid plans begin at $25/month, and Rork Max runs $200/month
Articles/App Dev
App Dev/2026-07-16Advanced

Placing Native Ads in a Masonry Wallpaper Grid: Designing the Lifetime of an Ad Cell

One native ad in a masonry gallery pushed memory from 180 MB to 420 MB over twenty minutes of scrolling. Here is why cell recycling and ad object lifetime never line up, the pool-based implementation that fixed it, and how I picked the insertion interval from measured numbers.

Rork511Expo144AdMob70Native AdsFlashList5

Premium Article

I added exactly one native ad to a masonry gallery. One slot after every tenth wallpaper. That was the whole change.

The next morning I left Xcode's Memory Report open and scrolled for twenty minutes. Memory climbed from 180 MB to 420 MB and never came back down. Remove the ad and it flattens out around 190 MB. The ad itself was not the problem. My decision to treat the ad as part of a cell was.

Running a handful of wallpaper apps on my own, ad integration is where I stall every single time. What caught me here was that list recycling — a React Native concern — and native ad object lifetime — an ad SDK concern — operate on completely different clocks.

This is how I absorbed that mismatch in the design. The masonry layout itself is covered in Laying Out Variable-Height Images in Two Columns: A Masonry Wallpaper Gallery in a Rork Expo App, so here I will stay on the ads.

Three symptoms, one root cause

They showed up together.

SymptomWithout adsWith ads (naive)
Memory after 20 min of scrolling (iPhone 14, device)Flat around 190 MBMonotonic climb to 420 MB
Dropped frames per minute (60fps target)2–428–41
Impressions / slots reached0.62 (four in ten were blank)

Three separate-looking problems, one cause: I was requesting the ad from inside renderItem.

// The naive version. All three symptoms came from here.
function renderItem({ item }: { item: GridItem }) {
  if (item.type === 'ad') {
    return <NativeAdCell unitId={AD_UNIT_ID} />; // loads inside the cell
  }
  return <WallpaperCell wallpaper={item.wallpaper} />;
}

NativeAdCell loads its ad in its own useEffect. It reads fine. But neither FlashList nor FlatList destroys a cell that scrolls off screen — it reuses the view with different data. So this component mounts and unmounts constantly as you scroll.

Every mount fires a new ad request. Every request produces a new native ad object. Nobody throws any of them away.

Ad lifetime and cell lifetime are not the same clock

This was the part I had wrong.

We write React components assuming they only need to live while they are on screen. Unmount, and they are gone. That is the mental model.

Native ads do not work that way. A GADNativeAd on iOS or a NativeAd on Android survives on the native side even after the JavaScript reference drops. Image assets and click-tracking views stay alive until something explicitly calls destroy(). JavaScript's garbage collector does not sweep the native heap for you.

Worse, an ad takes 400–900 ms to load — longer than it takes a cell to leave the screen. Scroll quickly and the order becomes:

  1. The cell mounts and sends an ad request.
  2. 320 ms later the cell scrolls off and unmounts.
  3. 610 ms later the ad arrives — there is no view left to attach it to.
  4. The ad object stays resident. Nobody calls destroy().

That "0.62 impressions per slot reached" number is step 3 happening forty percent of the time. Ads that loaded, were never seen, and only cost memory.

I started calling this the lifetime gap. A cell lives for a few hundred milliseconds; an ad lives for minutes. Tie them to the same lifecycle and one of them will break.

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
Why list cell recycling and native ad object lifetime run on different clocks, and how to isolate the resulting memory growth
A pool that owns the ads and only lends them to cells, with the full implementation and the rule for when to actually destroy one
Choosing insertion interval, pausing refills during scroll, and sizing ad slots in a masonry layout — all from measured field data
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

App Dev2026-07-07
Laying Out Variable-Height Images in Two Columns: A Masonry Wallpaper Gallery in a Rork Expo App
From why numColumns cannot pack variable-aspect images cleanly, to a dependency-free column-balancing algorithm, to keeping virtualization with FlashList masonry and a pragmatic no-dependency fallback, building a wallpaper gallery with real code.
App Dev2026-07-14
Long-Press Context Menus for a Gallery Item in a Rork Expo App
Long-pressing a wallpaper card does nothing, yet iOS users expect a preview and a menu. From why Pressable alone falls short, to a native context menu with zeego, resolving the scroll-vs-long-press conflict, wiring up save and share, and a custom overlay fallback for Android — all with working code.
App Dev2026-07-05
Building a One-Time Code Field in Expo — SMS Autofill and Segmented Display Together
A six-digit verification screen looks trivial, but once you account for SMS autofill, pasting, and deleting one digit at a time, it needs real care. Here is how to nail the iOS and Android autofill first, then build a segmented look on top of a single TextInput that does not break.
📚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 →