Mobile games live and die by performance. A dropped frame during a boss fight gets noticed. A mysterious battery drain gets your game uninstalled. A laggy menu makes players abandon onboarding. When you add a LiveOps SDK into the mix, you are introducing network calls, memory allocations, and background processing into an environment where every millisecond of frame budget counts.
This tutorial walks through the practical steps for integrating the Ilara Unity SDK into a mobile game without sacrificing performance. We will cover memory management, network optimization, caching strategies, and platform-specific considerations for both iOS and Android. Every recommendation here comes from real constraints: the 22 ms frame budget at 30 fps, the 2-6 GB of RAM shared with the OS, and the reality of cellular networks that fluctuate between 50 ms and 500+ ms of latency.
Understanding the Mobile Frame Budget
Before touching any SDK code, it helps to internalize the numbers you are working with. At 30 fps your game has roughly 22 ms per frame. At 60 fps that shrinks to about 11 ms. The Ilara SDK, like any analytics and feature flag SDK, should consume a negligible fraction of that budget. The target is under 0.5 ms of CPU time per frame from SDK operations.
Mobile CPUs also thermal-throttle aggressively. A sustained workload that runs fine for the first five minutes of a play session can cause the CPU to clock down, turning a comfortable frame budget into a missed one. Even "small" inefficiencies compound over time. The SDK needs to be consistently lightweight, not just fast during the first few minutes.
Initializing the SDK Without Blocking the Main Thread
The Ilara SDK uses a singleton pattern with DontDestroyOnLoad, which means it persists across scene loads. The recommended approach is to initialize it in the first scene's Awake() or Start() method so that feature flags and player configuration are ready before the player reaches gameplay.
public class GameBootstrap : MonoBehaviour{void Awake(){class=class="code-string">"code-comment">// IlaraClient initializes from the IlaraConfig ScriptableObjectclass=class="code-string">"code-comment">// and persists across scenes via DontDestroyOnLoadvar client = IlaraClient.Instance;}async void Start(){class=class="code-string">"code-comment">// Identify the player early so flags are bootstrappedclass=class="code-string">"code-comment">// before they reach the main menuawait IlaraClient.Instance.IdentifyPlayer(playerId);}}
Configuration is handled through a ScriptableObject called IlaraConfig, which is the Unity-idiomatic way to expose editor-configurable settings. This avoids hardcoding values and lets you tune parameters like batch sizes and cache durations without recompiling.
Minimizing Memory Allocations and GC Pressure
Garbage collection spikes are the single most common source of frame hitches on mobile. When the managed heap grows and the runtime pauses to reclaim memory, you get a visible stutter. The target is to keep per-frame allocations below 1 KB across your entire game, which means the SDK must contribute zero allocations per frame when idle.
The Ilara SDK takes several steps to achieve this:
- Pre-allocated collections. The EventService maintains a pre-allocated List<AnalyticsEvent> for its event queue rather than creating new collections on each tracking call. This avoids repeated heap allocations as events flow through the system.
- Lock-based thread safety. The event queue uses lock (_queueLock) for thread safety instead of async state machines. This is deliberate: async/await patterns generate state machine allocations on each invocation, while a simple lock is effectively allocation-free.
- Gated debug logging. String interpolation for debug messages is wrapped behind an if (debugMode) check. Without this guard, string allocations happen on every call regardless of whether anyone is reading the logs.
class=class="code-string">"code-comment">// Good: allocation only occurs when debug mode is onif (config.debugMode){Debug.Log($class="code-string">"Ilara: Tracked event {eventName} with {properties.Count} properties");}class=class="code-string">"code-comment">// Bad: string allocation happens every time, even in productionDebug.Log($class="code-string">"Ilara: Tracked event {eventName} with {properties.Count} properties");
Beyond SDK-level optimizations, keep these general rules in mind for your integration code: avoid LINQ in hot paths because it creates iterator and delegate allocations, use StringBuilder for string building in loops, and prefer value types (structs) over reference types (classes) for small, short-lived data structures.
Network Optimization: Batching, Compression, and Retries
Network requests are expensive on mobile. They consume CPU for TLS handshakes, drain battery by activating the cellular radio, and introduce unreliability. The Ilara SDK addresses these costs through its batching system.
Request Batching
Events are automatically batched rather than sent individually. Two triggers control when a batch is flushed:
- Count-based: when the queue reaches eventBatchSize (default: 10 events), a flush fires immediately.
- Time-based: every eventFlushInterval (default: 30 seconds), any pending events are flushed regardless of count.
| Parameter | Recommended Value | Description |
|---|---|---|
| eventBatchSize | 15 | Balance between freshness and efficiency |
| eventFlushInterval | 20s | Seconds between time-based flushes |
| maxOfflineQueueSize | 500 | Cap memory usage during extended offline |
| enableOfflineQueue | true | Re-queue failed events for later delivery |
The batch endpoint (POST /v1/events/batch) accepts up to 1,000 events per request, which dramatically reduces HTTP overhead compared to individual calls. Each separate HTTP request requires a DNS lookup, TLS handshake, and radio wake-up that together add 200-600 ms of overhead. Batching 15 events into a single request means you pay that cost once instead of fifteen times.
Offline Resilience
Mobile players lose connectivity constantly: in elevators, subways, tunnels, or areas with poor coverage. The SDK handles this through its offline queue system. When a flush fails, events are re-inserted at the front of the queue and retried on the next flush cycle. The maxOfflineQueueSize parameter prevents unbounded memory growth during extended offline periods, using FIFO eviction to drop the oldest events first.
For games where no event should be lost, consider persisting the offline queue to disk:
class=class="code-string">"code-comment">// Persist events to Application.persistentDataPath on pausestring path = Path.Combine(Application.persistentDataPath, class="code-string">"ilara_event_queue.json");File.WriteAllText(path, JsonUtility.ToJson(eventQueueWrapper));
This ensures events survive app kills, which happen frequently on mobile when the OS terminates background apps without warning.
Retry Strategy
When network requests fail, not all failures are equal. Server errors (5xx), timeouts, and connection drops are worth retrying. Authentication failures (401) or validation errors (422) are not. For retryable failures, use exponential backoff with jitter:
| Attempt | Base Delay | With Jitter |
|---|---|---|
| 1 | 1.0s | 1.0s + random 0-20% |
| 2 | 2.0s | 2.0s + random 0-20% |
| 3 | 4.0s | 4.0s + random 0-20% |
| 4 | 8.0s | 8.0s + random 0-20% |
| 5 | 16.0s | 16.0s + random 0-20% (capped at 60s) |
The jitter (a random 0-20% offset on each delay) prevents the thundering herd problem, where thousands of players who lost connectivity simultaneously all retry at the same moment.
Caching Feature Flags for Instant Access
Feature flag reads must be instantaneous. Any perceptible delay when checking whether a player should see a promotional banner or an experimental difficulty curve will break the experience. The Ilara SDK handles this with a layered caching approach.
In-Memory Cache
When IdentifyPlayer() is called, the SDK bootstraps all flags for that player in a single bulk request to /v1/flags/evaluate. The results are stored in an in-memory Dictionary<string, FlagEvaluation>. Subsequent reads via GetValue<T>() are synchronous dictionary lookups with zero allocation and zero latency.
The cache TTL is controlled by flagCacheDuration (default: 300 seconds). When the cache expires, the SDK uses a stale-while-revalidate pattern: it returns the cached value immediately and triggers a background refresh. This ensures the game never blocks on a network call for a flag value.
class=class="code-string">"code-comment">// This call is a synchronous dictionary lookup, safe to call in Update()bool showPromo = IlaraClient.Instance.GetFlag(class="code-string">"holiday_promo", defaultValue: false);
Persistent Cache for Cold Starts
The in-memory cache is lost when the app is killed. On a cold start, there is a window between app launch and the first successful API response where no flag values are available. To close this gap, persist flag values to disk:
class=class="code-string">"code-comment">// Save flags to persistent storage after each successful refreshstring flagData = JsonUtility.ToJson(flagCacheWrapper);string path = Path.Combine(Application.persistentDataPath, class="code-string">"ilara_flags.json");File.WriteAllText(path, flagData);class=class="code-string">"code-comment">// Load persisted flags on cold start, before the network call completesif (File.Exists(path)){string cached = File.ReadAllText(path);class=class="code-string">"code-comment">// Populate in-memory cache from disk}
Include a timestamp in the persisted data so you can distinguish between reasonably fresh values and data that is days or weeks old.
Lifecycle Hooks: Flush on Pause, Not Just Quit
On desktop, OnApplicationQuit() is reliably called when the player closes the game. On mobile, the OS can and will terminate your app in the background without calling OnApplicationQuit(). The Ilara SDK hooks into OnApplicationPause(bool paused) to flush all pending events when the app moves to the background. This is one of the most important mobile-specific details to get right.
class=class="code-string">"code-comment">// The SDK handles this internally, but your custom analyticsclass=class="code-string">"code-comment">// wrappers should follow the same patternvoid OnApplicationPause(bool paused){if (paused){class=class="code-string">"code-comment">// Flush everything; we may never get another chanceIlaraClient.Instance.FlushEvents();}else{class=class="code-string">"code-comment">// App resumed: refresh flag cache and re-establish sessionIlaraClient.Instance.RefreshFlags();}}
Polling vs. WebSocket: Choosing the Right Update Strategy
The Ilara execution API supports both WebSocket connections (/ws/flags, /ws/player/{id}) and standard HTTP polling. On mobile, the choice between these has significant battery implications.
If you do use WebSocket on mobile, implement a heartbeat/ping-pong mechanism (every 30 seconds), reconnection with exponential backoff, and graceful degradation to polling if the WebSocket connection fails repeatedly.
Platform-Specific Considerations
For both platforms, the SDK automatically sends Application.platform as an X-Platform header on every request, enabling server-side platform segmentation in the Ilara dashboard.
Profiling Your Integration
After integrating the SDK, validate that it stays within the performance budget. Connect the Unity Profiler to a real device (the Editor has different performance characteristics) and look for these markers:
- Frame time contribution: SDK operations should be under 0.5 ms per frame.
- GC.Alloc: Zero allocations per frame when idle. Any allocation from SDK code during gameplay is a target for optimization.
- Memory footprint: Total SDK heap usage should remain under 1-2 MB (flag cache + event queue + HTTP buffers).
Wrap SDK operations with custom profiler markers for easier identification:
Profiler.BeginSample(class="code-string">"Ilara.EventFlush");await eventService.FlushAsync();Profiler.EndSample();
Establish a performance baseline before SDK integration, then compare after. This gives you hard numbers rather than subjective impressions about whether the SDK is "fast enough."
Summary
Optimizing a LiveOps SDK for mobile comes down to respecting scarce resources: frame budget, battery, and network reliability. Batch your network requests. Cache flag values aggressively with stale-while-revalidate. Eliminate per-frame allocations. Flush on pause. Provide sensible defaults so the game works offline. Profile on real hardware.
The Ilara Unity SDK is designed with these constraints in mind, but how you integrate it matters just as much as how it is built. Use the configuration options (eventBatchSize, eventFlushInterval, flagCacheDuration, maxOfflineQueueSize) to tune the SDK for your game's specific performance profile. Start with the recommended defaults, profile on your target devices, and adjust from there.