RN首屏加载速度优化全方案

如果你说的是 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 当前文档也支持通过 inlineRequiresnonInlinedRequires 精细控制哪些模块进行 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/网络逐项排查命令 + 可直接复制的代码模板" ,适合直接放进团队技术规范里。

相关推荐
前端精髓3 小时前
React 更新 state 中的对象
javascript·react.js·ecmascript
10share9 小时前
我为什么用 React 重写了一个 VitePress
前端·react.js
天道kabuto9 小时前
前端每日知识点:React 与 Vue Diff 算法对比 · 完整总结
vue.js·react.js·前端框架
moMo9 小时前
React 全栈实战:从组件到鉴权的完整指南
react.js
光影少年1 天前
如何实现RN 多环境、多渠道打包
前端·react native·react.js
●VON1 天前
鸿蒙跨平台框架怎么选?从真实需求比较 Flutter、React Native、KMP/CMP 与 Web 路线
flutter·react native·harmonyos
云上工程笔记1 天前
AstraFlow + React/Vite:如何用自然语言开发并部署一个 AI 咖啡网站原型
前端·人工智能·react.js
光影少年1 天前
React18 对RN 的影响
android·前端·react.js·ios·前端框架