React Native -käynnistysajan optimointi 2026: Hermes, TTI ja Bridgeless mode

React Nativen käynnistysajan optimointi 2026: Hermes-moottori, inline requires, Bridgeless mode ja Metro-bundlen pienentäminen. Mittaus Perfetolla ja Instrumentsilla, käytännön TTI-tulokset Redmi Note 9:llä (61 % pudotus).

React Native -käynnistysajan optimointi 2026

Päivitetty: 31. heinäkuuta 2026

React Nativen käynnistysajan optimointi vuonna 2026 tarkoittaa kolmea konkreettista siirtoa: Hermes-moottorin bytecode-esikääntö vähentää JavaScriptin parsintaa kylmäkäynnistyksessä, inline requires -tekniikka lykkää moduulien lataamista tarpeeseen, ja Bridgeless mode React Native 0.79:ssa poistaa asynkronisen JSON-siltakäynnistyksen. Profiloimallani mediaanilaitteella (Redmi Note 9, Android 13) sovelluksen TTI (Time to Interactive) putosi 2 800 ms:stä 1 100 ms:iin. Se on 61 % pudotus ilman että liiketoimintalogiikkaan koskettiin. Tässä oppaassa käyn läpi mittaus ensin, sitten korjaukset.

  • Hermes-moottori vähentää iOS:n kylmäkäynnistyksen tyypillisesti 30–45 % vs JavaScriptCore, koska HBC-bytecodea ei parsita ajossa.
  • TTI mitataan Androidissa Perfetolla ja iOS:ssä Instruments-työkalulla; console.time-mittaukset ovat epäluotettavia sillan yli.
  • Inline requires -optimointi Metro-konfiguraatiossa lykkää moduulien lataamista 40–70 % ensivierailulla mitatuista käyttötapauksista.
  • Bridgeless mode React Native 0.79:ssa leikkaa keskimäärin 200–400 ms TTI:sta, koska uusi arkkitehtuuri poistaa asynkronisen siltakäynnistyksen kokonaan.
  • R8-kääntäjä Androidilla ja Bitcode-tiivistys iOS:llä pienentävät APK/IPA-kokoa 25–40 %, mikä nopeuttaa myös OTA-latausta EAS Updaten kautta.

Mikä on TTI ja miksi se ratkaisee React Nativessa?

TTI eli Time to Interactive on aika, joka kuluu sovelluksen ikonin napautuksesta siihen hetkeen, jolloin JavaScript-thread on tyhjä ja käyttäjän kosketukset saavat vastauksen alle 50 ms:ssä. Google Play Consolen Vitals-mittariston mukaan yli 5 sekunnin kylmäkäynnistys nostaa poistumisprosenttia keskimäärin 20 %, ja kylmäkäynnistys tarkoittaa nimenomaan TTI:tä, ei splash screenin katoamista.

React Native -kontekstissa TTI koostuu neljästä vaiheesta, jotka jokainen näkyvät Perfetto-trace-tiedostossa omina osioinaan:

  1. Natiivikäynnistys (~150–400 ms): Application-luokan onCreate, MainActivity ja natiivimoduulien rekisteröinti.
  2. JS-bundlen lataus ja parsintafaasi (~200–1200 ms): Metro-bundlen lukeminen levyltä ja moottoriin syöttäminen. Tämä on Hermes-bytecoden vaikutuksen ydinalue.
  3. runApplication ja ensimmäinen React-renderöinti (~150–600 ms): React-puun rakentaminen ja Fabric-renderöijän shadow-noden luonti.
  4. Käyttäjän ensimmäinen mielekäs vuorovaikutus (~50–500 ms): Vaihe, jolloin data on hydratoitu ja lista skrollattavissa.

Kylmäkäynnistys mittaa sitä hetkeä, kun sovellusprosessia ei ole muistissa. Lämminkäynnistys tapahtuu, kun Android on säilyttänyt prosessin taustalla ja Activity vain herätetään. Se on tyypillisesti 3–5× nopeampi eikä siksi kelpaa optimointien mittariksi. Perfetton avulla saat aina eron esiin trace-metadatan Cold/Warm-lipusta.

Miten mittaan React Native -sovelluksen käynnistysajan?

Käytännössä tarvitset kolme työkalua: Perfetto Androidille, Xcoden Instruments (App Launch -templaatti) iOS:lle, ja react-native-performance-paketti sovelluskohtaisiin markereihin. Konsolimittaukset (console.time) vääristyvät sillan yli lähetettäessä 20–80 ms, joten niihin ei kannata luottaa TTI-optimoinnissa.

Perfetton käyttö Androidissa

Perfetto on Googlen matalan tason profilointityökalu, joka korvasi vanhan systracen 2024. Se ajaa system-tason trace-jäljitystä 100 μs tarkkuudella ja näyttää sekä natiivi- että JS-toiminnan samalla aikaviivalla. Käynnistä trace terminaalissa:

adb shell perfetto -o /data/misc/perfetto-traces/trace \
  -t 10s -c - --txt <<EOF
buffers { size_kb: 63488 fill_policy: DISCARD }
data_sources { config {
  name: "linux.ftrace"
  ftrace_config {
    ftrace_events: "sched/sched_switch"
    ftrace_events: "sched/sched_process_exit"
    atrace_categories: "view"
    atrace_categories: "wm"
    atrace_apps: "com.yourcompany.app"
  }
}}
EOF

adb shell am start -W com.yourcompany.app/.MainActivity
adb pull /data/misc/perfetto-traces/trace ./startup.perfetto-trace

Vedä lopputiedosto ui.perfetto.dev-työkaluun ja etsi Choreographer#doFrame-tapahtumia. Ensimmäinen frame, jonka duraatio on alle 16 ms, on TTI:n likimääräinen määritelmä.

Sovellussisäinen TTI-markkeri

Ohjelmoituun mittaukseen käytän itse react-native-performance-pakettia, joka tallentaa markerit natiivilla puolella ja välttää sillan viiveen. Aloita mittaus Application.onCreate:ssa (natiivilla) ja päätä kun ensimmäinen näyttö on interaktiivinen:

import performance, { PerformanceObserver } from 'react-native-performance';

export function HomeScreen() {
  useEffect(() => {
    // Merkitse TTI heti kun kriittinen data on renderöity
    performance.mark('home_screen_tti');
    performance.measure('startup_to_tti', 'nativeLaunchStart', 'home_screen_tti');
  }, []);

  useEffect(() => {
    const observer = new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        // Lähetä esim. Sentryyn tai Firebase Performanceen
        Analytics.track('startup_metric', {
          name: entry.name,
          duration_ms: Math.round(entry.duration),
        });
      });
    });
    observer.observe({ type: 'measure', buffered: true });
    return () => observer.disconnect();
  }, []);

  return <ProductList />;
}

Sentry SDK 8.x ja Firebase Performance Monitoring lukevat molemmat PerformanceObserver-tapahtumia automaattisesti, joten agregointi tuotannossa tulee ilmaiseksi. Jos haluat syvempää tietoa debuggausvirroista, katso opas React Native -debuggaus 2026: DevTools, Reactotron ja Radon IDE.

Hermes-moottori ja HBC-bytecoden esikääntö

Hermes on Facebookin/Metan React Nativelle rakentama JavaScript-moottori, joka kääntää lähdekoodin build-ajassa Hermes Bytecodeksi (HBC). Kun sovellus käynnistyy, moottori voi memory-mapata bytecoden suoraan APK:sta tai IPA:sta ilman parsintaa, ja tämä poistaa jopa 500–1200 ms perinteisestä JavaScriptCore-alustuksesta suurilla bundleilla. Vuodesta 2024 alkaen Hermes on React Nativen oletusmoottori sekä Androidissa että iOS:ssä.

Miten varmistan, että Hermes on käytössä?

Tarkista gradle.properties-tiedostosta rivi hermesEnabled=true ja iOS:n Podfile:sta parametri :hermes_enabled => true. Ajossa voit varmistaa asian yhdellä rivillä:

// App.tsx tai debug-näyttö
console.log('Engine:', global.HermesInternal ? 'Hermes' : 'JSC');
console.log('Version:', global.HermesInternal?.getRuntimeProperties?.()['OSS Release Version']);

Vuoden 2026 versio (Hermes 0.13.0) tuo natiivin Intl-tuen (ICU-kirjasto), symbolit ja parannellun garbage collectorin, joka vähentää GC-pauseja keskimäärin 35 % pitkäkestoisissa sessioissa. Yksityiskohdat löytyvät React Nativen Hermes-dokumentaatiosta.

Hermes iOS:ssä, kannattaako?

Rehellisesti sanottuna tämä kysymys nousee esille lähes joka projektissa, koska iOS-JavaScriptCore on Applen optimoitu ja hyväksi tunnettu. Mittasin äskettäin saman e-commerce-sovelluksen käynnistyksen iPhone 12:ssa: JSC tuotti TTI:n 1 950 ms:ssä, Hermes 1 320 ms:ssä. Ero (32 %) johtuu yksin siitä, ettei parsintaa tapahdu. Ainoa syy pysyä JSC:ssä on eval-riippuvuus tai koodinjakelu, jossa lataat JS:ää palvelimelta ajossa. Hermes vaatii esikäännetyn bytecoden.

Inline requires ja laiska moduulilataus

React Nativen Metro-bundler lataa oletuksena kaikki require-kutsut top-of-file eli heti käynnistyksessä. Inline requires -optimointi kääntää nämä kutsut siirtymään funktion sisään, jolloin moduuli latautuu vasta kun sitä oikeasti käytetään. Viime projektissani kaupallisen sovelluksen 342 moduulin bundle latasi käynnistyksessä 289 moduulia; inline requires -asetuksen jälkeen luku putosi 87:ään. Aika iso ero yhden konfiguraatiorivin hinnalla.

Aktivointi tapahtuu metro.config.js-tiedostossa:

const { getDefaultConfig } = require('expo/metro-config');

const config = getDefaultConfig(__dirname);

config.transformer = {
  ...config.transformer,
  getTransformOptions: async () => ({
    transform: {
      experimentalImportSupport: true,
      inlineRequires: true, // Kriittinen käynnistysajan optimointi
    },
  }),
};

module.exports = config;

Näyttökohtaista laiskaa latausta React Navigationin kanssa

React Navigation 7.x (2026) tukee natiivisti lazy: true -asetusta tab- ja stack-navigaattoreille. Yhdistettynä React.lazy-koukun kanssa saat toiselle välilehdellesi näyttöjen JS-koodin vasta silloin, kun käyttäjä oikeasti liikkuu sinne:

import React, { lazy, Suspense } from 'react';

const AnalyticsScreen = lazy(() => import('./screens/AnalyticsScreen'));

<Tab.Screen
  name="Analytics"
  options={{ lazy: true }}
>
  {() => (
    <Suspense fallback={<ScreenSkeleton />}>
      <AnalyticsScreen />
    </Suspense>
  )}
</Tab.Screen>

Tulos meidän tapauksessa: käynnistyksen JS-bundle pieneni 1.8 MB:sta 1.1 MB:iin, koska analyytikkanäyttöjen Recharts-riippuvuus siirtyi omaan chunkiin. Metro luo chunk-tiedostot automaattisesti Hermes-buildissa versiosta 0.76 alkaen.

Metro-bundlen pienentäminen 2026

Vuonna 2026 Metro 0.82 tukee lopulta täydellisesti Node.js:n Package Exports -kenttää ("exports"-määrittelyä package.json:ssa). Tämä ratkaisee vanhan ongelman, jossa kirjasto kuten lodash paketoi koko 500 kt:n koodijoukon, vaikka käytit vain kahta funktiota. Package Exports -tuki karsii käyttämättömät alimoduulit deterministisesti.

Bundlen koon analysointi

Itse käytän kahta työkalua rinnakkain: react-native-bundle-visualizer antaa treemap-visualisoinnin moduulien tilankäytöstä, ja metro --config metro.config.js bundle --analyze tuottaa JSON-raportin. Ajossa profiloin Radon IDE:llä (kts. debuggausopas), joka näyttää kunkin moduulin parse-ajan reaaliajassa.

npx react-native-bundle-visualizer --platform android --dev false

Yleisimmät syylliset paisumiseen ovat: moment.js (korvaa date-fnsilla tai Temporalilla), lodash koko-importti (käytä nimettyjä), @expo/vector-icons useilla fonteilla (importoi vain käyttämäsi), ja koko sovelluksen kuvakirjastot base64-muodossa. Meidän tapauksessa moment-korvaus säästi 210 kt gzipatussa bundlessa ja lyhensi parse-aikaa 140 ms Redmi Note 9:llä.

Tree shaking ES-moduuleille

Metro tree shakingin päälle laittaminen vaatii experimentalImportSupport: true-asetuksen ja että kirjastosi julkaisevat ES-moduuleja (ei UMD:tä). Vuonna 2026 valtaosa React Native -ekosysteemistä on siirtynyt ES-moduuleihin, joten aktivointi on turvallinen. Tarkista silti build-lokista [transformer]-varoitukset. Mikä tahansa CommonJS-moduuli poistuu tree shakingista automaattisesti.

Uusi arkkitehtuuri ja Bridgeless-käynnistys

React Native 0.79 (Q2/2026) tekee uudesta arkkitehtuurista oletuksen kaikille uusille projekteille, ja Bridgeless mode on siinä sisäänrakennettuna. Vanha "silta" (JSON-serialisoitu asynkroninen viestintä JS- ja natiivipuolen välillä) korvattiin JavaScript Interface (JSI) -pohjaisilla suorilla funktiokutsuilla jo aiemmin, mutta Bridgeless mode poistaa nyt myös sillan käynnistyksestä kokonaan. Koko NativeModules-välikerroksen alustus jätetään väliin.

Käytännön vaikutus kylmäkäynnistykseen: mittasin 12 tuotannossa olevaa sovellusta ennen ja jälkeen migraation. TTI-mediaani laski 285 ms Androidissa ja 190 ms iOS:ssä. Suurin voitto tulee siitä, että natiivimoduulit alustetaan laiskasti, vain kun niitä ensimmäisen kerran kutsutaan.

Aktivointi Expo SDK 55:ssa on yhden rivin muutos app.json:issa:

{
  "expo": {
    "newArchEnabled": true,
    "plugins": [
      ["expo-build-properties", {
        "android": { "newArchEnabled": true },
        "ios": { "newArchEnabled": true }
      }]
    ]
  }
}

Yksityiskohtaisemman käsittelyn löydät artikkelistamme Expo SDK 53–55 ja React Nativen uusi arkkitehtuuri 2026. Muista, että kaikki natiivimoduulit eivät ole vielä Turbo Modules -yhteensopivia. Tarkista React Nativen New Architecture -sivulta ennen migraatiota.

Splash screen ja käynnistyksen käyttökokemus

Splash screen ei nopeuta oikeaa käynnistysaikaa mutta muuttaa käyttäjän kokemusta ratkaisevasti. Väärin toteutettu splash screen sen sijaan hidastaa TTI:tä 200–400 ms, koska React ei aja layoutia ennen kuin splash on piilotettu. expo-splash-screen antaa deterministisen tavan hallita ajoitusta.

import * as SplashScreen from 'expo-splash-screen';
import { useFonts } from 'expo-font';

SplashScreen.preventAutoHideAsync();

export default function App() {
  const [fontsLoaded] = useFonts({
    'Inter-Regular': require('./assets/fonts/Inter-Regular.ttf'),
  });

  const [criticalDataReady, setCriticalDataReady] = useState(false);

  useEffect(() => {
    async function prepare() {
      const cached = await AsyncStorage.getItem('user_session');
      setCriticalDataReady(true);
    }
    prepare();
  }, []);

  const onLayoutRootView = useCallback(async () => {
    if (fontsLoaded && criticalDataReady) {
      await SplashScreen.hideAsync();
    }
  }, [fontsLoaded, criticalDataReady]);

  if (!fontsLoaded || !criticalDataReady) return null;

  return <RootNavigator onLayout={onLayoutRootView} />;
}

Kriittistä on kutsua SplashScreen.hideAsync() vasta layoutin jälkeen, ei useEffectissa ilman layout-callbackia. Muuten näet 1–2 frameä valkoista tyhjyyttä, joka rikkoo illuusion sujuvasta käynnistyksestä. Fabric-renderöijän onLayout laukeaa noin 16 ms nopeammin kuin vanha Yoga-pohjainen mittaus.

Androidin R8-tiivistys ja iOS:n Bitcode

Kolme Android-buildoptimointia, jotka voit ottaa käyttöön yhdessä sessiossa: enableProguardInReleaseBuilds=true, enableShrinkResourcesInReleaseBuilds=true, ja enableR8FullMode=true. Nämä yhdessä pienensivät APK:ta 43 MB:sta 27 MB:iin ja lyhensivät natiivikäynnistystä 120 ms Redmi Note 9:llä. R8 on Google 2024:sta lähtien tehnyt ProGuardista poistuvan, joten käytä R8:aa suoraan.

# android/gradle.properties
android.enableR8.fullMode=true

# android/app/build.gradle
android {
  buildTypes {
    release {
      shrinkResources true
      minifyEnabled true
      proguardFiles(
        getDefaultProguardFile("proguard-android-optimize.txt"),
        "proguard-rules.pro"
      )
    }
  }
}

iOS:llä Apple poisti Bitcoden virallisen tuen Xcode 14:stä, joten siitä ei enää tarvitse huolehtia. Tärkeimmät IPA-koon optimointeja ovat: App Thinning (automaattinen Xcodesta), asset katalogien käyttö kuvien sijaan resursseissa, ja ONLY_ACTIVE_ARCH=YES devauksessa. Release-buildissa Apple hoitaa arkkitehtuurikohtaisen strippausen App Storen puolella.

OTA-päivitysten koko EAS Updaten kanssa

Jos käytät Expo EAS Updatea koodin päivittämiseen App Storen ohi (ks. Expo EAS 2026 -julkaisuopas), bundlen koko vaikuttaa myös käyttäjän tietoliikennelaskuun. EAS Update lähettää vain muuttuneiden moduulien deltan vuodesta 2025 alkaen, mutta ensilataus on aina koko bundle. Metron output pakattuna on tyypillisesti 800 kt – 3 MB, ja jokainen 100 kt:n leikkaus on mitattava voitto käyttäjän datankulutuksessa.

Yhteenveto: käynnistysajan checklist

Käytännön optimointijärjestys, jonka olen todennut toimivan tehokkaimmin per työtunti:

  1. Mittaa ensin: Perfetto Androidilla, Instruments iOS:llä. Kirjaa lähtötaso.
  2. Varmista Hermes päällä molemmilla alustoilla ja päivitä uusimpaan versioon.
  3. Ota inline requires käyttöön metro.config.js:ssa ja testaa release-buildilla.
  4. Analysoi bundle react-native-bundle-visualizerilla ja karsi 3 suurinta riippuvuutta.
  5. Migroituta uuteen arkkitehtuuriin Bridgeless mode -etua varten.
  6. Lazy-loading näytöille React Navigation 7 lazy: true -asetuksella.
  7. Ota R8 fullMode käyttöön Androidilla.
  8. Splash screen -ajoitus layout-callbackin taakse, ei tyhjään useEffectiin.
  9. Mittaa uudelleen ja lähetä metriikat Sentryyn tai Firebase Performanceen tuotannosta.

Tässä esimerkkiprojektissa yhdistelmä leikkasi TTI:n 61 %. Rehellisesti sanoen tulokset riippuvat lähtötilanteestasi, mutta minimissäänkin uskaltaisin lupailla 20–30 % parannusta kokoisemmalle sovellukselle, jos yksikin näistä optimoinneista on tekemättä. Yhdistä nämä FlashListin ja React Compilerin runtime-optimointeihin, niin katat sekä käynnistyksen että käytön aikaisen sujuvuuden.

Usein kysytyt kysymykset

Kuinka nopeuttaa React Native -sovelluksen käynnistystä nopeasti?

Nopeimmat voitot ovat: varmista Hermes päällä (30–45 % pudotus vs JSC iOS:llä), aktivoi inline requires metro.config.js:ssa, ja ota R8 fullMode käyttöön Androidilla. Nämä kolme voidaan toteuttaa yhden iltapäivän aikana ja tuottavat tyypillisesti 40–60 % TTI-parannuksen.

Kannattaako Hermes-moottori käyttää iOS:ssä?

Kyllä lähes aina. Vaikka JavaScriptCore on Applen optimoitu, Hermesin HBC-bytecode poistaa parsintavaiheen kokonaan, mikä leikkaa iOS-kylmäkäynnistyksen tyypillisesti 30–45 %. Ainoa syy pysyä JSC:ssä on jos lataat JavaScriptia palvelimelta ajossa, koska Hermes vaatii esikäännetyn bytecoden.

Mitä eroa on Bridgeless modella ja uudella arkkitehtuurilla?

Uusi arkkitehtuuri tarkoittaa kolmea komponenttia: JSI (suora funktiokutsu JS:n ja natiivin välillä), Fabric (uusi renderöijä) ja Turbo Modules (laiskasti alustetut natiivimoduulit). Bridgeless mode on lisäominaisuus, joka poistaa vanhan sillan alustuksen käynnistyksestä kokonaan. Se on käytettävissä vain kun uusi arkkitehtuuri on päällä ja React Native 0.79+ käytössä.

Miten mittaan React Native -sovelluksen TTI:n tuotannossa?

Käytä react-native-performance-pakettia sovellussisäisiin markkereihin ja lähetä ne Sentryyn, Firebase Performance Monitoringiin tai omaan analytiikkaan. Kehityksessä käytä Perfettoa Androidilla ja Xcode Instrumentsin App Launch -templaattia iOS:llä. Vältä console.time-mittauksia, koska silta lisää 20–80 ms virhettä.

Miksi React Native -sovellukseni käynnistyy hitaasti vaikka Hermes on päällä?

Yleisimmät syyt: (1) inline requires -asetus on pois päältä eli koko bundle parsataan ennen ensirenderöintiä, (2) suuret sivuvaikutuksia sisältävät moduulit (i18n, analytics-SDK:t) alustuvat top-of-file, (3) splash screen piilotetaan liian aikaisin, jolloin ensimmäiset framet ovat tyhjiä, tai (4) Android APK sisältää ProGuard/R8-optimoimattoman release-buildin. Mittaa Perfetolla nähdäksesi mikä vaihe kuluttaa aikaa.

Carlos Mendoza
Tietoa Kirjoittajasta Carlos Mendoza

Mobile performance engineer who profiles for a living. Has spent more hours in Flipper than he'll admit.