树摇与代码移除
编辑
了解 Expo CLI 如何优化生产 JavaScript 包。
树摇(也称为 死代码移除)是一种从生产包中移除未使用代码的技术。 Expo CLI 采用不同的技术,包括 minification,通过移除未使用的代码来提高启动时间。
平台摇动
Expo CLI 采用称为 平台摇动 的过程进行应用打包,为每个平台(Android、iOS、web)创建单独的包。它确保代码仅在一个平台上使用,并从其他平台中移除。
任何基于 react-native 的 Platform 模块条件使用的代码都将从其他平台中移除。然而,这种排除特别适用于直接从 react-native 中导入的 Platform.select 和 Platform.OS 实例。如果这些通过不同的模块重新导出,则它们在打包过程中不会被移除。
例如,考虑以下变换输入:
import { Platform } from 'react-native'; if (Platform.OS === 'ios') { console.log('Hello on iOS'); }
生产包将移除基于平台的条件:
%%placeholder-start%%Empty on Android %%placeholder-end%%
console.log('Hello on iOS');
此优化仅适用于生产,并逐文件运行。如果从不同的模块重新导出 Platform.OS,则不会从生产包中移除。
process.env.EXPO_OS 可用于检测 JavaScript 为哪个平台打包(运行时无法改变)。由于 Metro 在依赖解析后压缩代码,此值不支持平台摇动导入。
移除仅开发使用的代码
在您的项目中,可能有代码是为了帮助开发过程而设计的。它应该从生产包中排除。要处理这些情况,请使用 process.env.NODE_ENV 环境变量或非标准的 __DEV__ 全局布尔值。
1
例如,以下代码片段将从生产包中删除:
if (process.env.NODE_ENV === 'development') { console.log('Hello in development'); } if (__DEV__) { console.log('Another development-only conditional...'); }
2
在 常量折叠 发生后,可以静态评估条件:
if ('production' === 'development') { console.log('Hello in development'); } if (false) { console.log('Another development-only conditional...'); }
3
在 minification 期间,无法访问的条件被移除:
%%placeholder-start%%Empty file %%placeholder-end%%
为了提高速度,Expo CLI 仅在生产构建中执行代码消除。上述代码片段中的条件在开发构建中保留。
自定义代码移除
EXPO_PUBLIC_ 环境变量在压缩过程之前被内联。这意味着它们可以用于从生产包中移除代码。例如:
1
EXPO_PUBLIC_DISABLE_FEATURE=true;
if (!process.env.EXPO_PUBLIC_DISABLE_FEATURE) { console.log('Hello from the feature!'); }
2
上述输入代码片段在 babel-preset-expo 之后被转换为以下内容:
if (!'true') { console.log('Hello from the feature!'); }
3
上述代码片段随后被压缩,从而移除未使用的条件:
// 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
if (typeof window === 'undefined') { console.log('Hello on the server!'); }
2
上一步的输入代码在针对服务器环境(API 路由、服务器渲染)打包时被 babel-preset-expo 转换为以下代码片段:
if (true) { console.log('Hello on the server!'); }
为 Web 或本地应用打包客户端代码时,不会替换 typeof window,除非设置了 minifyTypeOfWindow: true:
if (typeof window === 'undefined') { console.log('Hello on the server!'); }
3
对于服务器环境,上述代码片段随后被压缩,移除未使用的条件:
console.log('Hello on the server!');
if (typeof window === 'undefined') { console.log('Hello on the server!'); } // Empty file
React Native web 导入
babel-preset-expo 为 react-native-web 批量文件提供内置优化。如果您使用 ESM 直接导入 react-native,则批量文件将从生产包中移除。
如果使用静态 import 语法导入 react-native,则批量文件将被移除。
import { View, Image } from 'react-native';
import View from 'react-native-web/dist/exports/View'; import Image from 'react-native-web/dist/exports/Image';
如果使用 require() 导入 react-native,则批量文件将在生产包中保持不变。
const { View, Image } = require('react-native');
const { View, Image } = require('react-native-web');
移除未使用的导入和导出
在 SDK 52 及更高版本中试验性可用。
您可以试验性地启用自动移除未使用的导入和导出跨模块的支持。这对于加速本地 OTA 下载和优化必须使用标准 JavaScript 引擎解析和执行的 Web 性能非常有用。
考虑以下示例代码:
import { ArrowUp } from './icons'; export default function Home() { return <ArrowUp />; }
export function ArrowUp() { /* ... */ } export function ArrowDown() { /* ... */ } export function ArrowRight() { /* ... */ } export function ArrowLeft() { /* ... */ }
由于 index.js 中仅使用 ArrowUp,生产包将移除 icons.js 中的所有其他组件。
export function ArrowUp() { /* ... */ }
此系统可扩展到自动优化应用中所有 import 和 export 语法,跨所有平台。虽然这会导致更小的包,但处理 JS 仍然需要时间和计算机内存,因此避免导入数以百万计的模块。
- 树摇仅在生产包中运行,并且只能在使用
import和export语法的模块上运行。使用module.exports和require的文件将不会被树摇。 - 避免添加将
import/export语法转换为 CJS 的 Babel 插件,例如@babel/plugin-transform-modules-commonjs。这将破坏整个项目的树摇。 - 被标记为副作用的模块将不会从图中移除。
export * from "..."将被扩展和优化,除非导出使用module.exports或exports。- Expo SDK 中的所有模块都作为 ESM 打包并可以彻底树摇。
启用树摇
在 SDK 52 及更高版本中试验性可用。
1
确保 experimentalImportSupport 并确保您的应用可以按预期构建和运行。
注意:在 SDK 54 及更高版本中默认启用。
如何在旧 SDK 版本中启用导入支持?
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,以在整个图创建之前保留模块。确保您的应用在启用此功能时的生产模式下按预期构建和运行,然后再继续。
EXPO_UNSTABLE_METRO_OPTIMIZE_GRAPH=1
这将仅在生产模式下使用。
3
打开环境变量 EXPO_UNSTABLE_TREE_SHAKING=1 以启用该功能。
EXPO_UNSTABLE_TREE_SHAKING=1
这将仅在生产模式下使用。
4
在生产模式下捆绑您的应用,以查看树摇的效果。
- npx expo export此功能是非常实验性的,因为它改变了 Metro 打包代码的基本结构。默认情况下,Metro 会按需和延迟打包一切,以确保最快的开发时间。相反,树摇需要在整个包创建后延迟某些转换。这意味着更少的代码可以被缓存,这通常是可以的,因为树摇是仅在生产中使用的特性,而生产包通常不使用转换缓存。
批量文件
在 SDK 52 及更高版本中试验性可用。
通过 Expo 树摇,星形导出将根据使用情况自动展开并摇动。例如,考虑以下代码片段:
export * from './icons';
优化通道将爬行 ./icons 并将导出添加到当前模块。如果导出未使用,它们将从生产包中移除。
export { ArrowRight, ArrowLeft } from './icons';
这将根据标准树摇规则进行摇动。如果您只导入 ArrowRight,则 ArrowLeft 将从生产包中移除。
如果星形导出拉入模糊的导出,例如 module.exports.ArrowUp 或 exports.ArrowDown,那么优化通道将不会扩展星形导出,并且不会从批量文件中移除任何导出。您可以使用 Expo Atlas 来检查扩展的导出。
您可以使用此策略与如 lucide-react 等库结合,移除您应用中未使用的所有图标。
递归优化
在 SDK 52 及更高版本中试验性可用。
Expo 通过在整个图中递归以寻找未使用的导入来优化模块。考虑以下代码片段:
export function foo() { // 因为这里使用了 bar,所以不能被移除。 bar(); } export function bar() {}
在这种情况下,bar 在 foo 中使用,因此不能被移除。然而,如果 foo 在应用中没有被使用,那么将移除 foo,并将模块重新扫描以查看 bar 是否可以被移除。这个过程会针对给定模块递归执行 5 次,然后由于性能原因退出。
副作用
Expo CLI 根据 Webpack 系统 遵循模块副作用。副作用通常用于定义全局变量(console.log)或修改原型(避免这样做)。
您可以在 package.json 中标记您的模块是否有副作用:
{ "name": "library", "sideEffects": ["./src/*.js"] }
副作用将防止未使用模块的移除,并禁用模块内联,以确保 JS 代码按预期顺序运行。如果副作用为空或仅包含注释和指令("use strict"、"use client" 等),则将被移除。
当启用 Expo 树摇时,您可以安全地在 metro.config.js 中为生产包启用 inlineRequires。这将在评估时延迟加载模块,从而导致更快的启动时间。避免在没有 Expo 树摇的情况下使用此功能,因为它将以可能改变副作用执行顺序的方式移动模块。
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 导入:
import { doSomething } from './dev-only'; if (process.env.NODE_ENV === 'development') { doSomething(); }
在这两种情况下,整个模块在生产包中将是空的。