RORK LABJP
PLAY — Google Play's target API level 36 requirement took effect yesterday, August 31. From today, new apps and updates must target Android 16VISIBILITY — Apps still on API 35 stay listed but disappear for users on newer Android versions. No error is raised; new installs simply fade, which makes the change easy to missEXTENSION — If you missed the deadline, an extension through November 1, 2026 can be requested in Play Console — best filed alongside a concrete migration planAPPLE — On the Apple side, the event lands September 9 and iOS 27 is reported to ship September 14. Testing generated apps on iOS 27 hardware before release week is time well spentEXPO — Expo released expo-paste-input on August 28, a native module that brings image, GIF, and sticker paste to React Native TextInputEAS — EAS Observe reached general availability on August 20, putting crash and performance monitoring on the same EAS platform as builds and updatesPLAY — Google Play's target API level 36 requirement took effect yesterday, August 31. From today, new apps and updates must target Android 16VISIBILITY — Apps still on API 35 stay listed but disappear for users on newer Android versions. No error is raised; new installs simply fade, which makes the change easy to missEXTENSION — If you missed the deadline, an extension through November 1, 2026 can be requested in Play Console — best filed alongside a concrete migration planAPPLE — On the Apple side, the event lands September 9 and iOS 27 is reported to ship September 14. Testing generated apps on iOS 27 hardware before release week is time well spentEXPO — Expo released expo-paste-input on August 28, a native module that brings image, GIF, and sticker paste to React Native TextInputEAS — EAS Observe reached general availability on August 20, putting crash and performance monitoring on the same EAS platform as builds and updates
Articles/Dev Tools
Dev Tools/2026-06-01Advanced

Managing Startup Time as a Budget: How I Deferred SDK Init Across Six Apps

Instead of optimizing startup ad hoc, I switched to allocating a per-phase time budget and defending it. Here's how I deferred SDK init across six Rork-built apps and added a CI gate that fails the build when the budget is exceeded.

React Native234Startup TimePerformance25AdMob70Architecture22

Premium Article

It started with a review on my most-downloaded wallpaper app, one of six I run in parallel: "it feels sluggish to open lately." Not a crash. It doesn't fall over, the features work. But the time from tapping the icon to seeing the first wallpaper grid was clearly slow even to me. When I measured it, cold-start TTI (time to interactive) had drifted from 1,500ms six months ago to 2,400ms.

Every feature I'd added had quietly grown the init path: the ad SDK, analytics, remote config, font preloading, restoring favorites. Each one passed review on its own because "it's only a few dozen milliseconds." Across eight years of solo development and over 50 million cumulative downloads, this is the mistake I've repeated most. Every individual decision is sound, but if no one sets a ceiling on startup as a shared resource, the total grows without limit. This is the record of how I turned startup time from something I "optimized" into something I "budgeted and defended."

Why I stopped optimizing and started budgeting

There are plenty of techniques for faster startup. Use Hermes, split the bundle, lazy-load images. I tried them all. But applying techniques one at a time has a weakness: it's fast right after you fix it, and slow again a few months later.

The reason is plain. Optimization removes the slowness you have today; it does nothing to prevent the slowness you add tomorrow. Every feature grows the init path, and within half a year you're back where you started. This happened on roughly the same cycle across all six of my apps.

So I changed the framing. Startup time is a finite shared resource, like CPU time or memory. If that's true, then I should treat it like a household budget: decide a total, allocate how much each task may spend, and stop when something goes over. The moment I shifted from "make it faster" to "keep it within the envelope," operations got dramatically more stable.

I think of my grandfathers, who were temple carpenters. I'm told that joinery isn't done by deciding "I'll spend X minutes shaving this beam," but by fixing the overall dimensions and joints first, then cutting each piece to fit. Startup time is the same: drawing the overall envelope first and then allocating to each task holds up far better over time, in my experience.

Split startup into three phases and allocate a budget

The first thing I did was split startup into three meaningful phases. As long as "startup is slow" stays a single blob, you can't decide what to cut.

The three boundaries I use across all six apps:

  1. Native startup phase: from process creation to the React root view being mounted. App-side JS has almost no control here.
  2. First paint phase: from JS starting to execute, to the first meaningful screen (the wallpaper-grid skeleton) being painted.
  3. Interactive phase: from the skeleton appearing, to actually responding to scroll and taps (TTI).

Then I allocate a budget (a ceiling in ms) to each phase. Here's the allocation I currently use across six apps:

  • Native startup phase: ceiling 700ms
  • First paint phase: ceiling 600ms
  • Interactive phase: ceiling 500ms
  • Total budget: 1,800ms (TTI)

I set 1,800ms as an empirical line — on mid-range physical devices (a Pixel 6a and an iPhone SE 3rd gen in my testing) — just before users start to feel they're waiting. Device performance varies widely, so the essence isn't the absolute number; it's putting a budget into your operations at all. What matters is the constraint it creates: anyone who wants to add new init work (my past self included) must find that time somewhere in this table.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
A way to split startup into three phases and allocate a budget to each, plus the actual table I use across six apps (1,800ms total)
Before/After code that defers AdMob and Crashlytics init past the first frame, with a measured TTI drop from 2,400ms to 1,500ms
A CI threshold gate that stops startup regressions, and why I made over-budget builds actually fail rather than just warn
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $15 for lifetime access
View Membership →

Related Articles

Dev Tools2026-07-24
Collapsing Duplicate Requests Into One: A Reference-Counted Single-Flight Layer
When several components fire the same API call at launch, you get a burst of identical requests. Here is a single-flight layer that shares one in-flight promise instead: how to build the key that decides the folding, the trap of handing out a failed promise forever, the trap of one caller's abort cancelling everyone, and the test that keeps it all from regressing, with the real network numbers alongside.
Dev Tools2026-06-02
Measuring the Real eCPM Yourself: Piping Impression-Level Ad Revenue Into Your Own Analytics
The AdMob dashboard's average eCPM hides which regions and placements actually earn. Here is how I capture per-impression revenue with paidEventListener, normalize it, and pipe it into my own analytics to compute a real eCPM by network, placement, and country.
Dev Tools2026-08-04
Exactly 20 Characters, Still Rejected: Putting Character Counting Behind One Boundary
Emoji and combining marks make your client and your server disagree about length. I measured four counting methods on Node v22 and found 75.6% of naive truncations split a grapheme and 36.1% produced replacement characters over UTF-8. Here is the shared-module design that fixed it.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →