◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-04-22Intermediate

Practical Techniques to Cut the Startup Time of Rork-Generated Apps

If a Rork-generated app feels sluggish on launch, the cause is almost always in how initial loading is assembled. Here's the exact process I used to cut startup time to a third of its original value, measurements included.

rork59performance11startup14optimization5react-native12

When you open a Rork-generated app on a real device for the first time, have you ever thought "this feels kind of heavy"? I've been there many times. Apps with AI integration are particularly susceptible — initialization ends up juggling API key validation, model list fetching, and auth token refresh all in parallel, and it's not unusual for the first screen to take three seconds or more to appear.

To start with the conclusion: startup time in Rork-generated apps can be halved in most cases without major code rewrites. This article walks through the process I used, including the measurement logs I captured as I tuned.

First, measure startup time accurately

Everyone knows "optimization starts with measurement," but measuring React Native startup time correctly takes a little know-how. If you judge by gut feel — "this feels faster now" — you'll hit the same wall again a few weeks later.

These are the four timestamps I use.

  • T0: The instant the user taps the app icon (iOS Launch Screen appearing).
  • T1: JavaScript bundle load complete (when AppRegistry.runApplication fires).
  • T2: The first route component's onLayout fires.
  • T3: The user can first interact (tap, etc.).

For measurement, I've found plain Date.now() combined with InteractionManager.runAfterInteractions more practical than React Native's PerformanceObserver.

// index.js
const t0 = Date.now();
global.__APP_START__ = t0;
 
// App.tsx
useEffect(() => {
  const t1 = Date.now();
  console.log(`[startup] bundle->app mount: ${t1 - global.__APP_START__}ms`);
 
  InteractionManager.runAfterInteractions(() => {
    console.log(`[startup] interactive: ${Date.now() - global.__APP_START__}ms`);
  });
}, []);

This mundane log immediately reveals where the heaviness is. My initial measurement showed T0→T3 averaging 3,200ms, broken down as T0→T1 (bundle) 1,100ms, T1→T2 (initial render) 800ms, and T2→T3 (interactive) 1,300ms.

Don't chain await calls on the initial screen

If you open the root component of a Rork-generated app, very often you'll find a useEffect that sequentially awaits user info, theme settings, AI model list, and notification permission. This is the single biggest bottleneck in startup time.

Code written as await A(); await B(); await C(); can be parallelized with Promise.all whenever the calls are independent.

// Before (slow)
const user = await fetchUser();
const theme = await fetchTheme(user.id);
const models = await fetchAvailableModels();
 
// After (fast)
const [user, models] = await Promise.all([
  fetchUser(),
  fetchAvailableModels(),
]);
const theme = await fetchTheme(user.id);  // Only things depending on user come next

Just this rewrite shaved 800ms for me. When you look carefully at dependencies, the separation between "can parallelize" and "can't parallelize" becomes obvious.

Minimize images loaded at startup

Another big win was how logos and onboarding images were handled right after launch. In code that Rork auto-generates, home-screen and carousel images sometimes end up bundled in the initial JS bundle via require().

Switching them to lazy loading shrinks the JavaScript bundle itself, dramatically improving T0→T1.

// Before (included in bundle)
import heroImage from './assets/hero.png';
<Image source={heroImage} />
 
// After (fetched after launch)
<Image source={{ uri: 'https://cdn.example.com/hero.png' }} />

That said, logos and icons are better kept as require() — otherwise you get a "momentary blank image" UX regression. Realistically, only CDN-ify things that can show up a bit later: carousels, backgrounds, sample images.

Move auth token verification to after render

The trickiest part I encountered in Rork-generated apps was the auth token verification flow. Many implementations wait for verifyToken() to respond before rendering the first screen — but in most cases this can be deferred.

The vast majority of users open the app with a valid token, so rendering optimistically and verifying in the background dramatically improves perceived speed.

// Before
if (\!tokenVerified) return <Splash />;  // Waits for network
return <MainApp />;
 
// After
const [tokenVerified, setTokenVerified] = useState(true);  // Optimistically true
 
useEffect(() => {
  verifyToken().then(valid => {
    if (\!valid) {
      setTokenVerified(false);
      navigation.reset({ routes: [{ name: 'Login' }] });
    }
  });
}, []);
 
return <MainApp />;

This optimistic rendering alone cut T2→T3 by about 600ms. When the token is invalid, the user briefly sees the home screen before being redirected to login — but that transition actually feels natural as UX.

Enable Hermes and ProGuard

On both iOS and Android, reviewing build settings improves startup. Rork-generated defaults aren't guaranteed to be current, so check the following.

Android: In android/app/build.gradle, enable enableHermes: true and enableProguardInReleaseBuilds: true. Hermes pre-compiles JavaScript and measurably shortens startup.

iOS: In ios/Podfile, check :hermes_enabled => true and :fabric_enabled => true (for New Architecture apps only). Hermes helps on iOS too.

In my case, just enabling Hermes cut T0→T1 by 400ms. Many projects already have it on, but apps generated from older Rork templates can have it disabled.

Defer heavy stores until after first paint

Another pattern I see often in Rork apps: the root component rehydrates persisted state (MMKV, AsyncStorage, Redux Persist) synchronously before the first render. For anything beyond a few hundred keys, this becomes visible on mid-range devices.

The fix is to split persisted state into two tiers — "needed to render the first screen" (theme, locale, auth flag) and "needed eventually" (draft history, analytics consent, cache of past chats). Rehydrate the first tier synchronously, load the second tier in InteractionManager.runAfterInteractions.

useEffect(() => {
  // Tier 1 already hydrated before mount
  InteractionManager.runAfterInteractions(async () => {
    await hydrateDraftHistory();
    await hydrateAnalyticsConsent();
  });
}, []);

Checking AsyncStorage.getAllKeys().length on app launch is a quick way to find out if this applies to you. Anything above 300 keys is a smell.

Watch for over-eager font loading

React Native's custom font loading is another silent startup cost. If useFonts() from expo-font awaits three to five custom fonts before rendering, you're adding 200-400ms on the first launch after install. System fonts for the first screen, custom fonts loaded in parallel behind the scenes, is a better default.

Final measurements

In the end, my app's T0→T3 average dropped to 1,050ms — roughly a third of the original 3,200ms.

  • T0→T1: 1,100ms → 550ms (Hermes + smaller bundled images)
  • T1→T2: 800ms → 300ms (Promise.all parallelization)
  • T2→T3: 1,300ms → 200ms (optimistic token verification)

Once you reach this point, users notice — the "feels faster" sense becomes unmistakable. Store reviews started including speed-improvement comments.

Pitfalls I ran into

Measuring on the dev menu–enabled build. React Native's dev mode skips JavaScript compilation and adds remote debugging overhead, sometimes inflating numbers by 40-60%. Always measure on a Release build on a real device. My Debug-build numbers looked fine, but Release-build measurements told a different story.

Cold launch vs. warm launch. iOS and Android treat the app differently depending on whether it's already in memory. Numbers from "launch → background → relaunch within 10 seconds" are warm launches and will look artificially fast. Reboot the device before your final measurement.

Chasing the wrong metric. I once spent a whole evening shaving 50ms off T0→T1 when T2→T3 had 900ms of headroom. Always start from the largest segment. The biggest win usually comes from optimizing what's currently at least 40% of total startup.

Over-parallelizing. Promise.all is powerful, but firing 10 network requests simultaneously on a 4G connection can actually serialize at the device's network stack. I cap concurrency at 4-5 requests for any startup batch, which kept perceived parallelism without hitting the mobile radio ceiling.

Comparing iOS and Android numbers

Something that surprised me: the same Rork-generated app showed meaningfully different startup profiles across iOS and Android, and the biggest wins were on different axes. On iOS, enabling Hermes and deferring the token check gave me the largest gains. On Android, the dominant cost was the JavaScript bundle load itself — ProGuard plus asset pruning did more than any JavaScript-level tweak.

If you're shipping to both platforms, measure independently and be prepared to apply different optimizations. Specifically, on Android I found these three checks more impactful than the JavaScript-side changes:

  • Make sure ProGuard/R8 is enabled in the release variant of build.gradle.
  • Check that multiDexEnabled true is not triggering a DEX-load delay by auditing what's actually in the secondary DEX files.
  • Confirm that resConfigs in defaultConfig is narrowed to the locales you actually ship — without this, Android ships every translated string in every support library, bloating the APK by several MB.

On a Pixel 6a running Android 14, these three changes alone took my cold launch from 4,100ms down to 2,800ms before I had even touched the JavaScript layer.

Try this next

Start by measuring T0→T3 on your own app. Having numbers instead of gut feelings makes the priority of each optimization visible. A realistic first target is sub-1,500ms.

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 $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⬡ Dev Tools2026-05-20
Fixing 0x8badf00d Watchdog Kills That Wipe Out Rork Apps at Launch
Your Rork iOS app dies right after launch on real devices, but never on your bench. Crashlytics shows exception code 0x8badf00d. Here is how to read the exact watchdog budget out of the crash log and rearrange your launch path so it stops.
⬡ Dev Tools2026-04-24
When Your Rork App Binary Tops 150MB: 5 Causes to Isolate and a Step-by-Step Slim-Down Plan
My Rork-built app archived at 180MB and App Store Connect stopped me cold. Here is how I broke the bloat into 5 measurable causes and got it down to 69MB — including the three cuts that backfired.
⬡ Dev Tools2026-10-15
When EXPO_PUBLIC_ Values Read Fine in Dev but Come Back undefined in Production: Telling the Spellings Apart
The API URL reads fine while developing and turns undefined once the app is built. The cause was how process.env was written. I lined up babel-preset-expo's output for dev and production and tabulated which spellings get replaced and which are left behind.