RORK LABJP
PLAY — Google Play's target API level 36 requirement took effect yesterday, August 31. From today, new apps and updates must target Android 16VISIBILITY — Apps still on API 35 stay listed but disappear for users on newer Android versions. No error is raised; new installs simply fade, which makes the change easy to missEXTENSION — If you missed the deadline, an extension through November 1, 2026 can be requested in Play Console — best filed alongside a concrete migration planAPPLE — On the Apple side, the event lands September 9 and iOS 27 is reported to ship September 14. Testing generated apps on iOS 27 hardware before release week is time well spentEXPO — Expo released expo-paste-input on August 28, a native module that brings image, GIF, and sticker paste to React Native TextInputEAS — EAS Observe reached general availability on August 20, putting crash and performance monitoring on the same EAS platform as builds and updatesPLAY — Google Play's target API level 36 requirement took effect yesterday, August 31. From today, new apps and updates must target Android 16VISIBILITY — Apps still on API 35 stay listed but disappear for users on newer Android versions. No error is raised; new installs simply fade, which makes the change easy to missEXTENSION — If you missed the deadline, an extension through November 1, 2026 can be requested in Play Console — best filed alongside a concrete migration planAPPLE — On the Apple side, the event lands September 9 and iOS 27 is reported to ship September 14. Testing generated apps on iOS 27 hardware before release week is time well spentEXPO — Expo released expo-paste-input on August 28, a native module that brings image, GIF, and sticker paste to React Native TextInputEAS — EAS Observe reached general availability on August 20, putting crash and performance monitoring on the same EAS platform as builds and updates
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.

Rork547Expo193AdMob70Native 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 $15 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-08-23
Bumping targetSdk to 36 surfaced a 16 KB warning. These are two different deadlines
The August 31 target API 36 requirement and the February 1, 2027 16 KB page size requirement are separate conditions. Here is how to inspect your AAB without installing the NDK, and why a LOAD misalignment and a zip boundary miss need completely different fixes.
App Dev2026-08-21
Apple's automated pass reads what your purpose strings are for, not whether they exist
Submit a Rork or Expo iOS build and you may get it back under Guideline 5.1.1 with a note about placeholder or otherwise insufficient purpose strings. Here is how to read a rejection that names no key, and how to rewrite the strings from app.json.
📚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 →