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
Frontend Engineer

How we came to treat preloading as a lifecycle in the recurring ad missions of Hourly Golden Egg (aka PlayHub v2.1)
Introduction
Hourly Golden Egg (aka PlayHub v2.1) is a reward experience embedded in multiple mobile apps where users participate in ads and receive rewards such as points. In one participation flow, users watch ads to collect coins, then open a golden egg after completing the final ad.
Ads do not appear only once in this flow. Users tap the ad participation button each time they collect a coin, then watch one more ad to open the golden egg. If every ad requires a wait of several seconds, each delay may be short, but the repeated interruptions break the overall experience.
At the time, some ad supply paths needed more time than the existing limit to complete bidding and prepare creatives. If we waited long enough, they could return an ad, but many responses arrived too late to be considered. Increasing the timeout for every ad request was not a good answer either, because it would also delay supply paths that were already fast.
The initial question was simple.
Could we reduce the wait by requesting an ad before the user taps the button?
As implementation progressed, however, we learned that preloading was not merely a matter of moving one request earlier. We also had to decide which ads to request in advance, whether a prepared ad still respected the original delivery order, when to count an invisible ad as an impression, and when to discard an unused ad.
The SDK did not officially support preloading. We knew that starting real ad execution early could misalign exposure timing with SDK state, but judged the wait repeated at every ad entry to be the larger product risk. This article is therefore not just about requesting an ad sooner. It is about how we bounded the new responsibilities created by that decision and made the behavior reversible.
This article explains how we separated allocation from SDK initialization, preparation from exposure and counting, and failure from recovery while applying preloading to Hourly Golden Egg's recurring ad missions.
More precisely, this work did not preload every ad in Hourly Golden Egg. It prepared a particular type of SDK ad in advance on the external participation screen. On the frontend, I implemented the flow that requested ad candidates in separate groups and merged them again, the lifecycle that prepared an invisible SDK ad and revealed it when its turn arrived, a local reproduction environment, and post-CTA wait-time instrumentation. I worked with colleagues to align the server contracts for per-request timeouts and ad priority, and to address an SDK cleanup issue discovered after release.
1. Making users wait longer was not an option
Several steps are required before an ad can be shown. We first request candidates from multiple supply paths, determine the priority of the returned candidates, and then prepare the selected creative or SDK. An external ad supplier may run another auction inside that process.
The supply path in question needed more time to finish bidding. A longer timeout would increase its chance of entering the candidate list. In a structure that evaluated ad candidates together, however, increasing the overall timeout could delay the point at which every other supply path was finalized.
There were two broad options.
- Make the user wait longer after tapping the button.
- Start preparing the ad while the user is looking at the screen.
The first option carried a high cost in a recurring reward flow because users enter ads several times while collecting coins and opening the golden egg. Instead of passing the longer bidding time directly to the user, we decided to begin preparation as soon as the product knew that the user's next action would be ad participation.
It was not the technically cleanest choice
We knew that starting the real execution path of an SDK without official preload support could misalign the exposure point with the SDK's internal state. An ad that was invisible to the user might already be running from the SDK's perspective. The impression we controlled could also occur at a different time from the supplier's internal signal. Product code would need to decide when to close an unused hidden session and which session to preserve if the final ad order changed.
The cleaner technical choice would have been to wait until the supplier provided an official preload API, or to exclude the ad from delivery. That would, however, leave the wait users experienced at every ad view untouched. The exposure-timing difference was limited to one SDK and could be coordinated by aligning counting criteria with the ad partner. The wait, by contrast, repeatedly appeared directly in the user experience and interrupted the participation flow. We therefore judged reducing the recurring wait to be the more important product problem at the time.
This does not mean that we had already measured a resulting churn rate. It was a product and business judgment about which risk to reduce first, based on how frequently each problem occurred, how narrowly we could bound it, and whether the decision was reversible.
We therefore did not apply preloading because we were unaware of its technical limitations. We accepted those limitations to address the larger problem first. We limited the rollout to a specific SDK and the external participation screen, made the entire behavior immediately reversible through a feature flag, and observed preparation, exposure, and disposal as separate events. To make a technically imperfect choice defensible from a business perspective, we had to expose its risks, bound them, and retain a rollback path.
The feature flag served as more than an emergency switch. We separated off from the preload-enabled value and evaluated it with unit information. Once the initial code was deployed, the feature could be enabled or disabled for selected units and rollout scopes from the admin interface without another code change. We could start narrowly, expand gradually, and immediately return to the original request-on-CTA path when necessary.
We also recorded the flag value in the event that measured wait time after the CTA. This let us observe non-enabled and enabled modes through the same event schema and supported a gradual rollout from a narrow scope. A feature flag and common event schema do not, by themselves, constitute a causal A/B test; that would require randomized assignment and control of supply conditions. This article therefore does not claim an A/B result or improvement percentage.
The important trigger was not the button itself. A button can be replaced by another component or temporarily disabled as the screen evolves. The product state—"the user's next action is ad participation"—is a more durable contract.
I therefore defined the preload start conditions as follows:
- the user is on the external participation screen,
- server state and user identity information are ready,
- the user is not waiting to return to the app and receive a reward, and
- the next action is watching a coin ad or golden-egg ad.
Preloading was connected to the lifecycle of the product stage rather than the lifecycle of a UI button.
The SDK session starts first, but preload success is not a prerequisite for the product flow. At CTA time the product chooses consumption, waiting, or fallback from the current state, and cleans up unused sessions in both the DOM and the SDK.
2. Fetching ad candidates early was not enough
It may seem that running the existing candidate request earlier would be sufficient. But deciding on an ad candidate and being able to show that ad immediately are different states.
An image or HTML creative can be rendered after its response arrives. Some SDK ads, however, require us to prepare a script, request real ad execution, and wait for the SDK's internal loading to finish. If SDK initialization still begins after the button click, fetching the candidate early does not remove the user's wait.
I therefore divided preloading into two stages.
- Pre-allocate ad candidates: Fetch candidates from multiple supply paths in advance.
- Pre-execute the SDK ad: If an eligible SDK ad is allocated, initialize it while hidden.
SDK-supported preload and our preload were different
We need to distinguish two approaches that share the name "preload."
Some SDKs officially support preloading, such as the Google Ad Placement API. When a product passes the preloadAdBreaks setting, the SDK manages the internal download and cache, while the product calls adBreak() at the actual ad moment. Google's Ad Placement API documentation likewise explains that downloaded and cached ads are used by a later adBreak() call. Download and cache can therefore be handled separately from the API call that initiates exposure.
The SDK in this case had no separate preload API. The only way to prepare an ad was to call the real execution function. We were not asking the SDK to put an ad into a cache. We started the real rendering path, hid the resulting ad frame, and withheld user input and exposure signals. When the ad's turn arrived, we revealed the running session instead of starting a new ad.
| Dimension | SDK-supported preload | Preload by hiding real execution |
|---|---|---|
| How preparation starts | Preload setting or API call | Real ad execution function |
| Screen during preparation | Downloaded and cached inside the SDK | Ad DOM exists but remains hidden |
| Actual exposure | Separate show or break call later | Reveal the running hidden session |
| Lifecycle responsibility | SDK manages download and cache; product decides when to expose | Product code manages DOM, SDK session, and tracking together |
| Representative risks | Calling too early, misreading availability, cache expiry | Early impression, session collision, cleanup omissions, hidden resource use |
With official preloading, the product decides when to expose while download and cache stay within the SDK contract. With hidden execution, product code directly manages the intermediate state from execution through reveal or disposal and SDK termination.
Using an SDK without a preload API still let us pay the rendering cost before the user clicked. If the ad was eventually selected, we could reveal an already prepared screen. From the SDK's perspective, however, it might be "running an ad" rather than "preparing one." Product code had to create the distinction between preparation and exposure, and explicitly terminate unused sessions.
That also introduced structural waste: the SDK ad prepared first was not always the ad ultimately exposed. We could begin hidden execution when the target appeared in the SDK group, then choose a different, higher-priority ad after merging the final candidate list. In that case, the prepared session had to be discarded. Selecting a preload target and selecting the final delivery candidate were separate decisions, which is why unused sessions and cleanup responsibility existed at all.
This implementation did not call an SDK-provided preload operation. It created the effect of preloading by safely hiding real execution and consuming it later. The SDK session cleanup and exposure-counting problems described below both followed from this distinction.
SDK ads and image, HTML, or video ads differ in how they are prepared and in how long their supply paths can spend bidding. Rather than combine them into one request, we requested SDK and non-SDK groups in parallel. This let non-SDK ads use a longer bid time while SDK responses could begin initialization as soon as they arrived.
We did not add a separate concept of a "preload ad" to the server. Instead, we extended the existing ad lookup contract so it could accept the ad format, desired number of candidates, and per-request time limit. The frontend used the same API at a different point—before the user's click.
This kept the server responsible for selecting ordinary ad candidates while the frontend managed when to request and consume them for the current product stage.
3. Arrival order could not replace ad priority
Splitting candidates into two request groups introduced another problem. SDK and non-SDK responses arrived at different times. If we showed whichever group arrived first, we could violate the priority established by the server.
Preloading was meant to prepare ads sooner, not to favor a particular supplier.
Each ad response already carried the priority value used by the delivery policy. I re-sorted the responses obtained from both groups before the deadline using that value, then merged them into one candidate list. Only ads with the same priority were placed in a stable randomized order within a participation cycle.
We also had to decide how long to wait. If one group remained pending indefinitely, preloading could prevent ad participation from starting at all. After a fixed deadline, we finalized the current candidate list from the groups that had completed.
That required distinguishing three kinds of time limits:
- how long a supply path may spend bidding,
- the deadline for including a result in the current candidate list, and
- when the browser aborts the network request itself.
These are not the same. Passing the candidate-list deadline does not make the network request disappear immediately. A late SDK response was not inserted back into the finalized order. While the same participation cycle remained valid, it could still be observed as input to hidden preparation, but if it could not be consumed from the finalized list it was ultimately discarded.
We did not guarantee that splitting requests would produce the same candidate set as a single request; group deadlines could change which candidates arrived in time. The contract we preserved was narrower: arrival order would not replace the existing priority value among candidates included in the current list.
The timeout was not merely "failure after N seconds." It defined how far a late result was allowed to travel through the decision process.
4. We deferred the exposure signals under our control until reveal
Preparing an SDK ad before the button click required beginning some part of real execution. The frame was invisible and could not receive user input, but the SDK had already started loading the ad.
If we did not distinguish preparation from actual visibility, exposure counting would drift.
We therefore withheld the following signals during preload:
- the service's ad impression tracker,
- the frontend exposure event, and
- the impression signal explicitly sent to the SDK.
We retained those signals until the candidate was actually selected and revealed, then sent them together.
hidden preload: prepare SDK ad → defer exposure signals- Actual ad turn: reveal hidden session → send deferred exposure signals
We did not assume that the frontend could control every internal measurement performed by the external SDK. We separated the service exposure point we controlled from the SDK's black-box behavior, and defined operational metrics around the point at which the service actually revealed the ad.
We aligned the service-level meaning of exposure with the actual reveal. We did not assume that the frontend also controlled every impression or billing decision inside the supplier SDK. That boundary remained a separate risk to align with the partner and verify against operational data.
5. The original path still had to work when preloading did not finish
Users do not wait for preloading to finish before pressing a button. On a slow network they may tap the CTA while preloading is still in progress, an eligible ad may never be allocated, or SDK initialization may fail.
We did not make successful preloading a prerequisite for the product flow.
At CTA time, the flow selected one of three states.
| State | Handling |
|---|---|
| Ad candidates and SDK are ready | Consume the prepared list and session |
| Preloading is still in progress | Wait until the defined candidate-finalization point, then consume the result or fall back |
| No prepared result or preparation failed | Fall back to the original request-on-CTA flow |
If the selected SDK ad ended early, we tried the next candidate. If every candidate failed or no ad was available, we preserved the existing NoFill participation contract. A preload failure therefore did not escalate into an ad participation or reward failure.
Turning the feature flag off returned the entire request, exposure, and reward flow to the original request-on-CTA behavior. A performance feature needs not only a successful path but also a way to bypass the whole feature immediately.
6. Removing the frame did not terminate the SDK
The initial implementation also created new failure paths.
With a contract such as Google's, which separates preparation from exposure, the API manages the internal lifecycle of downloading and caching. Product code connects the preload configuration to the actual exposure call. We, however, were starting real ad execution and hiding it. That meant we had to manage the SDK session and the DOM even while the ad was invisible. This structural difference caused the first operational problem.
We initially assumed that removing an unused hidden ad frame from the DOM would also end the session. The external SDK still retained an "ad running" state. When we attempted to execute another ad, the SDK rejected it because an ad was already in progress.
For users, the ad never appeared. The loading screen remained until the flow moved to a skip prompt.
The team's post-release analysis separated cleanup into two responsibilities.
- Remove the hidden ad frame from the screen.
- Explicitly terminate the SDK session that is still running.
Conversely, when the same ad was allocated again under a new allocation identifier, we did not always close and restart the session. If the ad and ad network were the same, we reused the running session and updated the identifiers used to locate it on screen. Exposure tracking retained the context of the ad that had actually started rendering. Closing and loading the same prepared ad again could otherwise cause a visible flicker.
This led to one important rule.
The lifetime of a UI resource is not the lifetime of an external SDK session.
Cleaning up a React component and its DOM does not imply that the state of an external system has also been cleaned up.
7. We also changed what to measure
Moving the ad request to page entry does not, by itself, prove that the user experience improved.
The purpose of preloading was not to reduce page load time. It was to reduce the time a user waited for an ad after tapping the CTA. We therefore recorded a single outcome when one of the following occurred:
- the ad appeared on screen,
- the SDK ad started,
- the product moved to a no-ad notice,
- an alternative CTA appeared after a fixed delay, or
- the user closed the ad screen.
Each outcome included elapsed time since the CTA, the original feature-flag value, the resolved mode, whether a preloaded candidate was used, and whether a hidden SDK session was used. The same event schema let us split non-enabled and enabled modes. Ad exposure, NoFill, an alternative CTA, and user closure are different outcomes, however, so they must be interpreted separately rather than combined into one wait-time distribution.
Observability was not an accessory to the product code; it was part of the feature. A feature that separates request time from consumption time scatters one user action across multiple asynchronous operations. To reconstruct a failure, we needed to connect the user's participation, the ad request, the candidate, and the SDK session in one context.
This article does not claim that wait time improved by a specific percentage. We verified the implementation and measurement structure, but we could not establish comparable before-and-after operational data as evidence for this article. What we could verify structurally was that the original participation path remained available regardless of preload success, service exposure signals were withheld until reveal, and unused SDK sessions could be terminated.
How our view of preloading changed
At first, I saw ad preloading as a performance optimization that moved a network request earlier.
Implementation and operations changed the criteria I use.
- Choose the preload point from product state rather than a UI event.
- Treat ad allocation and SDK preparation as separate stages.
- Do not let arrival order replace the existing delivery priority.
- Separate preparation from actual exposure and counting.
- Preserve the original participation flow when preloading fails.
- Measure success by the user's wait, not by when the request started.
- Distinguish SDK-supported preloading from the responsibilities of preloading by hiding real execution.
This choice was defensible while the repeated user wait was substantial, the rollout could be limited, and the feature flag provided an immediate rollback. It stops being defensible when supplier-counting discrepancies keep growing, unused-session and exception-handling costs slow feature development, or an official preload contract becomes available.
Hidden sessions, deferred exposure signals, reallocation, and disposal logic built on an SDK without preload support are all complexity that product code has taken on. When those signals appear, the product should move to an official preload contract, improve the supplier agreement, and remove the lifecycle compensation logic from product code.
I consider this deliberate technical debt. Technical debt does not simply mean untidy code. It is the decision to borrow future maintenance cost for present product value while understanding that cost. The important thing is not to pretend that no debt was created, but to record why we accepted it, which risks we would observe, and under what conditions we would repay it.
The idea that showing an ad sooner simply requires starting the work sooner is only half right. The earlier work begins, the more intermediate states we must manage: unused ads, late responses, and external SDK sessions.
Ultimately, the central question of preloading was not "How early did we send the request?"
Which participation flow is this not-yet-exposed ad being prepared for, when should it become an actual ad, and how should we end it safely if it is never used?
Only when we can answer that question can preloading become a product feature that reduces user wait rather than a request sent early.


