Skip to content
:tech
← Articles

AI-translated. Read the original in KoreanEmbedded slides, videos and images remain in their original language.

What We Learned Applying Preloading to an Ad SDK Without a Preload API

Justin Jeong
Justin Jeong

Frontend Engineer

What We Learned Applying Preloading to an Ad SDK Without a Preload API

To shorten the wait that came before every ad in Hourly Golden Egg (aka PlayHub v2.1), we started real ad execution early in an SDK that had no preload API, and kept it hidden. The price was having to deal with ads that we had removed from the screen but that were still running inside the SDK.

Introduction

Hourly Golden Egg is a web-based reward service that opens inside multiple mobile apps, where users participate in ads and receive rewards such as points. In one participation flow, users tap the ad participation button (CTA) each time they collect a coin, then watch one final ad to open the golden egg. If every ad comes with a wait of several seconds, each wait may be short, but the flow keeps getting interrupted.

At the time, some ad supply paths took longer than the existing time limit to complete bidding and prepare creatives, so ads that could have been shown given enough time were often dropped from the candidates. Yet in a structure that evaluated all candidates together, raising the time limit across the board would have made ads that were already ready quickly wait as well.

Could we reduce the wait by requesting an ad before the user taps the button?

Once we started implementing it, however, preloading turned out to be more than moving a single request earlier. We also had to decide which ads to request in advance, whether a prepared ad still matched the delivery order, when to count an impression for an ad that was not visible, and when to discard an ad that went unused. The SDK of the ad we wanted to prepare in advance did not officially support preloading. We knew that starting real ad execution early could misalign the exposure point with the SDK's state, but we judged the repeated wait to be the larger product risk.

1. Making users wait longer was not an option

To show an ad, we request candidates from multiple supply paths, rank the candidates that respond, and then prepare the creative or SDK of the selected ad. An external ad supplier may also run its own auction within that process.

Rather than make users wait longer after tapping the CTA, we decided to start preparing while they were looking at the screen.

We set preloading to start when the screen where users watch ads appears in the product flow. Starting preloading earlier than necessary can leave a prepared ad unused. To start preloading as early as needed to secure preparation time without wasting it, we defined the start conditions as follows:

  • the user is using the product,
  • server state and user identity information are ready, and
  • the next action is watching a coin ad or golden-egg ad.

2. Fetching ad candidates early was not enough

Having a candidate decided and being able to show the ad immediately were different states. Image or HTML creatives can be rendered once the response arrives, but some SDK ads required us to prepare a script, request ad execution, and then wait for the SDK to finish its internal loading. Even if candidates are fetched early, the wait remains if SDK initialization only starts after the click. So I split preloading into two stages: pre-allocation, which fetches candidates in advance, and pre-execution, which starts initialization in a hidden state once an eligible SDK ad is allocated.

Without a preload API, we had to hide the real execution

The Google Ad Placement API, which officially supports preloading, by default prefetches and caches ads based on heuristics and uses them for adBreak() calls. Calling adConfig({preloadAdBreaks: 'on'}) before the first adBreak() ensures that an ad is ready before that adBreak(), and any attempt to change the setting after it has been set is ignored (Google documentation).

The SDK in our case had no such API, so the only way to prepare an ad was to call the real ad execution function. We started the real rendering path first, hid the resulting ad frame with visibility: hidden so it would not receive user input, and withheld exposure signals. From here on, we call a session that is running while hidden in this way a "hidden session," and making that session visible when the ad's actual turn comes, instead of starting a new ad, "revealing" it.

DimensionUsing a preload APIPreloading by hidden execution
How preparation startsThe SDK starts it automatically, or it is requested through a preload setting or APICall the real ad execution function
State during preparationDownloaded and cached inside the SDKAd DOM is created but hidden
Actual exposureA separate exposure call laterReveal the running hidden session
Lifecycle responsibilitySDK handles download and cache; product decides when to exposeProduct code manages DOM, SDK session, and tracking together
Representative risksConstraints on when settings can be made, ad not ready before first exposure, expiry of prefetched ads (e.g., AdMob app open ads)Early impression counting, collisions with a running session, missed cleanup, hidden resource use
A comparison: with SDK-supported preload the product decides when to expose while the SDK manages download and cache, whereas with hidden execution product code is responsible for execution, hiding, final selection, reveal or disposal, and SDK termination

Hidden execution completes script preparation and internal loading before the click, so a prepared ad can be revealed right away. However, a box with visibility: hidden takes part in layout but is not painted (CSS Display specification), so the painting work still remains for the moment of reveal. In a hidden <iframe>, some browsers also pause requestAnimationFrame() calls (MDN).

It was not the technically cleanest choice

We could have waited for official preload API support to ship. But it was hard to predict how long that feature would take to prepare, and there was no release plan for it. Removing the ad from the delivery candidates would have eliminated that SDK's initialization wait, but at the cost of the opportunity to show that ad and the revenue it brings. The exposure-timing difference, on the other hand, was limited to a specific SDK, and we judged it to be a problem that could be adjusted by aligning counting criteria with the supplier. So we decided to first reduce the wait that users hit again and again and that interrupted the participation flow. It was a business judgment about which risk to reduce first, based on how often each problem recurred, how controllable its impact was, and whether the decision was reversible.

In exchange, we limited the scope to a specific SDK, made the behavior immediately reversible with a feature flag, and observed preparation, exposure, and disposal as separate events.

This approach has costs beyond the difference in when ads are prepared. An ad that is discarded after losing on priority can end without ever being shown to the user, even though it started the real rendering path. How suppliers treat such ads in terms of counting and billing, or traffic quality, is outside the frontend's control. We weighed this cost as part of the decision as well.

A preloaded ad uses network and device resources even while invisible, and if it is discarded unused, that usage is simply wasted. We therefore limited the number of hidden sessions kept alive at any one time to one, and cleaned them up when the external browser went to the background or the user left the page.

3. We held back the exposure signals the service controls until reveal

If we did not distinguish between what we had prepared and what we had shown, exposure counting would drift, so we did not immediately send the following signals generated during preloading:

  • the service's ad impression tracker,
  • the frontend exposure event, and
  • the impression signal explicitly sent to the SDK.

We held these signals until the ad was selected from the final list and revealed, then sent them together. We also based operational metrics on the moment the service actually revealed the ad. We agreed on the hidden execution approach and the counting criteria with the supplier before release. However, we did not consider impression counting or billing decisions inside the supplier SDK to be under the frontend's control. That boundary remained a separate risk to be verified by reconciling operational data, and within the range we reconciled, no discrepancy was observed.

4. The original path still had to work when preloading did not finish

Users can tap the CTA while preloading is still in progress, and ad allocation or SDK initialization can fail. We did not make preload success a prerequisite for the product flow; instead, we handled each case according to the state at the moment the CTA was tapped.

StateHandling
Ready (READY): candidates and SDK are readyConsume the prepared list and session
Preparing (PREPARING): preloading is in progressWait until the defined candidate-finalization point, then consume the result or fall back
Failed (FAILED): no prepared result, or preparation failedFall back to the original request-on-CTA ad request
// 단순화한 흐름(의사코드)
async function onAdCtaClick(preload) {
  switch (preload.state) {
    case 'READY':
      return consume(preload.result);
    case 'PREPARING': {
      const result = await preload.waitForCandidateList(); // 정해 둔 후보 확정 시점까지(기존 제한 시간 안에서)
      return canConsume(result) ? consume(result) : requestAdsAtCta(); // 폴백은 남은 시간 안에서
    }
    default: // 준비된 결과가 없거나 실패함
      return requestAdsAtCta();
  }
}

Users can tap the CTA even while preloading is in progress, and the ad is shown as soon as it is ready.

A lifecycle that prepares ad candidates and a hidden SDK session before the click, then chooses reveal, wait, or fallback based on the READY, PREPARING, or FAILED state at CTA time

We also needed a recovery path that bypassed the feature entirely. Turning off the feature flag returns the whole ad request, exposure, and reward flow to the original request-on-CTA behavior. We split the flag values into off and the preload-enabled value, and had them evaluated together with unit (ad placement) information. After deployment, we could select units and rollout scope and turn the feature on and off through admin settings alone, without code changes, expanding it gradually from a small scope.

5. Removing it from the screen did not end it in the SDK

We assumed that removing an unused hidden ad frame from the DOM would also end the session. Inside the external SDK, however, the ad was still in a running state, and when we requested execution again while preparing the next ad, the SDK rejected the request because an ad was already in progress. Users saw a loading screen with no ad, which eventually moved on to a skip prompt. This problem, which surfaced in observability events after release, affected the entire rollout scope that was enabled at the time. Because the participation and reward flow still continued after the skip prompt, however, we responded with a code fix rather than turning off the feature flag.

The team's follow-up analysis split cleanup responsibility into two steps.

  1. Remove the hidden ad frame from the screen.
  2. Explicitly terminate the still-running SDK session using the SDK's termination API.

We also added a regression test that reproduces the same situation. We should not have assumed that cleaning up a React component and its DOM also cleaned up the state of an external system.

Conversely, when the same ad came down again with a new allocation identifier, we did not close the session if the ad itself and the ad network were the same. We reused it and only updated the identifying information used to locate the session on screen to match the new allocation. Starting a new session could have caused the already prepared ad to load again and the screen to flicker. Exposure tracking kept the context of the ad that had actually started rendering, so the deferred exposure signals were also sent for the allocation that was rendered first.

6. We also redefined what to measure

The goal was not to move requests earlier but to reduce how long users waited for an ad after tapping the CTA, so we made it possible to record the following outcomes against a single standard:

  • the ad appeared on screen,
  • the SDK ad started,
  • the product moved to a notice screen because no ad was available,
  • an alternative CTA appeared after a fixed delay, or
  • the user closed the ad screen.

With each outcome we recorded the elapsed time since the CTA, the raw feature-flag value and the mode actually applied, and whether the allocated ad list and the preloaded ad were used. This let us compare results with and without preloading within the same event schema and use them as grounds for expanding the rollout. Ad exposure, NoFill, an alternative CTA, and user closure mean different things, however, so they had to be interpreted by type rather than mixed into a single wait-time distribution.

Once requests and consumption are decoupled, a single user action is scattered, so to reconstruct a cause we needed to be able to link the participation, the ad request, the candidate, and the SDK session in one context.

Measurement scope and limitations

In the same period (October 22–28, 2026), we compared non-SDK ads shown from preloaded candidates with those shown without them. The metric is the time from the CTA tap until the ad is displayed.

p50p75p90
Without preloaded candidates471ms1,161ms1,997ms
With preloaded candidates24ms43ms774ms

Compared with the period before the rollout (July 2–8, 2026), the direction was the same. The time from the CTA tap until the ad modal switched to its showing state fell from 708ms to 74ms at p50 and from 893ms to 445ms at p75, while p90 stayed at 1,352ms. This metric marks the modal transition, which comes earlier than the moment an SDK ad is actually rendered.

Hidden execution (SDK ads). The clearest gain showed up on SDK ads. Without hidden execution, these ads were not ready even after 10 seconds, and users saw a fallback CTA instead of the ad. With hidden execution, the ad appeared for 61% of sequences at a median of 5.5 seconds after the CTA tap.

Ad ready within 10 secondsWait until ready (median)
Without hidden execution0%Fallback CTA after waiting more than 10 seconds
With hidden execution61%5.5 seconds (p90 8.7 seconds)

For sequences that became ready, the median wait fell by at least 4.5 seconds, close to half. The original wait exceeded 10 seconds, so the actual reduction is larger. This SDK does not report when an ad is painted on screen, so we measured against its ready signal. Readiness does not fully complete while the session is hidden, which leaves about 4 seconds after the reveal; shortening that is the next improvement.

How our criteria for preloading changed

Half-measure preloading was justified while the repeated wait was judged to be significant, the scope could be limited, and the feature flag allowed immediate rollback. Conversely, if discrepancies with supplier counting appear and grow large enough to matter, if the cost of exception handling holds back feature expansion, or if an alternative that supports an official preload API becomes available, there is no reason to keep this temporary structure.

Product code took on the complexity of hidden sessions, deferred exposure signals, and reallocation and disposal logic in the SDK's place. I see this as closer to deliberately chosen technical debt. When the conditions above appear, we should move to an official preload API or improve the integration terms with the supplier, and then remove the compensating logic from product code. What matters, I think, is not pretending the debt does not exist, but knowing why we accepted it, what we will observe, and when we will repay it.

The idea that showing ads sooner simply means starting the work sooner was only half right. The earlier the work starts, the more intermediate states there are to manage, such as unused ads, late responses, and external SDK sessions. Only when we can answer which participation each ad was prepared for, when it will be revealed, and how to end it safely if it goes unused can preloading go beyond simply requesting early and become a product feature that reduces wait time.

For an article on the same product that deals with waiting on screen assets rather than ad preparation, see Video Optimization Was Never Just About the Video.

All articles