如果你说的是 React Native App 冷启动/首屏(Cold Start → First Screen)优化,建议不要只盯着 JS 代码,而是把链路拆成:
Native 启动 → JS Runtime → JS Bundle → 首屏 JS 执行 → React Render → Native UI → 首屏数据/图片 → 可交互
真正有效的方案,通常是 "测量 → 定位瓶颈 → 分层优化 → 再测量" ,而不是单纯开启 Hermes 或减少几个组件。
一、先建立首屏性能指标
建议至少定义这 5 个指标:
| 指标 | 含义 | 优化目标 |
|---|---|---|
| Native Start | App 进程启动到 RN Runtime 开始 | Native 初始化 |
| JS Start | JS Runtime 开始执行 | Hermes / Bundle |
| JS Bundle Execute | Bundle 开始执行到入口执行完成 | JS 代码量 |
| FCP | 首屏第一次出现有效内容 | React Render |
| TTI | 首屏真正可交互 | JS 主线程 + 数据 |
新版本 RN 已经提供 performance.rnStartupTiming,可以拿到 RN runtime 初始化、JS Bundle entry 执行以及 runtime 初始化结束等时间点。(React Native)
最重要的是:
不要用开发模式测首屏。
一定使用:
makefile
Android:
./gradlew assembleRelease
iOS:
Release configuration
因为 Hermes 的字节码是在构建阶段生成的,Release 构建才能真正反映生产启动性能。(React Native)
二、首屏优化全景图
可以把优化优先级理解成下面这样:
arduino
RN 首屏启动
│
┌─────────────────┼─────────────────┐
↓ ↓ ↓
Native 启动 JS 启动 UI 渲染
│ │ │
Splash/SDK Hermes React
初始化 Bundle Component
SoLoader Module Navigation
Firebase import List
埋点 SDK 执行 Image
│ │ │
└─────────────────┼─────────────────┘
↓
首屏数据加载
│
┌──────────┼──────────┐
↓ ↓ ↓
API Image Storage
│ │ │
并行化 缩略图 MMKV
缓存 CDN SQLite
三、第一优先级:Hermes
如果项目还没使用 Hermes,优先级最高。
Hermes 本身就是针对 React Native 启动性能设计的 JS Engine,核心优势之一是提前生成 bytecode,减少运行时 JS 解析/编译成本。(React Native)
检查:
ini
const isHermes = !!global.HermesInternal;
console.log('Hermes:', isHermes);
但要注意:
markdown
HermesInternal 存在
≠
一定正在使用最优 Hermes bytecode
生产环境应该确认实际加载的是 Hermes .hbc。
四、第二优先级:干掉首屏无关 JS
这是很多大型 RN App 最大的瓶颈。
例如你的入口:
javascript
import React from 'react';
import './polyfills';
import { Provider } from 'react-redux';
import { NavigationContainer } from '@react-navigation/native';
import Analytics from './analytics';
import Firebase from './firebase';
import Sentry from './sentry';
import UserService from './user';
import PaymentService from './payment';
import Home from './screens/Home';
import Profile from './screens/Profile';
import Settings from './screens/Settings';
import Mall from './screens/Mall';
import Order from './screens/Order';
问题是:
用户还没看到 Home,就已经加载了一堆东西。
应该把首屏依赖控制成:
diff
index
↓
App
↓
Home
↓
首屏必要组件
而不是:
css
index
↓
App
├── Home
├── Profile
├── Settings
├── Payment
├── Mall
├── Order
├── Analytics
├── Firebase
├── ...
└── 5000 modules
五、使用 lazy import / 动态加载
例如:
javascript
const ProfileScreen = React.lazy(
() => import('./screens/Profile')
);
const SettingsScreen = React.lazy(
() => import('./screens/Settings')
);
或者路由层进行拆分。
核心思想:
首屏只加载首屏需要的代码。
特别是这些模块应该重点检查:
支付
地图
视频
IM
WebView
图表
富文本
编辑器
大型 UI Kit
国际化
埋点 SDK
第三方登录
低频业务页面
六、Metro:inlineRequires
对于大型 RN 项目,inlineRequires 是非常值得检查的优化项。
Metro 支持:
css
// metro.config.js
module.exports = {
transformer: {
getTransformOptions: async () => ({
transform: {
inlineRequires: true,
},
}),
},
};
它的核心思想是把:
javascript
import A from './A';
import B from './B';
import C from './C';
部分依赖从:
javascript
App启动
↓
全部 require
↓
全部初始化
↓
首屏
变成更接近:
App启动
↓
只初始化必要模块
↓
首屏
↓
用户进入其他页面
↓
再加载对应模块
Metro 当前文档也支持通过 inlineRequires 和 nonInlinedRequires 精细控制哪些模块进行 inline。(GitHub)
七、不要盲目 RAM Bundle
这一点非常重要。
老 RN 项目经常会看到:
diff
RAM Bundle
+ inlineRequires
但如果现在使用 Hermes:
通常不要再把 RAM Bundle 当第一优化方案。
React Native 当前文档明确指出,RAM bundle 不支持 Hermes bytecode,而且 Hermes 在这些场景下已经提供相同或更好的性能。(React Native)
所以:
Hermes
↓
减少 JS bundle 执行成本
↓
inlineRequires
↓
减少启动阶段模块初始化
通常比:
关闭 Hermes
↓
RAM Bundle
更合理。
八、第三优先级:减少 App.tsx 的启动工作
这是最容易被忽略的一层。
很多项目:
scss
function App() {
initUser();
initConfig();
initAnalytics();
initPush();
initDatabase();
initRemoteConfig();
checkUpdate();
loadLanguage();
loadTheme();
return <Home />;
}
这会导致:
JS启动
↓
初始化 10 个东西
↓
等待
↓
React Render
↓
首屏
应该改成:
JS启动
↓
最小化初始化
↓
首屏 Render
↓
requestIdleCallback / InteractionManager
↓
后台初始化
例如:
scss
function App() {
useEffect(() => {
const task = InteractionManager.runAfterInteractions(() => {
initAnalytics();
initRemoteConfig();
initPush();
initOtherServices();
});
return () => task.cancel();
}, []);
return <Home />;
}
现代 RN 项目也可以根据实际架构选择合适的调度方式。
核心不是 API 名字,而是:
非首屏必要工作,不要阻塞首屏。
九、第三方 SDK 是首屏杀手
建议把 SDK 分三类:
A. 必须首屏
例如:
Crash SDK
基础配置
必要鉴权
B. 首屏后立即初始化
Analytics
Push
Remote Config
AB Test
C. 使用时再初始化
地图
支付
视频
IM
WebView
广告 SDK
尤其注意:
javascript
import SomeHugeSDK from 'xxx';
即使你:
ini
if (Platform.OS === 'android') {
...
}
这个 import 本身也可能已经进入 Bundle/module graph。
应该从依赖关系本身开始优化。
十、第四优先级:首屏 React Render
JS Bundle 很快,并不代表首屏快。
例如:
xml
<Home>
<Header />
<Banner />
<Category />
<Recommend />
<ProductList />
<Activity />
<Footer />
</Home>
如果一次 render 3000 个节点:
markdown
JS Bundle 300ms
JS 初始化 200ms
React Render 800ms
Native Commit 300ms
----------------------
FCP 1600ms
这时候继续压 Bundle 已经意义不大。
十一、首屏不要一次渲染完整页面
推荐:
css
第一阶段:
Header
Skeleton
核心内容
然后:
第二阶段:
Banner
Category
最后:
第三阶段:
推荐
猜你喜欢
低优先级模块
例如:
javascript
function Home() {
return (
<>
<Header />
<Hero />
<AboveFoldContent />
<DeferredRecommendations />
</>
);
}
不要:
xml
<HomeEverything />
一次性把所有业务模块全部 mount。
十二、FlatList 是首屏另一个大坑
例如:
ini
<FlatList
data={1000}
renderItem={renderItem}
/>
建议至少检查:
ini
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={item => item.id}
initialNumToRender={8}
maxToRenderPerBatch={6}
windowSize={5}
removeClippedSubviews
/>
但不要机械照抄这些参数。
真正原则是:
markdown
initialNumToRender
↓
首屏需要多少?
maxToRenderPerBatch
↓
每一批渲染多少?
windowSize
↓
保持多少屏?
getItemLayout
↓
如果高度固定,直接提供
尤其是固定高度列表:
perl
getItemLayout={(_, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
通常非常值得做。
十三、首屏图片优化往往比 JS 优化更明显
典型问题:
yaml
服务器返回:
2000 × 2000
2MB JPEG
实际手机只显示:
100 × 100
这非常浪费。
应该:
css
原图
↓
CDN Resize
↓
100 × 100 WebP/AVIF
↓
RN Image
例如:
ini
https://cdn.xxx.com/a.jpg?w=200&q=75
而不是:
arduino
https://cdn.xxx.com/original/xxxx.jpg
十四、图片一定要区分首屏图片
推荐:
markdown
LCP / Hero Image
↓
优先加载
其他:
markdown
Below Fold
↓
延迟加载
可以建立:
arduino
<FastImage
source={{
uri: image,
priority: 'high',
}}
/>
或者根据你使用的图片库实现优先级。
十五、首屏图片不要全部同时请求
错误:
Home
├── Image 1
├── Image 2
├── Image 3
├── Image 4
├── Image 5
├── Image 6
├── Image 7
└── Image 8
最终:
arduino
API
├── image
├── image
├── image
├── image
├── image
└── image
更合理:
swift
首屏
├── Hero
└── 关键图片
屏幕下方
└── lazy
下一屏
└── lazy
十六、第五优先级:网络请求并行化
非常常见的错误:
ini
const user = await getUser();
const config = await getConfig();
const home = await getHome();
const banner = await getBanner();
变成:
diff
100ms
+
100ms
+
200ms
+
100ms
=
500ms
如果互相没有依赖:
scss
const [ user, config, home, banner,] = await Promise.all([
getUser(),
getConfig(),
getHome(),
getBanner(),
]);
变成:
scss
max(100,100,200,100)
≈ 200ms
十七、进一步:首屏 API 合并
如果首屏需要:
bash
/user
/config
/banner
/category
/recommend
可以考虑:
arduino
GET /home/bootstrap
返回:
json
{
"user": {},
"config": {},
"banner": [],
"category": [],
"recommend": []
}
尤其适合:
首页
Feed
商城首页
Dashboard
减少:
arduino
DNS
TCP
TLS
HTTP
Header
Server processing
的重复开销。
十八、缓存:让第二次启动极快
如果是你之前关注的 RN 离线缓存 / 图片缓存 场景,这一块尤其重要。
推荐:
sql
App Start
│
┌────────┴────────┐
↓ ↓
Local Cache Network
│ │
↓ ↓
首屏立即展示 后台更新
│ │
└───────┬─────────┘
↓
更新 UI
也就是:
Cache First + Network Refresh
例如:
ini
const cachedHome = await storage.getItem('home');
if (cachedHome) {
render(cachedHome);
}
fetchHome().then(data => {
storage.setItem('home', data);
update(data);
});
对于首页尤其有效。
十九、Storage 不要乱用 AsyncStorage
如果首屏读取大量数据:
arduino
AsyncStorage
↓
大量 JSON
↓
parse
↓
JS thread
可能造成明显的 JS 阻塞。
建议根据数据类型选择:
markdown
配置 / Token / 小数据
↓
MMKV 类方案
结构化大量数据
↓
SQLite
复杂离线数据
↓
专门数据库方案
图片
↓
File/Image Cache
关键不是"哪个库最快",而是:
首屏只读取真正需要的数据。
二十、Native 层也要优化
很多 RN 开发只优化 JS:
JS
✓
但是:
markdown
Application.onCreate()
↓
Firebase
↓
地图
↓
广告
↓
Push
↓
数据库
↓
RN
那 RN 再快也没用。
Android:
scss
Application.onCreate()
iOS:
didFinishLaunchingWithOptions
里面应该尽量:
最小化初始化
而不是:
初始化所有 SDK
↓
初始化所有 Service
↓
初始化所有数据库
↓
启动 RN
二十一、Android 首屏尤其关注 Splash
推荐架构:
markdown
Native Splash
↓
RN Runtime
↓
首屏 Skeleton
↓
真实内容
而不是:
白屏
↓
启动 RN
↓
等待 JS
↓
等待 API
↓
突然出现页面
用户感知上:
白屏 1500ms
和:
css
Splash 500ms
Skeleton 500ms
Content 500ms
虽然技术耗时可能类似,但 UX 完全不同。
二十二、Navigation 也可能拖慢首屏
比如:
NavigationContainer
↓
Stack
↓
Tab
↓
Stack
↓
Home
如果 Navigation 层级非常复杂,首屏也会承担大量初始化。
建议:
css
Root
└── Auth
└── Main
└── Home
而不是启动阶段把所有 navigator / screen 都初始化。
低频页面尽量延迟。
二十三、避免首屏大规模 Context 更新
例如:
xml
<AppProvider>
<ThemeProvider>
<UserProvider>
<ConfigProvider>
<PermissionProvider>
<FeatureProvider>
<Navigation />
</FeatureProvider>
</PermissionProvider>
</ConfigProvider>
</UserProvider>
</ThemeProvider>
</AppProvider>
然后启动时:
scss
setUser(...)
setConfig(...)
setPermission(...)
setFeature(...)
可能导致:
r
Provider 更新
↓
大量 Consumer
↓
React Re-render
↓
首屏变慢
建议把:
首屏状态
和:
全局状态
分开。
二十四、避免首屏 JSON 大解析
比如后端返回:
json
{
"home": [
// 5000 items
]
}
即使 UI 只需要:
前 10 个
你依然:
javascript
下载
↓
JSON.parse
↓
创建对象
↓
Redux
↓
Selector
↓
Render
应该尽量:
arduino
Server pagination
而不是:
一次返回整个首页数据
二十五、删除 console.log
生产环境应该检查:
perl
grep -R "console.log" src
尤其:
大量循环
FlatList render
Redux middleware
Network interceptor
不要留下:
javascript
console.log(JSON.stringify(hugeObject));
这会明显增加 JS thread 工作。
React Native 官方性能文档也把生产环境的 console.log 等 JS 工作列为常见性能问题。(archive.reactnative.dev)
二十六、Bundle 分析
建议把 Bundle 拆开看:
css
Bundle
│
├── react
├── react-native
├── navigation
├── redux
├── lodash
├── moment
├── icon
├── UI Library
├── business
└── third-party
重点寻找:
swift
谁最大?
谁被首屏引用?
谁可以 lazy?
谁可以删除?
谁重复?
尤其注意:
lodash
javascript
import _ from 'lodash';
尽量:
javascript
import debounce from 'lodash/debounce';
moment
如果项目还在大量使用:
javascript
import moment from 'moment';
通常值得考虑替换成更轻量方案。
Icon
不要:
css
整个 icon library
全部进入首屏。
二十七、字体也是隐藏成本
如果 App 有:
diff
20 个字体
+
多个 weight
+
多语言字体
启动阶段可能非常重。
建议:
首屏字体
↓
最少
其他字体
↓
按需
二十八、首屏不要做这些事情
看到下面代码,基本都值得怀疑:
scss
useEffect(() => {
await initDatabase();
await migrateDatabase();
await syncUser();
await loadRemoteConfig();
await loadAllProducts();
await preloadImages();
await initAnalytics();
await checkUpdate();
}, []);
更合理:
scss
useEffect(() => {
// 首屏必要
loadHome();
}, []);
useEffect(() => {
// 首屏之后
requestIdleCallback(() => {
initAnalytics();
preloadImages();
loadRemoteConfig();
});
}, []);
二十九、一个推荐的首屏架构
最终可以做到:
sql
Native Launch
│
▼
Native Splash
│
▼
Hermes Start
│
▼
Minimal JS Bundle
│
▼
App Bootstrap
│
┌───────────┴───────────┐
│ │
Local Cache Essential API
│ │
└───────────┬───────────┘
▼
Home Render
│
▼
FCP / LCP
│
▼
User Interactive
│
┌───────────┼───────────┐
▼ ▼ ▼
Analytics Prefetch Images
│ │ │
▼ ▼ ▼
Background Initialization
三十、推荐的优化优先级
如果你现在接手一个 RN 项目,我建议按这个顺序做:
P0:先测
sql
Cold Start
Warm Start
FCP
TTI
JS Bundle Execute
首屏 API
首屏图片
↓
P1:Hermes
sql
Hermes
Release Build
↓
P1:减少 Bundle
swift
Bundle Analyzer
↓
删除无用依赖
↓
lazy import
↓
inlineRequires
↓
P1:减少启动 JS
diff
App.tsx
index.js
RootProvider
Navigation
SDK
↓
P1:首屏 React
r
减少组件
减少 Context
减少 Re-render
FlatList
Skeleton
↓
P1:网络
javascript
Promise.all
API 合并
CDN
缓存
↓
P2:图片
css
缩略图
WebP/AVIF
CDN resize
Image Cache
Lazy Load
↓
P2:Native
Application.onCreate
AppDelegate
SDK 初始化
Splash
↓
P3:高级优化
JSI
TurboModule
Fabric
Native Preloading
Bundle 拆分
启动阶段 Native 化
三十一、我会给项目设这样的目标
不要单纯说:
"首屏优化到 1 秒。"
应该拆成:
sql
Cold Start
│
├── Native < 300ms
├── Hermes < 200ms
├── JS Execute < 200ms
├── React < 200ms
├── API < 300ms
├── Image < 300ms
└── FCP < 1000ms
当然这些不是通用硬指标,要根据设备档位、网络和业务复杂度建立 P50/P75/P90/P95 基线。
最关键的是 P50/P90,而不是拿一台旗舰机测一次。
三十二、如果你的项目现在已经很慢,我建议这样排查
直接抓一次:
sql
Cold Start
↓
┌─────────────────────────────┐
│ Native Start │
├─────────────────────────────┤
│ Hermes Initialization │
├─────────────────────────────┤
│ JS Bundle Execute │
├─────────────────────────────┤
│ App Bootstrap │
├─────────────────────────────┤
│ Navigation │
├─────────────────────────────┤
│ React Render │
├─────────────────────────────┤
│ API │
├─────────────────────────────┤
│ Image │
├─────────────────────────────┤
│ First Contentful Paint │
├─────────────────────────────┤
│ Interactive │
└─────────────────────────────┘
然后根据结果选择方案。
例如:
ini
Bundle Execute = 800ms
→ 优先查 Bundle / import / Hermes。
ini
React Render = 1000ms
→ 优先查组件树 / FlatList / Context / re-render。
ini
API = 1500ms
→ 优先查接口、缓存、并行请求。
ini
Image = 1200ms
→ 优先查 CDN、尺寸、格式、缓存。
ini
Native = 1500ms
→ JS 再怎么优化都没用,去查 Android/iOS SDK 初始化。
最值得记住的一句话
RN 首屏优化不是"让 RN 更快",而是让"首屏真正需要做的事情更少"。
最终目标应该是:
markdown
原来
启动
↓
加载所有 JS
↓
初始化所有 SDK
↓
初始化所有 Provider
↓
请求所有 API
↓
加载所有图片
↓
Render 整个页面
↓
用户看到内容
优化后
启动
↓
Hermes
↓
最小 JS
↓
首屏必要初始化
↓
Cache / Essential API
↓
Render Above Fold
↓
用户看到内容
↓
后台慢慢做剩下的事
这通常比单独优化某一个 React Component 的收益大得多。(React Native)
如果你愿意,我也可以进一步给你整理一套 "RN 首屏性能优化 Checklist + Android/iOS/JS/Metro/网络逐项排查命令 + 可直接复制的代码模板" ,适合直接放进团队技术规范里。