树摇与代码移除

编辑

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

Android
iOS
tvOS
Web

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

平台摇动

Expo CLI 采用称为 平台摇动 的过程进行应用打包,为每个平台(Android、iOS、web)创建单独的包。它确保代码仅在一个平台上使用,并从其他平台中移除。

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

例如,考虑以下变换输入:

Input
import { Platform } from 'react-native'; if (Platform.OS === 'ios') { console.log('Hello on iOS'); }

生产包将移除基于平台的条件:

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

此优化仅适用于生产,并逐文件运行。如果从不同的模块重新导出 Platform.OS,则不会从生产包中移除。

process.env.EXPO_OS 可用于检测 JavaScript 为哪个平台打包(运行时无法改变)。由于 Metro 在依赖解析后压缩代码,此值不支持平台摇动导入。

移除仅开发使用的代码

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

1

例如,以下代码片段将从生产包中删除:

Input
if (process.env.NODE_ENV === 'development') { console.log('Hello in development'); } if (__DEV__) { console.log('Another development-only conditional...'); }

2

常量折叠 发生后,可以静态评估条件:

Post constants folding
if ('production' === 'development') { console.log('Hello in development'); } if (false) { console.log('Another development-only conditional...'); }

3

minification 期间,无法访问的条件被移除:

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

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

自定义代码移除

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 之后被转换为以下内容:

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

3

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

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

移除服务器代码

通常使用 typeof window === 'undefined' 有条件地启用或禁用服务器和客户端环境的代码。

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

您可以通过传递 { minifyTypeofWindow: true } 来配置 babel-preset-expo 以启用此变换。 默认情况下,此变换在 Web 环境中保持禁用,因为 Web Worker 不会有 window 全局。

1

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

2

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

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

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

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

3

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

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 web 导入

babel-preset-exporeact-native-web 批量文件提供内置优化。如果您使用 ESM 直接导入 react-native,则批量文件将从生产包中移除。

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

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,则批量文件将在生产包中保持不变。

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

移除未使用的导入和导出

在 SDK 52 及更高版本中试验性可用。

您可以试验性地启用自动移除未使用的导入和导出跨模块的支持。这对于加速本地 OTA 下载和优化必须使用标准 JavaScript 引擎解析和执行的 Web 性能非常有用。

考虑以下示例代码:

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 中的所有其他组件。

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

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

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

启用树摇

在 SDK 52 及更高版本中试验性可用。

1

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

注意:在 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 一起使用,以进一步实验性地优化您的包。

2

打开环境变量 EXPO_UNSTABLE_METRO_OPTIMIZE_GRAPH=1,以在整个图创建之前保留模块。确保您的应用在启用此功能时的生产模式下按预期构建和运行,然后再继续。

.env
EXPO_UNSTABLE_METRO_OPTIMIZE_GRAPH=1

这将仅在生产模式下使用。

3

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

.env
EXPO_UNSTABLE_TREE_SHAKING=1

这将仅在生产模式下使用。

4

在生产模式下捆绑您的应用,以查看树摇的效果。

Terminal
npx expo export

此功能是非常实验性的,因为它改变了 Metro 打包代码的基本结构。默认情况下,Metro 会按需和延迟打包一切,以确保最快的开发时间。相反,树摇需要在整个包创建后延迟某些转换。这意味着更少的代码可以被缓存,这通常是可以的,因为树摇是仅在生产中使用的特性,而生产包通常不使用转换缓存。

批量文件

在 SDK 52 及更高版本中试验性可用。

通过 Expo 树摇,星形导出将根据使用情况自动展开并摇动。例如,考虑以下代码片段:

Input
export * from './icons';

优化通道将爬行 ./icons 并将导出添加到当前模块。如果导出未使用,它们将从生产包中移除。

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

这将根据标准树摇规则进行摇动。如果您只导入 ArrowRight,则 ArrowLeft 将从生产包中移除。

如果星形导出拉入模糊的导出,例如 module.exports.ArrowUpexports.ArrowDown,那么优化通道将不会扩展星形导出,并且不会从批量文件中移除任何导出。您可以使用 Expo Atlas 来检查扩展的导出。

您可以使用此策略与如 lucide-react 等库结合,移除您应用中未使用的所有图标。

递归优化

在 SDK 52 及更高版本中试验性可用。

Expo 通过在整个图中递归以寻找未使用的导入来优化模块。考虑以下代码片段:

Input
export function foo() { // 因为这里使用了 bar,所以不能被移除。 bar(); } export function bar() {}

在这种情况下,barfoo 中使用,因此不能被移除。然而,如果 foo 在应用中没有被使用,那么将移除 foo,并将模块重新扫描以查看 bar 是否可以被移除。这个过程会针对给定模块递归执行 5 次,然后由于性能原因退出。

副作用

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

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

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

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

当启用 Expo 树摇时,您可以安全地在 metro.config.js 中为生产包启用 inlineRequires。这将在评估时延迟加载模块,从而导致更快的启动时间。避免在没有 Expo 树摇的情况下使用此功能,因为它将以可能改变副作用执行顺序的方式移动模块。

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;

针对树摇的优化

在 Expo 树摇之前,React Native 库会通过将导入包装在条件块中来移除导入,如下所示:

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

这很成问题,因为您没有准确的 TypeScript 支持,且这使图变得模糊,因为您无法静态分析代码。启用 Expo 树摇后,您可以重构此代码以使用 ESM 导入:

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

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