DEV Community

137Foundry
137Foundry

Posted on

The Real Cost of Letting Every SDK Initialize on the Main Thread

I audited a client's app startup sequence last quarter and counted eleven third-party SDKs initializing synchronously before the first screen rendered. Analytics, two crash reporters (don't ask), a feature flag service, an A/B testing tool, a push notification provider, a deep link handler, and a few others nobody could immediately explain. Individually, each one added twenty to sixty milliseconds. Together, they added almost a full second to every cold start.

Nobody added all eleven at once. Each one got added by a different engineer, at a different time, for a reason that made sense in isolation. Nobody ever sat down and looked at the whole list together until I asked them to.

Terminal screen showing code in monospace font
Photo by Max Chen on Pexels

Why this happens on every team eventually

Adding an SDK is a five-minute task: install the package, call .initialize() in your Application or AppDelegate class, done. It works, it's easy, and the cost is invisible in isolation because sixty milliseconds doesn't show up as a bug report. Nobody files a ticket that says "startup got 60ms slower." They file one two years later that says "the app feels sluggish to open," after a dozen of these additions have compounded.

This is a classic case of a cost that's individually negligible and collectively significant, and the reason it survives so long is that no single commit is ever the obviously guilty one.

The three questions worth asking about every SDK

For each third-party SDK your app depends on, three questions sort it into a bucket that tells you what to do:

  1. Does this need to be ready before the very first frame renders? For almost everything, the honest answer is no. The user doesn't need analytics tracking, crash reporting, or feature flags to be fully initialized before they see a screen.
  2. Does this need to be ready before the user's first interaction? More things land here, but it's still a smaller list than teams assume. A feature flag service, for instance, usually just needs a cached previous value available, with a fresh fetch happening in the background.
  3. Is this only needed for a specific feature the user might not even use this session? Anything in this bucket should initialize lazily, on first use of that feature, not at app launch regardless of whether the user ever touches it.

Running every SDK through these three questions typically moves 70 to 80 percent of them out of the synchronous startup path entirely.

What deferred initialization actually looks like

The mechanical fix is straightforward once you know what to defer: move the initialization call from your Application/AppDelegate startup method to run after the first frame has rendered, using whatever "post first frame" hook your platform provides, or lazily on first access via a simple lazy-initialization wrapper.

For SDKs that need to buffer events before they're initialized (a common analytics pattern, where events fired before .initialize() would otherwise be silently dropped), most SDKs have a queuing mode built in specifically for this. Check the SDK's own documentation before assuming you need to build a custom event buffer; most mature libraries anticipated this exact problem.

The crash reporter exception, and why it's smaller than it looks

The one SDK category where "just defer everything" doesn't cleanly apply is crash reporting, because a crash during the deferred-initialization window itself wouldn't get caught. In practice this window is small (milliseconds, not seconds) and the trade-off is almost always worth it: a marginally higher chance of missing a crash report during a tiny startup window, in exchange for a measurably faster launch for every single user, every single time. If your crash reporter genuinely needs to be first, initialize only that one SDK synchronously and defer the rest.

Measuring the actual savings

Before and after this kind of audit, measure cold start time on a mid-tier device, not your own dev phone. Android's official developer resources document the am start -W command for a quick baseline number, and the built-in CPU profiler for a deeper look at exactly which initializer is taking how long. On the client project I mentioned, deferring ten of the eleven SDKs (keeping only the crash reporter synchronous) cut roughly 700 milliseconds off cold start on a three-year-old mid-range Android device. Nothing about the app's functionality changed. Nothing was removed. The only change was when each thing ran.

Why nobody notices until someone measures

The uncomfortable part of this audit is that it usually reveals SDKs nobody on the current team can explain. A push provider from a feature that got cut eighteen months ago. An A/B testing tool for a test that concluded a year ago and was never removed. These aren't hypothetical; they show up on almost every app that's been in active development for more than two years without a dependency audit.

Firebase's performance monitoring tooling is a reasonable way to get visibility into which specific SDK calls and traces are actually costing time in a shipped build, which is a faster path to finding these than manually auditing every dependency by hand, especially on a codebase with unclear ownership history.

Watching the number so it doesn't come back

Once you've done the audit and deferred what can be deferred, set up ongoing monitoring so the gains actually stick instead of quietly eroding over the next year of feature work. Sentry's mobile performance monitoring can track cold start duration automatically across real user sessions, broken down by percentile, which catches a slow regression from a newly-added SDK within days instead of months. Alerting on the p90 number specifically matters here, because a single new SDK added by one team rarely moves the average enough to notice, but it shows up clearly in the tail once you're actually looking at it.

The audit is cheap; the drift back is not

Doing this audit once is a day or two of work for a mid-sized app. The harder part is keeping the gains, because the same pattern that created the problem (each individual addition looking harmless) will happily recreate it over the following year unless something changes about how new dependencies get added.

The fix that actually sticks: a short checklist item in your PR template asking whether a new dependency initializes synchronously at startup, and if so, why it can't be deferred. This is a five-second question for the reviewer to ask and it catches the problem at the moment of introduction instead of two years later during an emergency performance audit.

We've also found it useful to name an explicit owner for the startup path, even on small teams. Not a full-time role, just one engineer who's responsible for knowing what runs during application initialization and pushing back when a new dependency wants to join that list without a clear reason. Without a named owner, this kind of drift has no natural point of resistance, because each individual addition is easy to justify in isolation and nobody feels specifically responsible for the aggregate cost.

I wrote up the full picture of where cold start time goes, including the SDK piece and several other bottlenecks, in this longer guide on reducing mobile cold start time if you want the complete before-you-start-fixing checklist.

If there's one takeaway worth remembering past this specific audit, it's that startup cost is cumulative and nobody notices the individual additions. Revisit the list periodically, not just when someone finally complains. More of our production performance work is at 137Foundry.

Top comments (0)