A Developer's Guide: Understanding System Bundles in Apps for Amazon Fire TV

Understanding System Bundles in Apps for Amazon Fire TV

Aug 27th 2025, by @cebratec, @Abhay_Jain, @Christian_Van_Boven1

Modern apps built on React Native often ship with large monolithic JavaScript bundles. These bundles include everything - from core React libraries to app-specific business logic. While convenient, this approach creates challenges: bigger app sizes, slower startup, and more friction when delivering bug fixes.

To address this, Vega introduces a split bundle system, where common libraries live on the system and apps only ship the code that’s unique to them. Let’s explore what this means, why it matters, and how it works behind the scenes.

What is a Split Bundle?

Traditionally, a React Native app ships a single App Bundle containing:

  • Core libraries like react and react-native
  • Vega-specific modules
  • Third-party libraries
  • Your app’s code

With split bundles, the app bundle is divided into two parts:

1/ System (Common) Bundle
Pre-installed on the device, this contains widely used libraries such as react, react-native, and @amzn/react-native-vega. For details, see system distributed libraries. It is kept in sync with the native Vega runtime.

2/ Split App Bundle
Your app’s bundle, minus the common libraries. This contains only app-specific code and any unique dependencies.

How the transformation works

Below is a simplified view of how a monolithic bundle is split:

┌────────────────────────────┐
│     Monolithic App Bundle  │
│  (React, RN, Vega, App)  │
└────────────────────────────┘
              │
              ▼
 ┌────────────────────┐    ┌─────────────────────┐
 │   System Bundle    │    │   Split App Bundle  │
 │ (React, RN, Vega)│    │ (App-specific code) │
 └────────────────────┘    └─────────────────────┘

At runtime, Vega loads the system bundle first, then stitches in your split app bundle.

Why Split Bundles Matter

Split bundles bring several key advantages:

  • :rocket: Faster startup times (TTFD)
    Core libraries are preloaded by the system, reducing app initialization time by ~100–150ms.
  • :package: Smaller app size
    By removing common libraries from your app package, you reduce the size by ~1MB or more.
  • :counterclockwise_arrows_button: Automatic upgrades and bug fixes
    Since system libraries update independently, apps get bug fixes and performance improvements without requiring a new app release.
  • :link: Consistent sync between native and JS
    Both JS and native components live in the same system version, ensuring compatibility.

How the Split Bundle System Works

Behind the scenes, the magic relies on extending Metro, the JavaScript bundler used by React Native. Normally, Metro produces a single index.bundle with all your app’s code and dependencies. To enable splitting, Vega customizes Metro’s Serializer stage, which controls how modules are combined into bundles.

Two key hooks make this possible:

1/ processModuleFilter

  • Filters out modules already provided by the system bundle.
  • Ensures that app bundles don’t duplicate libraries like react or react-native.

2/ createModuleIdFactory

  • Assigns consistent, deterministic IDs to modules.
  • This is critical because system and app bundles must reference the same module IDs when sharing code.
  • Instead of Metro’s default “incremental +1” IDs, Vega uses a hash-based scheme (e.g. SHA256 of the module path) to guarantee stability across builds.
                      Build time                        Runtime
                  ──────────────────►               ───────────────►
┌─────────────┐    ┌──────────────┐   uses IDs in   ┌───────────────┐
│  Source     │─►  │  Core Bundle │───────────────► │ Load Core     │
│  (app + lib)│    │ (React, RN,  │                 │ (system)      │
└─────────────┘    │  Vega  )     │                 └───────────────┘
                   └─────┬────────┘
                         │  writes modules.txt
                         ▼
                   ┌──────────────┐   uses IDs in   ┌───────────────┐
                   │ Library Bndl │───────────────► │ Load Libraries│
                   │ (e.g. RNScrn)│                 │ (system)      │
                   └─────┬────────┘                 └───────────────┘
                         │  reuses IDs / filters
                         ▼
                   ┌──────────────┐                 ┌───────────────┐
                   │  App Bundle  │───────────────► │ Load App      │
                   │ (split)      │                 │ (on top)      │
                   └──────────────┘                 └───────────────┘

Types of Bundles

The split bundle system creates three main bundle types:

1st - Core Bundle (considered system)

  • Contains fundamental libraries (react, react-native, @amzn/react-native-vega).
  • Generated first, producing a modules.txt file mapping module paths to IDs.
  • This mapping ensures consistent referencing in dependent bundles.

2nd - Library Bundles (considered system)

  • For popular libraries like react-native-reanimated or react-native-screens.
  • Created using the modules.txt from core to avoid duplicating already-bundled dependencies.
  • Additional modules.txt files are generated for each library.

3rd - Application Bundle (Split App Bundle)

  • Your app’s code, filtered to exclude anything already included in system bundles.
  • Still references system-provided modules via the shared IDs.

At build time, metadata (e.g. keplerscript-app-system-bundles-config.json) is generated, telling Vega which system bundles your app requires. At runtime, the Vega runtime first loads the system bundles, then attaches the app bundle on top.

Handling Dependencies and Versions

A major challenge in split bundling is dependency versioning:

  • If the system bundle has react-native-screens@2.0.0 but your app requires 2.1.0, conflicts can arise.
  • To manage this, Vega uses versioned module paths internally (e.g. react-native-screens__2/).
  • Only major versions are encoded into paths (e.g. __2/), so bugfix upgrades (2.0.0 → 2.0.1) don’t break compatibility or force app rebuilds.

Additionally, when filtering dependencies:

  • System bundles include their own dependencies (e.g. lodash) but do not list them in modules.txt.
  • This ensures that if an app also depends on the same version of lodash, it’s still present in the app bundle, preventing accidental breakages when system libraries upgrade.
Same major, safe upgrade:
@amzn/react-native-screens__2/...   (2.0.0 → 2.0.1 keeps the same path)

Different major, intentionally distinct:
@amzn/react-native-screens__3/...

This approach, combined with stable IDs, lets the system update frequently while apps remain compatible.

:light_bulb: While this versioning strategy currently serves our needs well, we may explore additional optimization opportunities in the future. Any changes to the versioning scheme would be carefully planned and communicated to maintain backward compatibility.

Lifecycle of a System Bundle Build

Here’s the full lifecycle of a split bundle build:

  1. Core bundle is created (React, RN, Vega).
  2. Library bundles are created using dependency-aware filtering.
  3. App bundle is created with all system-provided modules removed.
  4. Metadata is generated listing which system bundles are required.
  5. At runtime, Vega loads system bundles first, then the app bundle.

The result is a modular system where apps stay small, fast, and resilient to library updates.

Split bundles are a powerful way to modernize JavaScript delivery for Vega apps. The users benefit from smaller and faster apps, developers get system bug fixes and performance improvements without re-releasing and system and app code stay in sync, reducing compatibility issues.

As this ecosystem matures, we’ll continue expanding support for more libraries in the system bundle while improving developer tooling around bundle creation and conflict resolution.

Additional Resources