0Pricing
React Native Academy · Lesson

Bundle Size and Lazy Loading

Analyze the JS bundle with react-native-bundle-visualizer, identify large dependencies, apply dynamic import() for heavy screens, and measure the before/after bundle size.

Bundle Size and Lazy Loading is a free React Native Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the React Native Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Why Bundle Size Affects Startup Time

The JavaScript bundle is the single file that contains all your app's code and most third-party libraries. Hermes parses and executes this file on every app cold start. A larger bundle means a longer startup time — even a 500KB increase can add hundreds of milliseconds to your time-to-first-interaction on mid-range Android devices.

Bundle size optimization is especially important in React Native because all JavaScript must load before the first screen can render. Unlike a web app where you can lazy-load route chunks before rendering, React Native apps pay the full bundle cost upfront.

Hermes Bytecode and Bundle Measurement

When Hermes is enabled (the default since RN 0.70), the JavaScript bundle is pre-compiled to bytecode at build time. This reduces parse time but the raw bundle size still affects how much bytecode the engine must load.

Measure your current bundle size by running the Metro bundler with the --bundle-output flag and checking the output file size. Compare the bundle before and after changes to measure the impact of your optimizations.

# Generate the bundle and measure its size
npx react-native bundle \
  --platform android \
  --dev false \
  --entry-file index.js \
  --bundle-output /tmp/bundle.js

# Check the size
ls -lh /tmp/bundle.js
# e.g., -rw-r--r-- 1 user group 2.4M /tmp/bundle.js

react-native-bundle-visualizer

react-native-bundle-visualizer generates a treemap visualization of your bundle, showing exactly which packages and modules take up the most space. Install it globally and run it against your project to see the breakdown.

Look for large packages in the treemap that might be replaceable with lighter alternatives. Common culprits include moment.js (replace with date-fns), lodash (import individual functions), and large icon libraries (use only the icons you need).

# Install globally
npm install -g react-native-bundle-visualizer

# Run from your project root
npx react-native-bundle-visualizer

# Opens a browser with an interactive treemap showing:
# - Each library's size
# - Which modules within each library are included
# - Total bundle composition breakdown

Tree Shaking and Named Imports

Tree shaking is the process where the bundler removes unused code from the final bundle. For tree shaking to work, you must import only what you use from libraries — using named imports instead of importing the entire default export.

Metro (React Native's bundler) performs basic tree shaking for ES modules. Libraries must use ES module syntax (export / import) for tree shaking to be effective. CommonJS (module.exports) modules are not tree-shakable.

// ❌ Imports the entire lodash library (~70KB)
import _ from 'lodash';
const sorted = _.sortBy(items, 'name');

// ✅ Imports only sortBy (~3KB)
import sortBy from 'lodash/sortBy';
const sorted = sortBy(items, 'name');

// ❌ Imports all of lucide-react-native icons
import * as Icons from 'lucide-react-native';

// ✅ Imports only the used icons
import { Home, Settings, User } from 'lucide-react-native';

Dynamic Import for Lazy Loading

React supports dynamic import with import() to load a module asynchronously at runtime instead of including it in the initial bundle. Combined with React.lazy and Suspense, you can defer loading heavy screens until the user actually navigates to them.

This splits the bundle into chunks. The initial bundle stays small and loads quickly. Heavy screens like a PDF viewer, a video editor, or a complex chart library are only downloaded when needed.

import React, { lazy, Suspense } from 'react';
import { ActivityIndicator } from 'react-native';

// Heavy screen loaded only when the user navigates to it
const ReportScreen = lazy(() => import('./screens/ReportScreen'));

export function AppNavigator() {
  return (
    <Stack.Navigator>
      <Stack.Screen name='Home' component={HomeScreen} />
      <Stack.Screen
        name='Report'
        component={() => (
          <Suspense fallback={<ActivityIndicator />}>
            <ReportScreen />
          </Suspense>
        )}
      />
    </Stack.Navigator>
  );
}

Replacing Heavy Libraries with Lightweight Alternatives

Sometimes the best bundle optimization is swapping a heavy library for a smaller one. Common replacements:

  • moment.js (67KB)date-fns (imported per function, ~3KB) for date formatting
  • lodash (full, 70KB) → individual lodash imports or native JS methods
  • axios (12KB)ky (3KB) or native fetch for simple use cases

Before replacing a library, verify the alternative covers all your use cases to avoid surprises in edge cases.

// Replace moment.js with date-fns
// Before (adds ~67KB to bundle):
import moment from 'moment';
const formatted = moment(date).format('MMM DD, YYYY');

// After (adds ~3KB for just this function):
import { format } from 'date-fns';
const formatted = format(new Date(date), 'MMM dd, yyyy');

Inline Requires for Module Loading

Metro supports inline requires, a Babel plugin that moves module imports to the first point of use instead of the top of the file. This delays the execution of module code until it is actually needed, improving cold start time even without true code splitting.

Enable inline requires in your metro.config.js or babel.config.js. This is one of the easiest wins for startup time — Meta reports it improving startup by 25-40% on large apps like Facebook.

// babel.config.js
module.exports = {
  presets: ['module:metro-react-native-babel-preset'],
  plugins: [
    'transform-inline-requires', // Delays module evaluation
  ],
};

// metro.config.js alternative:
module.exports = {
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: true,
      },
    }),
  },
};

Removing Unused Packages

Over time, projects accumulate dependencies that are no longer used. Tools like depcheck scan your codebase and report packages listed in package.json but never imported. Removing these shrinks the bundle and speeds up install times.

Run npx depcheck and review the output carefully. Some packages are used indirectly (as peer dependencies or via config files), so check before removing them. After cleanup, run a full build to confirm nothing broke.

# Check for unused dependencies
npx depcheck

# Output example:
# Unused dependencies:
# * react-native-camera (replaced by expo-camera)
# * lodash (replaced by individual imports)
# * @types/moment (moment was removed)

# Remove them:
npm uninstall react-native-camera lodash @types/moment

Analyzing Build Size by Platform

For iOS, open the .ipa file (it is a ZIP) and measure Payload/YourApp.app/main.jsbundle. For Android, open the .aab or .apk with bundletool or the Android Studio APK Analyzer to see the size breakdown.

EAS Build provides a bundle size history in the Expo dashboard, making it easy to track size regressions across builds. Set up a CI check that fails if the bundle size grows beyond a threshold to catch accidental size increases.

# Unzip the IPA to measure iOS bundle
unzip -o MyApp.ipa -d /tmp/ipa-contents
ls -lh '/tmp/ipa-contents/Payload/MyApp.app/main.jsbundle'

# Android APK Analyzer from the command line
# (Android Studio → Build → Analyze APK)
# Or use bundletool:
bundletool dump resources --bundle=app.aab

Measure Before Releasing

Before each production release, add bundle size measurement to your CI pipeline. A simple size check in a GitHub Actions workflow can flag when a pull request adds a significant amount of bytes to the bundle.

Combine size monitoring with app startup timing using Firebase Performance Monitoring's automatic trace for app_start. This gives you data from real devices in production, showing whether your optimizations are working for users on slower hardware.

# Example GitHub Actions step to fail if bundle exceeds 3MB
- name: Check bundle size
  run: |
    npx react-native bundle \
      --platform android \
      --dev false \
      --entry-file index.js \
      --bundle-output /tmp/bundle.js
    SIZE=$(wc -c < /tmp/bundle.js)
    MAX=3145728  # 3MB
    if [ $SIZE -gt $MAX ]; then
      echo "Bundle too large: $SIZE bytes (limit: $MAX)"
      exit 1
    fi

Profiling Startup Time

Bundle size is one factor in startup time; another is how much JS executes before the first screen renders. Use the Hermes Debugger Timeline in Flipper to record a cold start and see which modules take the longest to initialize.

Look for any synchronous module side effects — code at the top level of a module that runs when the module is first imported. Move expensive initialization into lazy functions called on demand, not at import time.

// ❌ Side effect at module level — runs at import time
const data = expensiveComputation(); // Blocks startup
export default function MyModule() { ... }

// ✅ Lazy initialization — only runs when called
let data: any = null;
function getdata() {
  if (!data) data = expensiveComputation();
  return data;
}
export default function MyModule() {
  const d = getdata(); // Called on first use, not at import
  ...
}

Quick Check

Test your understanding of React Native Mobile Development concepts from this lesson.

Lesson Recap

In this lesson you learned: how to measure bundle size and visualize its composition with react-native-bundle-visualizer, how named imports and tree shaking reduce unnecessary code in the bundle, and how React.lazy and dynamic import defer loading heavy screens until they are needed. Next up we set up Jest and write our first unit tests for React Native.

Frequently asked questions

Is the “Bundle Size and Lazy Loading” lesson free?

Yes — the full text of “Bundle Size and Lazy Loading” is free to read here on the web, and the React Native Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the React Native Academy course, upgrade to CoddyKit PRO.

What will I learn in “Bundle Size and Lazy Loading”?

Analyze the JS bundle with react-native-bundle-visualizer, identify large dependencies, apply dynamic import() for heavy screens, and measure the before/after bundle size. You practise React Native Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start React Native Academy?

No prior experience is required. React Native Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Bundle Size and Lazy Loading” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this React Native Academy lesson?

Yes. Every React Native Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Profiling with Flipper and React DevTools
  2. Memoization with React.memo, useCallback, useMemo
  3. FlatList Performance Tuning
  4. Bundle Size and Lazy Loading
← Back to React Native Academy