HomeGuidesReferenceLearn
ArchiveExpo SnackDiscord and ForumsNewsletter

Tree shaking and code removal

Learn about how Expo CLI optimizes production JavaScript bundles.


Tree shaking (also referred to as dead code removal) is a technique to remove unused code from the production bundle. Expo CLI employs different techniques, including minification, to improve startup time by removing unused code.

Platform shaking

Expo CLI employs a process known as platform shaking for app bundling, where it creates separate bundles for each platform (Android, iOS, web). It ensures that the code is only used on one platform and is removed from other platforms.

Any code that is used conditionally based on the Platform module from react-native is removed from the other platforms. However, this exclusion specifically applies to instances where Platform.select and Platform.OS are directly imported from react-native in each file. If these are re-exported through a different module, they will not be removed during the bundling process for different platforms.

For example, consider the following transformation input:

Input
import { Platform } from 'react-native';

if (Platform.OS === 'ios') {
  console.log('Hello on iOS');
}

The production bundle will remove the conditional based on the platform:

Output (Android)
%%placeholder-start%%Empty on Android %%placeholder-end%%
Output (iOS)
console.log('Hello on iOS');

This optimization is production only and runs on a per-file basis. If you re-export Platform.OS from a different module, it will not be removed from the production bundle.

Enable platform shaking (SDK 49 and lower)

Platform shaking is enabled by default in Expo SDK 50 and greater.

To remove code based on the Platform module from react-native, add the following to metro.config.js:

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

const config = getDefaultConfig(__dirname);

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

module.exports = config;

Then, configure babel.config.js to preserve import/export syntax:

babel.config.js
module.exports = function (api) {
  api.cache(true);
  const disableImportExportTransform = true;
  return {
    presets: [
      [
        'babel-preset-expo',
        {
          native: {
            disableImportExportTransform,
          },
          web: {
            disableImportExportTransform,
          },
        },
      ],
    ],
  };
};

Starting in SDK 51, process.env.EXPO_OS can be used to detect the platform that the JavaScript was bundled for (cannot change at runtime). This value does not support platform shaking imports due to how Metro minifies code after dependency resolution.

Remove development-only code

In your project, there might be code designed to help with the development process. It should be excluded from the production bundle. To handle these scenarios, use the process.env.NODE_ENV environment variable or the non-standard __DEV__ global boolean.

1

For example, the following code snippet will be removed from the production bundle:

Input
if (process.env.NODE_ENV === 'development') {
  console.log('Hello in development');
}

if (__DEV__) {
  console.log('Another development-only conditional...');
}

2

After constants folding takes place, the conditions can be evaluated statically:

Post constants folding
if ('production' === 'development') {
  console.log('Hello in development');
}

if (false) {
  console.log('Another development-only conditional...');
}

3

The unreachable conditions are removed during minification:

Output (production)
%%placeholder-start%%Empty file %%placeholder-end%%

To improve speed, Expo CLI only performs code elimination in production builds. Conditionals from the above code snippet are kept in development builds.

Custom code removal

With Expo SDK 50, EXPO_PUBLIC_ environment variables are inlined before the minification process. This means they can be used to remove code from the production bundle. For example:

1

.env
EXPO_PUBLIC_DISABLE_FEATURE=true;
Input
if (!process.env.EXPO_PUBLIC_DISABLE_FEATURE) {
  console.log('Hello from the feature!');
}

2

The above input code snippet is transformed to the following after babel-preset-expo:

Post babel-preset-expo
if (!'true') {
  console.log('Hello from the feature!');
}

3

The above code snippet is then minified, which removes the unused conditional:

Post minifier
// Empty file
  • This system does not apply to server code as environment variables are not inlined in server bundles.
  • Library authors should not use EXPO_PUBLIC_ environment variables as they only run in application code for security reasons.

Removing server code

SDK 51 and greater

It's common to use typeof window === 'undefined' to conditionally enable or disable code for server and client environments.

babel-preset-expo automatically transforms typeof window === 'undefined' to true when bundling for server environments and false when bundling for websites. The check remains unchanged when bundling for native client environments to support apps that polyfill window. This transform runs in both development and production but only removes conditional requires in production.

You can configure babel-preset-expo to skip the transform by passing { preserveTypeofWindow: false }.

1

Input
if (typeof window === 'undefined') {
  console.log('Hello on the server!');
}

2

The input code from the previous step is transformed to the following code snippet after babel-preset-expo when bundling for server environments (API routes, server rendering):

Post babel-preset-expo (bundling for server)
if (true) {
  console.log('Hello on the server!');
}

Bundling client code for web browsers will change typeof window to false:

Post babel-preset-expo
if (false) {
  console.log('Hello on the server!');
}

Bundling client code for native apps will leave typeof window in place:

Post babel-preset-expo
if (typeof window === 'undefined') {
  console.log('Hello on the server!');
}

3

The above code snippet is then minified, which removes the unused conditional:

Post minifier (server)
console.log('Hello on the server!');
Post minifier (client websites)
// Empty file

Remove unused imports and exports

As of Expo SDK 50, unused imports and exports are not removed from the production bundle. We plan to add this feature to all platforms in a future release.

Barrel files

As of Expo SDK 50, there are no built-in optimizations for barrel files.

Barrel files export all of the modules in a directory. They are used to make importing modules easier. For example, a component-based icon library does the following:

node_modules/<example library>/index.js
export { default as IconA } from './IconA';
export { default as IconB } from './IconB';
export { default as IconC } from './IconC';

To reduce the bundle size, identify which of these modules you are using and try to import them directly. Learn more in analyzing bundle size.

React Native web imports

babel-preset-expo provides a built-in optimization for the react-native-web barrel file. If you import react-native directly using ESM, then the barrel file will be removed from the production bundle.

If you import react-native using the static import syntax, the barrel file will be removed.

Input
import { View, Image } from 'react-native';
Output (web)
import View from 'react-native-web/dist/exports/View';
import Image from 'react-native-web/dist/exports/Image';

If you import react-native using require(), the barrel file will be left as-is in the production bundle.

Input
const { View, Image } = require('react-native');
Output (web)
const { View, Image } = require('react-native-web');