Skip to content

Latest commit

 

History

History
214 lines (155 loc) · 5.39 KB

File metadata and controls

214 lines (155 loc) · 5.39 KB
title Tree Shaking
impact HIGH
tags bundle, tree-shaking, dead-code, metro, repack

Skill: Tree Shaking

Enable dead code elimination to remove unused exports from your JavaScript bundle.

Quick Config

# .env (Expo SDK 52+)
EXPO_UNSTABLE_METRO_OPTIMIZE_GRAPH=1
EXPO_UNSTABLE_TREE_SHAKING=1
// metro.config.js
config.transformer.getTransformOptions = async () => ({
  transform: { experimentalImportSupport: true },
});
// babel.config.js (non-Expo projects must set `disableImportExportTransform`)
module.exports = {
  presets: [
    [
      'module:@react-native/babel-preset',
      { disableImportExportTransform: true },
    ],
  ],
};

When to Use

  • Bundle includes unused library code
  • Want automatic barrel export optimization
  • Using Expo SDK 52+ (recommended approach)
  • Project already uses Re.Pack (@callstack/repack)

Platform Support

Bundler Tree Shaking Notes
Metro ❌ No Use metro-serializer-esbuild
Expo (SDK 52+) ✅ Experimental Requires config
Re.Pack ✅ Yes Built-in via Webpack/Rspack

Setup: Expo SDK 52+

1. Enable Import Support

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

const config = getDefaultConfig(__dirname);

config.transformer.getTransformOptions = async () => ({
  transform: {
    experimentalImportSupport: true,
  },
});

module.exports = config;

2. Enable Tree Shaking

Create/edit .env:

EXPO_UNSTABLE_METRO_OPTIMIZE_GRAPH=1
EXPO_UNSTABLE_TREE_SHAKING=1

Note: Only applies in production builds.

Setup: Metro with ESBuild

npm install @rnx-kit/metro-serializer-esbuild

Configure in metro.config.js.

Setup: Re.Pack (Only If Already Installed)

Important: Re.Pack (@callstack/repack) is a custom bundler using Rspack/Webpack. Only apply these instructions if the project already has Re.Pack configured. Do not suggest migrating a codebase to Re.Pack—it's rarely necessary and requires significant setup.

If project has @callstack/repack in dependencies:

Tree shaking is enabled by default with Rspack. Verify in config:

// rspack.config.js or webpack.config.js
module.exports = {
  optimization: {
    usedExports: true, // Mark unused exports
    minimize: true, // Remove during minification
  },
};

Platform Shaking

Code inside Platform.OS and Platform.select checks is removed for other platforms:

// IMPORTANT: import Platform directly from 'react-native'
import { Platform } from 'react-native';

if (Platform.OS === 'ios') {
  // Removed from Android bundle
}

if (Platform.select({ ios: true, android: false }) === 'ios') {
  // Removed from Android bundle
}

Critical: Must use direct import. This does NOT work:

import * as RN from 'react-native';
if (RN.Platform.OS === 'ios') {
  // NOT removed - optimization fails
}

For non-Expo projects, requires both experimentalImportSupport: true in Metro config and disableImportExportTransform: true in Babel config.

Impact: Savings from enabling platform shaking on a bare React Native Community CLI project are:

  • 5% smaller Hermes bytecode (2.79 MB → 2.64 MB)
  • 15% smaller minified JS bundle (1 MB → 0.85 MB)

Requirements for Tree Shaking

ESM Imports Required

// ✅ ESM - Tree shakeable
import { foo } from './module';

// ❌ CommonJS - Not tree shakeable
const { foo } = require('./module');

Side Effects Declaration

Libraries must declare side-effect-free in package.json:

{
  "sideEffects": false
}

Or specify files with side effects:

{
  "sideEffects": ["*.css", "./src/polyfills.js"]
}

Size Impact

Bundle Type Metro (MB) Re.Pack (MB) Change
Production 35.63 38.48 +8%
Prod Minified 15.54 13.36 -14%
Prod HBC 21.79 19.35 -11%
Prod Minified HBC 21.62 19.05 -12%

Expected improvement: 10-15% bundle size reduction.

Verification

  1. Build production bundle (see bundle-analyze-js.md)
  2. Analyze with source-map-explorer (see bundle-analyze-js.md)
  3. Search for functions you know are unused
  4. If found → tree shaking not working

Test Example

// test-treeshake.js
export const usedFunction = () => 'used';
export const unusedFunction = () => 'unused'; // Should be removed

// app.js
import { usedFunction } from './test-treeshake';

After building, search bundle for unusedFunction. Should not exist.

Common Pitfalls

  • Not using production build: Tree shaking only in prod
  • CommonJS modules: Need ESM for full effectiveness
  • Side effects not declared: Library may not be shakeable
  • Dynamic imports: require(variable) prevents analysis
  • Babel/Metro config mismatch: disableImportExportTransform must match experimentalImportSupport

Related Skills