Skill: Remote Code Loading

March 19, 2026 ยท View on GitHub

Set up code splitting with Re.Pack for on-demand bundle loading from trusted, first-party assets.

Quick Pattern

Before (static import):

import SettingsScreen from './screens/SettingsScreen';

After (lazy loaded chunk):

const SettingsScreen = React.lazy(() =>
  import(/* webpackChunkName: "settings" */ './screens/SettingsScreen')
);

<Suspense fallback={<Loading />}>
  <SettingsScreen />
</Suspense>

When to Use

Consider code splitting when:

  • Not using Hermes (JSC/V8 benefits more)
  • App size exceeds 200 MB (Play Store limit)
  • Building micro-frontend architecture
  • Loading features based on user permissions
  • Other optimizations exhausted

Note: Hermes already uses memory mapping for efficient bundle reading. Benefits of code splitting are minimal with Hermes or even counterproductive in some cases.

Security Model

Remote chunks are executable application code. Only load chunks that you build and publish yourself.

Keep these guardrails in place:

  • Serve chunks only from a first-party, HTTPS-only origin you control
  • Resolve scriptId through a fixed allowlist or release manifest
  • Fail closed if a chunk is missing or unexpected
  • Do not load chunks from user-controlled input, query params, or third-party domains

Prerequisites

  • Re.Pack installed (replaces Metro)
npx @callstack/repack-init

Step-by-Step Instructions

1. Initialize Re.Pack

npx @callstack/repack-init

Follow prompts to migrate from Metro. Check migration guide.

2. Create Split Point with React.lazy

// BEFORE: Static import
import SettingsScreen from './screens/SettingsScreen';

// AFTER: Dynamic import (creates split point)
const SettingsScreen = React.lazy(() =>
  import(/* webpackChunkName: "settings" */ './screens/SettingsScreen')
);

3. Wrap with Suspense

import React, { Suspense } from 'react';

const App = () => {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <SettingsScreen />
    </Suspense>
  );
};

4. Configure Chunk Loading

// index.js (before AppRegistry)
import { ScriptManager, Script } from '@callstack/repack/client';

const CHUNK_URLS = {
  settings: 'https://assets.example.com/app/v42/settings.chunk.bundle',
};

ScriptManager.shared.addResolver((scriptId) => ({
  url: __DEV__ ? Script.getDevServerURL(scriptId) : getChunkUrl(scriptId),
}));

function getChunkUrl(scriptId) {
  const url = CHUNK_URLS[scriptId];

  if (!url) {
    throw new Error(`Unknown chunk: ${scriptId}`);
  }

  return url;
}

AppRegistry.registerComponent(appName, () => App);

5. Build and Deploy Chunks

Build generates:

  • index.bundle - Main bundle
  • settings.chunk.bundle - Lazy-loaded chunk

Deploy chunks to a first-party CDN with versioned paths, and keep the allowlist or manifest in sync with the app release.

Complete Example

// App.tsx
import React, { Suspense, useState } from 'react';
import { Button, View, ActivityIndicator } from 'react-native';

// Lazy load heavy feature
const HeavyFeature = React.lazy(() =>
  import(/* webpackChunkName: "heavy-feature" */ './HeavyFeature')
);

const App = () => {
  const [showFeature, setShowFeature] = useState(false);
  
  return (
    <View>
      <Button 
        title="Load Feature" 
        onPress={() => setShowFeature(true)} 
      />
      
      {showFeature && (
        <Suspense fallback={<ActivityIndicator />}>
          <HeavyFeature />
        </Suspense>
      )}
    </View>
  );
};

Module Federation (Advanced)

For micro-frontend architecture:

// Host app loads remote module
const RemoteModule = React.lazy(() =>
  import('remote-app/Module')
);

Enables:

  • Independent team deployments
  • Shared dependencies
  • Runtime composition

Complexity warning: Only use when organizational benefits outweigh overhead. Federation increases the trust boundary, so keep the same first-party origin and allowlist rules as above.

Version Management

Consider Zephyr Cloud for:

  • Sub-second deployments
  • Version management
  • Re.Pack integration

Caching Strategy

ScriptManager.shared.addResolver((scriptId) => ({
  url: getChunkUrl(scriptId),
  cache: {
    // Enable caching
    enabled: true,
    // Cache location
    path: `${FileSystem.cacheDirectory}/chunks/`,
  },
}));

When NOT to Use

ScenarioWhy Not
Using Hermesmmap already efficient
Small appOverhead not worth it
Simple navigationNative navigation better
Quick iteration neededAdded complexity

Hermes Memory Mapping

Hermes reads bytecode lazily via mmap:

  • Only loads executed code into memory
  • No parse step needed
  • Code splitting provides marginal benefit

Verification

// Check if chunk loaded correctly
ScriptManager.shared.on('loading', (scriptId) => {
  console.log(`Loading: ${scriptId}`);
});

ScriptManager.shared.on('loaded', (scriptId) => {
  console.log(`Loaded: ${scriptId}`);
});

ScriptManager.shared.on('error', (scriptId, error) => {
  console.error(`Failed: ${scriptId}`, error);
});

Common Pitfalls

  • Forgetting Suspense: Lazy components need fallback
  • Wrong CDN path: Chunks 404 in production
  • No caching: Re-downloads on every load
  • Too many chunks: Network overhead exceeds savings
  • Untrusted chunk source: Remote JS from third-party or user-controlled origins is equivalent to remote code execution