Tutorial

Unity SDK Deep Dive: Optimizing for Mobile

Performance tips and best practices for using Ilara in Unity mobile games.

MC
Marcus Chen/January 2, 2025/9 min read
60fps

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.

0 ms
Frame budget at 30 fps
0 ms
Frame budget at 60 fps
0 ms
SDK target per frame
SDK CPU Budget as % of Total Frame Time
30 fps
2.3%
60 fps
4.5%

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.

GameBootstrap.cs — SDK Initialization
csharp
public class GameBootstrap : MonoBehaviour
{
void Awake()
{
class=class="code-string">"code-comment">// IlaraClient initializes from the IlaraConfig ScriptableObject
class=class="code-string">"code-comment">// and persists across scenes via DontDestroyOnLoad
var client = IlaraClient.Instance;
}
 
async void Start()
{
class=class="code-string">"code-comment">// Identify the player early so flags are bootstrapped
class=class="code-string">"code-comment">// before they reach the main menu
await IlaraClient.Instance.IdentifyPlayer(playerId);
}
}
Initialization Order
Always call IdentifyPlayer() before any tracking or flag evaluation operations. The SDK enforces this with internal EnsureInitialized() checks, but calling operations in the wrong order will silently fail rather than crash. This can lead to confusing debugging sessions where events seem to vanish.

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:

Debug Logging — Good vs Bad
csharp
class=class="code-string">"code-comment">// Good: allocation only occurs when debug mode is on
if (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 production
Debug.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:

ParameterRecommended ValueDescription
eventBatchSize15Balance between freshness and efficiency
eventFlushInterval20sSeconds between time-based flushes
maxOfflineQueueSize500Cap memory usage during extended offline
enableOfflineQueuetrueRe-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:

Persisting the Offline Event Queue
csharp
class=class="code-string">"code-comment">// Persist events to Application.persistentDataPath on pause
string 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:

AttemptBase DelayWith Jitter
11.0s1.0s + random 0-20%
22.0s2.0s + random 0-20%
34.0s4.0s + random 0-20%
48.0s8.0s + random 0-20%
516.0s16.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.

Reading a Feature Flag
csharp
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);
Always Provide Defaults
Always provide a sensible default value. If the SDK cannot reach the server and no cached value exists, the default is returned. This guarantees the game remains playable regardless of network conditions.

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:

Persisting and Restoring Flag Cache
csharp
class=class="code-string">"code-comment">// Save flags to persistent storage after each successful refresh
string 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 completes
if (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.

OnApplicationPause — Flush and Refresh
csharp
class=class="code-string">"code-comment">// The SDK handles this internally, but your custom analytics
class=class="code-string">"code-comment">// wrappers should follow the same pattern
void OnApplicationPause(bool paused)
{
if (paused)
{
class=class="code-string">"code-comment">// Flush everything; we may never get another chance
IlaraClient.Instance.FlushEvents();
}
else
{
class=class="code-string">"code-comment">// App resumed: refresh flag cache and re-establish session
IlaraClient.Instance.RefreshFlags();
}
}
Critical Mobile Detail
Treat OnApplicationPause(true) as your last guaranteed opportunity to send data. Design your integration around this assumption, and you will never lose events to app kills.

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.

HTTP Polling (Recommended)
Make a standard HTTP request every 5-10 minutes to refresh flag values. The connection opens, transfers data, and closes. The cellular radio powers down between requests. Simple, reliable, and easy on the battery.
WebSocket (Opt-in)
A persistent connection provides sub-second flag updates and real-time notifications. However, it keeps the cellular radio active continuously, draining battery even when no data is transmitted. Use only when your game genuinely needs real-time updates, such as during live competitive events.
Relative Battery Impact (Lower is Better)
Polling
15%
WebSocket
72%

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

iOS
IL2CPP is mandatory on iOS, meaning all C# code is AOT-compiled. Enable code stripping in Player Settings to minimize binary size, but use a link.xml file to preserve SDK types accessed via reflection (AnalyticsEvent, FlagEvaluation). iOS is aggressive about killing background apps — never rely on OnApplicationQuit() being called.
Android
Android offers both IL2CPP and Mono backends. Use IL2CPP for production builds (better performance, smaller binary) and Mono for development iteration (faster build times). Be aware of Doze mode (Android 6+), which restricts network access when the device is idle. The offline queue becomes essential here.

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:

Wrap SDK operations with custom profiler markers for easier identification:

Custom Profiler Markers
csharp
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.

Get Started

Stay in the loop.

Get weekly insights on game LiveOps, AI, and player retention delivered to your inbox.

No credit card10 min setupSOC 2

We respect your privacy. No spam, ever.