了解 Expo CLI 如何优化生产 JavaScript 包。
Tree Shaking(也称为死代码删除)是一种从生产包中删除未使用代码的技术。Expo CLI 采用不同的技术(包括 minification),通过删除未使用的代码来缩短启动时间。
¥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.select
和 Platform.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:
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');
此优化仅适用于生产,并且在每个文件的基础上运行。如果你从不同的模块重新导出 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.
Expo SDK 50 及更高版本中默认启用平台震动。
¥Platform shaking is enabled by default in Expo SDK 50 and greater.
要从 react-native
中删除基于 Platform
模块的代码,请将以下内容添加到 Metro.config.js:
¥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;
然后,配置 babel.config.js 以保留 import/export
语法:
¥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,
},
},
],
],
};
};
从 SDK 51 开始,process.env.EXPO_OS
可用于检测打包 JavaScript 的平台(无法在运行时更改)。由于 Metro 在依赖解析后缩小代码的方式,该值不支持平台抖动导入。
¥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
在你的项目中,可能有一些旨在帮助开发过程的代码。它应该被排除在生产包之外。要处理这些场景,请使用 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:
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
minification 期间删除了不可达条件:
¥The unreachable conditions are removed during minification:
%%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 SDK 50,EXPO_PUBLIC_
环境变量在缩小过程之前内联。这意味着它们可用于从生产包中删除代码。例如:
¥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
上述输入代码片段在 babel-preset-expo
之后转换为以下内容:
¥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
该系统不适用于服务器代码,因为环境变量未内联在服务器包中。
¥This system does not apply to server code as environment variables are not inlined in server bundles.
库作者不应使用 EXPO_PUBLIC_
环境变量,因为出于安全原因它们仅在应用代码中运行。
¥Library authors should not use EXPO_PUBLIC_
environment variables as they only run in application code for security reasons.
¥Removing server code
SDK 51 及更高版本
¥SDK 51 and greater
通常使用 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
;在网站打包时,babel-preset-expo
自动将 false
转换。当打包原生客户端环境以支持填充 window
的应用时,该检查保持不变。此转换在开发和生产中运行,但仅消除了生产中的条件需求。
¥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.
你可以通过传递 { preserveTypeofWindow: false }
将 babel-preset-expo
配置为跳过转换。
¥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
在针对服务器环境(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):
if (true) {
console.log('Hello on the server!');
}
打包 Web 浏览器的客户端代码会将 typeof window
更改为 false
:
¥Bundling client code for web browsers will change typeof window
to false
:
if (false) {
console.log('Hello on the server!');
}
打包原生应用的客户端代码将保留 typeof window
:
¥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
¥React Native web imports
babel-preset-expo
为 react-native-web
桶文件提供了内置优化。如果你直接使用 ESM 导入 react-native
,那么 barrel 文件将从生产包中删除。
¥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.
如果使用静态 import
语法导入 react-native
,则桶文件将被删除。
¥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';
如果你使用 require()
导入 react-native
,则桶文件将按原样保留在生产包中。
¥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');
¥Remove unused imports and exports
实验性地在 SDK 52 及以上版本中可用。
¥Experimentally available in SDK 52 and above.
你可以实验性地启用对跨模块自动删除未使用的导入和导出的支持。这对于加快原生 OTA 下载和优化 Web 性能很有用,其中必须使用标准 JavaScript 引擎解析和执行 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:
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
中删除所有其他组件。
¥Since only ArrowUp
is used in index.js
, the production bundle will remove all other components from icons.js
.
export function ArrowUp() {
/* ... */
}
此系统可扩展以自动优化你应用中的所有 import
和 export
语法,适用于所有平台。虽然这会导致打包包更小,但处理 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 仅在生产包中运行,并且只能在使用 import
和 export
语法的模块上运行。使用 module.exports
和 require
的文件不会被树形优化。
¥Tree-shaking only runs in production bundles and can only run on modules that use import
and export
syntax. Files that use module.exports
and require
will not be tree-shaken.
避免添加 Babel 插件,例如将 import
/export
语法转换为 CJS 的 @babel/plugin-transform-modules-commonjs
。这将破坏整个项目的摇树。
¥Avoid adding Babel plugins such as @babel/plugin-transform-modules-commonjs
which convert import
/export
syntax to CJS. This will break tree-shaking across your project.
标记为副作用的模块不会从图表中删除。
¥Modules that are marked as side-effects will not be removed from the graph.
除非导出使用 module.exports
或 exports
,否则 export * from "..."
将进行扩展和优化。
¥export * from "..."
will be expanded and optimized unless the export uses module.exports
or exports
.
Expo SDK 中的所有模块都以 ESM 形式提供,并且可以进行详尽的 tree-shaking。
¥All modules in the Expo SDK are shipped as ESM and can be exhaustively tree-shaken.
¥Enabling tree shaking
实验性地在 SDK 52 及以上版本中可用。
¥Experimentally available in SDK 52 and above.
1
启用 experimentalImportSupport
并确保你的应用按预期构建和运行。你可能会遇到需要循环或混合 CommonJS 和 ESM 导入的问题。
¥Enable experimentalImportSupport
and ensure your app builds and runs as expected. You may experience issues with require cycles or mixing CommonJS and ESM imports.
const { getDefaultConfig } = require('expo/metro-config');
const config = getDefaultConfig(__dirname);
config.transformer.getTransformOptions = async () => ({
transform: {
experimentalImportSupport: true,
},
});
module.exports = config;
实验性导入支持使用 Metro 版本的 @babel/plugin-transform-modules-commonjs
插件。这大大减少了解析数量并简化了输出包。此功能可与 inlineRequires
一起使用,以进一步实验性地优化你的打包包。
¥Experimental import support uses Metro's 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.
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.
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.
-
npx expo export
此功能非常具有实验性,因为它改变了 Metro 打包代码的基本结构。默认情况下,Metro 按需并延迟打包所有内容以确保最快的开发时间。相比之下,tree shake 需要将某些转换延迟到整个打包包创建之后。这意味着可以缓存更少的代码,这通常是没问题的,因为 tree shake 是仅限生产的功能,而生产包通常不使用转换缓存。
¥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 及以上版本中可用。
¥Experimentally available in SDK 52 and above.
使用 Expo tree shake,星号导出将根据使用情况自动扩展和摇动。例如,考虑以下代码片段:
¥With Expo tree shaking, star exports will automatically be expanded and shaken based on usage. For example, consider the following code snippet:
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.
export { ArrowRight, ArrowLeft } from './icons';
这将根据标准摇树规则进行摇动。如果你只导入 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.ArrowUp
或 exports.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 及以上版本中可用。
¥Experimentally available in SDK 52 and above.
Expo 通过详尽地递归图表来查找未使用的导入,从而优化模块。考虑以下代码片段:
¥Expo optimizes a module by recursing through the graph exhaustively to find unused imports. Consider the following code snippet:
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:
{
"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 树摇动后,你可以安全地在 metro.config.js
中为生产包启用 inlineRequires
。这将在评估模块时延迟加载模块,从而缩短启动时间。避免在没有 Expo tree shake 的情况下使用此功能,因为它会以可能改变副作用执行顺序的方式移动模块。
¥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.
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 shake 后,你可以重构此代码以使用 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:
import { doSomething } from './dev-only';
if (process.env.NODE_ENV === 'development') {
doSomething();
}
在这两种情况下,整个模块在生产打包包中都是空的。
¥In both cases, the entire module will be empty in production bundles.