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.
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:
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:
%%placeholder-start%%Empty on Android %%placeholder-end%%
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.
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:
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:
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.
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:
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:
if ('production' === 'development') {
console.log('Hello in development');
}
if (false) {
console.log('Another development-only conditional...');
}
3
The unreachable conditions are removed during minification:
%%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.
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
EXPO_PUBLIC_DISABLE_FEATURE=true;
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
:
if (!'true') {
console.log('Hello from the feature!');
}
3
The above code snippet is then minified, which removes the unused conditional:
// Empty file
EXPO_PUBLIC_
environment variables as they only run in application code for security reasons.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
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):
if (true) {
console.log('Hello on the server!');
}
Bundling client code for web browsers will change typeof window
to false
:
if (false) {
console.log('Hello on the server!');
}
Bundling client code for native apps will leave typeof window
in place:
if (typeof window === 'undefined') {
console.log('Hello on the server!');
}
3
The above code snippet is then minified, which removes the unused conditional:
console.log('Hello on the server!');
// Empty file
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.
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:
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.
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.
import { View, Image } from 'react-native';
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.
const { View, Image } = require('react-native');
const { View, Image } = require('react-native-web');