React Native Bundle Splitting and Incremental Loading Strategy at Snowball
This article describes Snowball's evolution from a single React Native bundle to a multi‑bundle, code‑splitting architecture that reduces bundle size by 70%, shortens load time by 65%, and enables flexible, incremental updates through Metro‑based split‑packages and LRU engine caching.
Background
Since the release of React Native, its weekly npm downloads have risen from 200 k to 1.8 M, indicating rapid ecosystem growth. Snowball adopted RN in 2019, but the initial single‑bundle, single‑engine approach quickly led to large bundle sizes and inflexible release cycles.
Why Split Bundles?
Two major pain points motivated the split‑bundle strategy: (1) explosive bundle size growth, which increases download, unzip and load time and wastes storage; (2) lack of release flexibility, because any code change forces a full‑bundle update, requiring extensive coordination and regression testing.
Problem 1: Size Growth
From version v13.19 (2.2 MB) to v14.23 (5.1 MB) the bundle grew 150 % while the number of RN pages doubled from 44 to 94, leading to noticeable cold‑start latency.
Problem 2: Release Inflexibility
RN’s advantage of client‑free releases was lost when every modification required a full bundle redeployment, causing longer release cycles and higher risk of errors.
Evolution Roadmap
Phase 1 – Multi‑Engine (Resolve Project Dependencies)
Implemented support for loading and switching multiple bundles, allowing the Snowball and Fund RN projects to remain independent without code duplication.
Phase 2 – Core Library Extraction (Reduce Bundle Size)
Extracted roughly half of the bundle—static RN core libraries—into a local DLL that is only updated when the libraries change, cutting bundle size dramatically.
Phase 3 – Business‑Level Splitting (Independent Releases)
Divided business code into high, mid, and low priority bundles with distinct loading strategies:
High: pre‑download & pre‑load, enabling instant page opening.
Mid: pre‑download & on‑demand load, short loading time.
Low: on‑demand download & load, longer loading time.
This granularity allows independent versioning of each business module.
Phase 4 – Public Library Extraction (Further Size Reduction)
Removed redundant public libraries (e.g., Snowbox, custom controls) from bundles, sharing them across modules to lower code duplication and improve memory usage.
Results
After three iterations, the main business bundle shrank to 1.3 MB with an average load time of ~300 ms, achieving a 70 % size reduction and a 65 % decrease in total loading latency.
Implementation Details
5.1 RN Split‑Package Solution
Metro is used as the split‑package tool, enhanced by the open‑source metro-code-split plugin from 58.com. The configuration defines a common DLL bundle and multiple business bundles, each with its own entry file.
const mcs = new Mcs({
output: {},
dll: {
entry: [
'react-native', // RN core
'react', // React
// ...other libs
],
referenceDir: './bundle/dll', // output path
},
dynamicImports: false,
});Business bundles are split by route. A routes.js file lists all routes, and each route registers a component via AppRegistry.registerComponent .
import routes from '@/routes';
routes.forEach(route => {
AppRegistry.registerComponent(route.appKey, () => Wrapper(route.module, route.theme));
});Project structure (simplified):
snb-rn
├─ app.json
├─ babel.config.js
├─ bundle
├─ common // common libraries
├─ components
├─ index.js // entry
├─ metro.config.js // mcs config
├─ pages
├─ routes
│ ├─ high_xueqiu_routes.js
│ ├─ mid_private_routes.js
│ └─ low_media_routes.jsEach business bundle exports a route list, e.g., low_media_routes.js :
const Routes = [
{
title: '我的音频',
path: '/audio_album/subscribe',
module: MyAudios,
appKey: 'MyAudios',
},
];
Routes.forEach(route => {
AppRegistry.registerComponent(route.appKey, () => Wrapper(route.module));
});
export default Routes;5.2 Client Implementation
5.2.1 Update Logic
The client updates bundles in three scenarios: (1) on app launch, (2) when returning from background, and (3) asynchronously after a bundle’s pages are all closed.
5.2.2 Loading Strategy
Based on the bundle’s level (high/mid/low), the client chooses pre‑download + pre‑load, pre‑download + on‑demand, or on‑demand download + load respectively.
5.2.3 Routing Logic
The client matches the requested URL with the route table to determine which bundle to load, then loads the common DLL first, followed by the target bundle. Engine instances are cached in an LRU cache to avoid repeated initialization while keeping memory usage bounded.
val mManagerMap = LruCache
?>(CACHE_SIZE)
// cache engine when loading a new bundle
mManagerMap.put(managerKey, WeakReference(reactInstance))
// retrieve cached engine
mManagerMap.get(bundleName)
// remove on error
mManagerMap.remove(bundleName)Conclusion
Snowball’s RN split‑bundle solution reduces bundle size by 70 %, shortens load time by 65 %, and enables independent, on‑demand releases without client‑side code changes. The approach is well‑suited for fast‑iteration mobile apps, though it may need adaptation for other projects.
Snowball Engineer Team
Proactivity, efficiency, professionalism, and empathy are the core values of the Snowball Engineer Team; curiosity, passion, and sharing of technology drive their continuous progress.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.