树摇动和代码删除

了解 Expo CLI 如何优化生产 JavaScript 包。

Android
iOS
tvOS
Web

Tree shaking(也称为_死代码移除_)是一种从生产包中移除未使用代码的技术。Expo CLI 使用不同的技术,包括压缩,通过移除未使用代码来提高启动时间。

🌐 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 在应用打包时采用一种称为 平台摇晃 的流程,它会为每个平台(Android、iOS、Web)创建独立的包。这样可以确保代码只在一个平台上使用,并从其他平台中移除。

🌐 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.

任何基于 react-native 中的 Platform 模块有条件使用的代码都会从其他平台中移除。然而,这种排除特别适用于每个文件中直接从 react-native 导入 Platform.selectPlatform.OS 的情况。如果这些通过其他模块重新导出,它们在为不同平台打包时不会被移除。

🌐 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');

此优化仅用于生产环境,并按每个文件运行。如果你从不同的模块重新导出 Platform.OS,它将不会从生产包中移除。

🌐 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.

process.env.EXPO_OS 可用于检测 JavaScript 打包的目标平台(运行时无法更改)。由于 Metro 在解析依赖后压缩代码的方式,这个值不支持平台精简导入。

🌐 The 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

在你的项目中,可能有一些代码是为了辅助开发过程而设计的。这些代码应该从生产环境的打包中排除。为处理这些情况,可以使用 process.env.NODE_ENV 环境变量或非标准的全局布尔值 __DEV__

🌐 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%%

为了提高速度,Expo CLI 仅在生产构建中执行代码消除。上述代码片段中的条件语句在开发构建中会保留。

🌐 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

EXPO_PUBLIC_ 环境变量在压缩过程之前会被内联。这意味着它们可以用于从生产包中移除代码。例如:

1

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

2

babel-preset-expo 之后,上述输入代码片段被转换为如下内容:

🌐 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

上述代码片段随后被压缩,这会移除未使用的条件:

Post minifier
// Empty file
  • 该系统不适用于服务器端代码,因为环境变量不会内联到服务器打包包中。
  • 库作者不应使用 EXPO_PUBLIC_ 环境变量,因为出于安全原因,它们只在应用代码中运行。

移除服务器代码

🌐 Removing server code

在服务器和客户端环境中,有时会使用 typeof window === 'undefined' 来有条件地启用或禁用代码,这很常见。

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

babel-preset-expo 在为服务器环境打包时会将 typeof window === 'undefined' 转换为 true。默认情况下,在为 Web 客户端环境打包时,此检查保持不变。该转换会在开发和生产环境中运行,但仅在生产环境中移除条件性 require。

你可以通过传入 { minifyTypeofWindow: true } 来配置 babel-preset-expo 以启用此转换。默认情况下,即使在网页环境中,该转换仍然保持禁用,因为网页工作线程不会拥有 window 全局对象。

🌐 You can configure babel-preset-expo to enable this transform by passing { minifyTypeofWindow: true }. By default, this transform remains disabled even for web environments since web workers won't have a window global.

1

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

2

在为服务器环境(API 路由、服务器渲染)打包时,前一步的输入代码在 babel-preset-expo 之后被转换为以下代码片段:

🌐 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!'); }

将客户端代码打包用于 Web 或本地应用不会替代 typeof window,除非设置了 minifyTypeOfWindow: true

🌐 Bundling client code for web or native apps will not replace typeof window unless minifyTypeOfWindow: true is set:

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

3

对于服务器环境,上述代码片段随后会被压缩,从而移除未使用的条件:

🌐 For server environments, the above code snippet is then minified which removes the unused conditional:

Post minifier (server)
console.log('Hello on the server!');
Post minifier (client)
if (typeof window === 'undefined') { console.log('Hello on the server!'); } // Empty file

React Native 网络导入

🌐 React Native web imports

babel-preset-exporeact-native-web 桶文件提供了内置优化。如果你直接使用 ESM 导入 react-native,那么桶文件将会从生产包中移除。

如果你使用静态 import 语法导入 react-native,桶文件将被移除。

🌐 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';

如果你使用 require() 导入 react-native,在生产包中桶文件将保持不变。

🌐 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');

删除未使用的导入和导出

🌐 Remove unused imports and exports

在 SDK 52 及更高版本中可实验使用。

你可以尝试启用自动删除模块间未使用的导入和导出功能。这对于加快本地 OTA 下载速度以及优化需要使用标准 JavaScript 引擎解析和执行的网页性能非常有用。

🌐 You can experimentally enable support for automatically removing unused imports and exports across modules. This is useful for speeding up native OTA downloads and optimizing web performance where JavaScript must be parsed and executed using a standard JavaScript engine.

考虑以下示例代码:

🌐 Consider the following example code:

index.js
import { ArrowUp } from './icons'; export default function Home() { return <ArrowUp />; }
icons.js
export function ArrowUp() { /* ... */ } export function ArrowDown() { /* ... */ } export function ArrowRight() { /* ... */ } export function ArrowLeft() { /* ... */ }

由于在 index.js 中只使用了 ArrowUp,生产包将会从 icons.js 中移除所有其他组件。

🌐 Since only ArrowUp is used in index.js, the production bundle will remove all other components from icons.js.

icons.js (Output)
export function ArrowUp() { /* ... */ }

该系统可以扩展,以自动优化你应用中所有平台的所有 importexport 语法。虽然这会生成更小的包,但处理 JS 仍然需要时间和计算机内存,因此应避免导入数百万个模块。

🌐 This system scales up to automatically optimize all import and export syntax in your app, across all platforms. While this results in smaller bundles, processing JS still requires time and computer memory so avoid importing millions of modules.

  • Tree-shaking 仅在生产包中运行,并且只能在使用 importexport 语法的模块上运行。使用 module.exportsrequire 的文件不会被进行 tree-shaking。
  • 避免添加诸如 @babel/plugin-transform-modules-commonjs 之类的 Babel 插件,这些插件会将 import/export 语法转换为 CJS。这会破坏整个项目的 tree-shaking 功能。
  • 被标记为副作用的模块不会从图中移除。
  • export * from "..." 将被扩展和优化,除非导出使用 module.exportsexports
  • Expo SDK 中的所有模块都以 ESM 形式提供,并且可以被彻底进行 tree-shaking。

启用树摇优化

🌐 Enabling tree shaking

在 SDK 52 及更高版本中可实验使用。

1

确保 experimentalImportSupport 并确保你的应用可以按预期构建和运行。

🌐 Ensure experimentalImportSupport and ensure your app builds and runs as expected.

信息 注意:在 SDK 54 及更高版本中默认启用。

如何在较旧的 SDK 版本中启用导入支持?
metro.config.js
const { getDefaultConfig } = require('expo/metro-config'); const config = getDefaultConfig(__dirname); config.transformer.getTransformOptions = async () => ({ transform: { experimentalImportSupport: true, }, }); module.exports = config;

实验性导入支持使用自定义版本的 @babel/plugin-transform-modules-commonjs 插件。这显著减少了解析次数并简化了输出包。此功能可以与 inlineRequires 一起使用,以进一步实验性地优化你的打包文件。

🌐 Experimental import support uses a custom version of the @babel/plugin-transform-modules-commonjs plugin. This drastically reduces the number of resolutions and simplifies your output bundle. This feature can be used with inlineRequires to further optimize your bundle experimentally.

2

切换环境变量 EXPO_UNSTABLE_METRO_OPTIMIZE_GRAPH=1 以保留模块,直到整个图表创建完成。在继续之前,确保你的应用在启用此功能时在生产环境中能够正常构建和运行。

🌐 Toggle on the environment variable EXPO_UNSTABLE_METRO_OPTIMIZE_GRAPH=1 to keep modules around until the entire graph is created. Ensure your app builds and runs as expected in production with this feature enabled before continuing.

.env
EXPO_UNSTABLE_METRO_OPTIMIZE_GRAPH=1

这只会在生产模式下使用。

🌐 This will only be used in production mode.

3

开启环境变量 EXPO_UNSTABLE_TREE_SHAKING=1 以启用该功能。

🌐 Toggle on the environment variable EXPO_UNSTABLE_TREE_SHAKING=1 to enable the feature.

.env
EXPO_UNSTABLE_TREE_SHAKING=1

这只会在生产模式下使用。

🌐 This will only be used in production mode.

4

在生产模式下打包你的应用以查看树摇优化的效果。

🌐 Bundle your app in production mode to see the effects of tree shaking.

Terminal
npx expo export

这个功能非常实验性,因为它改变了 Metro 打包代码的基本结构。默认情况下,Metro 会按需和惰性打包所有内容,以确保尽可能快的开发时间。相比之下,tree shaking 需要在整个包创建完成后才能进行某些转换。这意味着可以缓存的代码更少,但通常没关系,因为 tree shaking 只是生产环境的功能,而且生产环境的包通常不使用转换缓存。

🌐 This feature is very experimental because it changes the fundamental structure of how Metro bundles code. By default, Metro bundles everything on-demand and lazily to ensure the fastest possible development times. In contrast, tree shaking requires some transformation to be delayed until after the entire bundle has been created. This means less code can be cached, which is generally fine because tree shaking is a production-only feature and production bundles often don't use transform caches.

桶文件

🌐 Barrel files

在 SDK 52 及更高版本中可实验使用。

使用 Expo 的 tree shaking 时,星号导出将根据使用情况自动展开和摇晃。例如,考虑以下代码片段:

🌐 With Expo tree shaking, star exports will automatically be expanded and shaken based on usage. For example, consider the following code snippet:

Input
export * from './icons';

优化过程将遍历 ./icons 并将导出添加到当前模块。如果这些导出未被使用,它们将从生产包中移除。

🌐 The optimization pass will crawl ./icons and add the exports to the current module. If the exports are unused, they will be removed from the production bundle.

Expanded
export { ArrowRight, ArrowLeft } from './icons';

这将根据标准的 tree shaking 规则进行处理。如果你只导入 ArrowRight,那么 ArrowLeft 将从生产包中移除。

🌐 This will be shaken according to standard tree shaking rules. If you only import ArrowRight, then ArrowLeft will be removed from the production bundle.

如果星号导入引入了模糊的导出,例如 module.exports.ArrowUpexports.ArrowDown,那么优化过程将不会展开星号导入,并且不会从总出口文件中移除任何导出。你可以使用 Expo Atlas 来查看展开后的导出。

🌐 If the star export pulls in ambiguous exports such as module.exports.ArrowUp or exports.ArrowDown, then the optimization pass will not expand the star export and no exports will be removed from the barrel file. You can use Expo Atlas to inspect the expanded exports.

你可以使用这个策略结合像 lucide-react 这样的库来移除应用中未使用的所有图标。

🌐 You can use this strategy with libraries like lucide-react to remove all icons that are not used in your app.

递归优化

🌐 Recursive optimizations

在 SDK 52 及更高版本中可实验使用。

Expo 通过彻底递归图表来优化模块,以查找未使用的导入。考虑以下代码片段:

🌐 Expo optimizes a module by recursing through the graph exhaustively to find unused imports. Consider the following code snippet:

Input
export function foo() { // Because bar is used here, it cannot be removed. bar(); } export function bar() {}

在这种情况下,bar 被用于 foo,所以它不能被删除。然而,如果 foo 在应用中没有任何地方被使用,那么 foo 将被删除,模块将被重新扫描,以查看 bar 是否可以被删除。对于给定的模块,这个过程会递归进行 5 次,然后由于性能原因退出。

🌐 In this case, bar is used in foo, so it cannot be removed. However, if foo is not used anywhere in the app, then foo will be removed and the module will be scanned again to see if bar can be removed. This process recurses 5 times for a given module before bailing out due to performance reasons.

副作用

🌐 Side-effects

Expo CLI 会根据 Webpack 系统 处理模块副作用。副作用通常用于定义全局变量(console.log)或修改原型(应避免这样做)。

🌐 Expo CLI respects module side-effects according to the Webpack system. Side-effects are generally used for defining global variables (console.log) or modifying prototypes (avoid doing this).

你可以在 package.json 中标注你的模块是否有副作用:

🌐 You can mark if your module has side-effects in the package.json:

package.json
{ "name": "library", "sideEffects": ["./src/*.js"] }

副作用将阻止移除未使用的模块并禁用模块内联,以确保 JS 代码按预期顺序运行。如果副作用为空或仅包含注释和指令("use strict""use client" 等),它们将被移除。

🌐 Side-effects will prevent the removal of unused modules and disable module inlining to ensure JS code runs in the expected order. Side-effects will be removed if they're empty or contain only comments and directives ("use strict", "use client", and so on).

启用 Expo tree shaking 后,你可以安全地在 metro.config.js 中为生产包启用 inlineRequires。这将在模块被评估时按需加载,从而加快启动时间。如果没有启用 Expo tree shaking,请避免使用此功能,因为它会移动模块位置,从而可能改变副作用的执行顺序。

🌐 When Expo tree shaking is enabled, you can safely enable inlineRequires in your metro.config.js for production bundles. This will lazily load modules when they're evaluated, leading to faster startup time. Avoid using this feature without Expo tree shaking as it will move modules around in ways that can change the execution order of side-effects.

metro.config.js
const { getDefaultConfig } = require('expo/metro-config'); const config = getDefaultConfig(__dirname); config.transformer.getTransformOptions = async () => ({ transform: { experimentalImportSupport: true, inlineRequires: true, }, }); module.exports = config;

针对摇树进行优化

🌐 Optimizing for tree shaking

在 Expo tree shake 之前,React Native 库会通过将导入封装在条件块中来删除导入,例如:

🌐 Before Expo tree shaking, React Native libraries would remove imports by wrapping them in conditional blocks such as:

if (process.env.NODE_ENV === 'development') { require('./dev-only').doSomething(); }

这是有问题的,因为你没有准确的 TypeScript 支持,并且这使得图表不明确,因为你无法静态分析代码。启用 Expo tree shaking 后,你可以重构此代码以使用 ESM 导入:

🌐 This is problematic because you don't have accurate TypeScript support and it makes the graph ambiguous since you cannot statically analyze the code. With Expo tree shaking enabled, you can restructure this code to use ESM imports:

Input
import { doSomething } from './dev-only'; if (process.env.NODE_ENV === 'development') { doSomething(); }

在这两种情况下,整个模块在生产打包包中都是空的。

🌐 In both cases, the entire module will be empty in production bundles.