Code Splitting is a technique that splits the code into multiple files, which can be loaded on demand and in parallel.
It can be used to:
Code Splitting is one of the most important features in Re.Pack, and it's based on Webpack's infrastructure as well as the native module that allows to execute the additional code on the same JavaScript context (same React Native instance).
For dynamic feature delivery, Code Splitting should be used as a mean to optimize the user experience by deferring the features or deliver existing features only to a subset of users.
Code Splitting with Re.Pack is not designed to add new features dynamically without doing the regular App Store or Play store release. It can be used to deliver fixes or tweaks to additional (split) code, similarly to Code Push, but you should not add new features with it.
Using Code Splitting to deliver new features without a regular App Store release is likely going to violate Apple's App Store Terms and your application might be rejected or banned.
You should provide access to all the features for the App Store review process.
Also, it might be beneficial to highlight that all split features are closely integrated with application and cannot work in isolation - you don't want to introduce confusion that your application might compete with Apple's App Store.
On that note, you might want to avoid using terms like mini-app or mini-app store in favour of modules, components, plugins or simply features.
The specific implementation of Code Splitting in your application can be different and should account for your project's specific needs, requirements and limitations.
In general, we can identify 3 main categories of implementation. All of those approaches are based on the same underlying mechanism: Re.Pack's ScriptManager
and the native module for it.
Use Glossary of terms to better understand the content of this documentation.
On a high-level, all functionalities that enable usage of Webpack's Code Splitting, are powered by
Re.Pack's ScriptManager
, which consists of the JavaScript part and the native part.
The ScriptManager
has methods which allows to:
loadScript
prefetchScript
resolveScript
invalidateScripts
In order to provide this functionalities, a resolver has to be added using ScriptManager.shared.addResolver
:
If the storage
is provided, the returned url
from resolve
will be used for cache management.
You can read more about it in Caching and Versioning.
Do not instantiate ScriptManager
yourself - use ScriptManager.shared
to get access to an instance.
Under the hood, the way a script gets loaded can be summarized as follows:
ScriptManager.shared.loadScript(...)
gets called, either:
import(...)
function handled by Webpack, when using Async chunks approachScriptManager.shared.loadScript(...)
is called scriptId
and caller
arguments, which are either provided by:
webpackChunkName
ScriptManager.shared.loadScript(...)
resolves the chunk location using ScriptManager.shared.resolveScript(...)
.storage
was provided and the script was resolved before.Promise
returned by ScriptManager.shared.loadScript(...)
gets resolved.ScriptManager.shared.prefetchScript(...)
follows
the same behavior except for #6, where it only downloads the file and doesn't execute it.
There are generally 3 approaches to Code Splitting with Webpack and Re.Pack. Keep in mind that the actual code you will have to create might be slightly different, depending on your project's requirements, needs and limitations.
Those approaches should be used as a base for your Code Splitting implementation.
It's recommended to read Generic usage first, to understand it on a high-level and get the necessary context.
Async chunks (or asynchronous chunks) are the easiest Code Splitting approach. They are usually
created by using dynamic import(...)
function, which makes them extremely easy to introduce it
into the codebase.
The async chunks are created alongside the main bundle as part of a single Webpack compilation, making it a great choice for a modular applications where all the code is developed in-house.
The usage of async chunks essentially boils down to calling import(...)
in your code, for example:
Async chunks created by dynamic import(...)
function can be nicely integrated using React.lazy
and React.Suspense
:
For each file in the dynamic import(...)
function a new chunk will be created - those chunks will
be remote chunks by default.
You can learn more about local and remote chunks in the dedicated Local vs Remote chunks guide.
To learn more or use async chunks in your project, check out our dedicated Async chunks guide.
To see import(...)
, React.lazy
and React.Suspense
in action, check out
Re.Pack's TesterApp
.
Don't forget to add resolver using ScriptManager.shared.addResolver
!
This approach allows to execute arbitrary code in your React Native application.
It's a similar concept as adding a new <script>
element to a Web page.
Those scripts can be written in-house or externally, bundled using Webpack or a different bundler. This also means that scripts can be created as part of separate Webpack compilations, or separate build pipelines, from separate codebases and repositories.
Scripts should only be used by advanced users with deep Webpack knowledge and experience.
Scripts give a lot of flexibility but it also means the support for them is limited. It's not possible for Re.Pack's contributors to support all potential setups using this approach.
Beware, with dynamic scripts there's no dependency sharing by default. If you want your scripts to reuse existing dependencies from the main bundle, it's up to you to figure out how to do it. A good starting point would be:
Loading a script is as simple as running a single function:
And adding a resolver to the ScriptManager
to resolve your
scripts:
Use Module Federation document for information on adoption of Module Federation in React Native projects with Re.Pack.
Let's assume, we are building an E-Learning application with specific functionalities for a student
and for a teacher. Both student and a teacher will get different UIs and different features, so it
would make sense to isolate the student's specific code from the teacher's. That's were Code
Splitting comes into play - we can use dynamic import(...)
function together with React.lazy
and
React.Suspense
to conditionally render the student and the teacher sides based on the user's role.
The code for the student and the teacher will be put into a remote async chunk, so that the initial
download size will be smaller.
It's recommended to read:
first, to understand Code Splitting, usage on a high-level and get the necessary context.
Before you begin, make sure the Re.Pack's native module is linked into your application:
Let's use the following code for the student's side:
And a code for the teacher's side:
Now in our parent component, which will be common for both the student and the teacher:
Since we are using Webpack's magic comments, we need to make sure Babel is not removing those.
Add comments: true
to your Babel config, for example:
At this point all the code used by StudentSide.js
will be put into student.chunk.bundle
and
TeacherSide.js
into teacher.chunk.bundle
.
Before we can actually render out application, we need to add resolver using ScriptManager.shared.addResolver(...)
,
so it can resolve the chunks:
This code will allow Re.Pack's ScriptManager
to
actually locate your chunks for the student and the teacher, and download them.
When bundling for production/release, all remote chunks, including student.chunk.bundle
and
teacher.chunk.bundle
will be copied to <projectRoot>/build/output/<platform>/remote
by default,
for example: <projectRoot>/build/output/ios/student.chunk.bundle
.
You should upload files from this directory to a remote server or a CDN from where ScriptManager
will download them.
You can change this directory and/or mark chunks as local. Refer to dedicated Local vs Remote chunks guide for more information.
Each chunk created by using dynamic import(...)
function can be either remote or a local chunk.
By default all chunks are remote.
Regardless of the chunk's type, it's important to understand how chunks are named.
Chunk naming is handled solely by Webpack. Re.Pack does not alter or customize chunk names in any way.
By default, chunk name is inferred by Webpack based on the source filename. For example if you have a file
./src/Button.js
, the inferred name would be src_Button_js
. This human-readable chunk name will only be used
in development though. By default, Webpack in production, minimizes chunk names to numbers to save space,
meaning src_Button_js
chunk in production might look like 137
.
This behavior is fine for Web, but in React Native with ScriptManager
, it's not ideal, because we don't know what
this number will be in production.
There are 2 options to address this problem:
optimization.chunkIds
option to named
in Webpack config./* webpackChunkName: "<name>" */
magic comment in import(...)
.You can use both optimization.chunkIds
and webpackChunkName
comment at the same time. They are not mutually exclusive.
Chunk extension can be configured in output.chunkFilename
, which is set to .chunk.bundle
by default.
It's usually not necessary to modify it.
chunkIds
config optionSetting optimization.chunkIds
option to named
in your Webpack config will
force Webpack to always use the human-readable form for the name of the chunks.
In version 3.x
, Re.Pack's templates for Webpack config have chunkIds
is set to named
by default.
Keep in mind that, webpackChunkName
magic comment will always take precedence, so even if you have chunkIds: 'named'
, Webpack will use name in webpackChunkName
comment for that chunk instead of the inferred one.
webpackChunkName
magic commentwebpackChunkName
magic comment allows you to provide your own custom name that should be used for the chunk when calling import(...)
function:
This example will result in chunk being named button
, so the filename with extension will be button.chunk.bundle
.
You can use webpackChunkName
magic comment to provide your custom name, regardless of the optimization.chunkIds
option.
webpackChunkName
will always take precedence.
By default all chunks are remote chunks, meaning they are not bundled into the application and will be downloaded from the remote location (usually the Internet) on demand. This helps with reducing the initial application size, especially if you have logic or features that only a subset of users will use - it doesn't make sense for everyone else to always have to download the code (together with the application) they won't need.
All remote chunks are stored under <projectRoot>/build/output/<platform>/remotes
by default. For example if button.chunk.bundle
is a remote chunk, it will be stored under:
<projectRoot>/build/output/ios/remotes/button.chunk.bundle
for iOS.
You can customize this by providing extraChunks
to RepackPlugin
:
This example, will mark all chunks as remote ones and store them under <projectRoot>/my/custom/path
.
If outputPath
in extraChunks
is a relative path, it will be joined with the value of context
property, which is set to root directory of your project by default.
You can also provide an absolute path, in which case, it won't be modified in any way and used as is.
You can provide multiple type: 'remote'
specs - see Advanced example for more info.
In some situations, having chunks as remote ones is not ideal. For example, if you know that majority of the users will need a specific chunk you can mark it as local.
Local chunks will always be included in the final application (.ipa
or .apk
) file alongside main bundle. This can save mobile data usage and make your application feel
faster (in cases where network connection is degraded), but you will still benefit from improved startup, because the JavaScript engine will defer parsing and evaluation of
local chunks until they are actually needed.
If you're using Hermes and you compile your code into bytecode bundles, it's better not to use local chunks and instead make the code a part of the main bundle.
Using local chunks with Hermes and bytecode bundles, will likely result in worse performance.
You can customize which chunk should be local by providing extraChunks
option in RepackPlugin
configuration:
The example above will make all chunks matching the RegExp /^.+\.local$/
a local chunks, for example student.local
(student.local.chunk.bundle
) will be a local chunk, whereas everything else will become a remote.
Specifying extraChunks
will override any defaults - you must configure remote
chunks yourself as well, otherwise they won't be stored anywhere!
Once you have some chunks as local, you need to alter the resolver in ScriptManager.shared.addResolver
:
To avoid having to repeat the RegExp, you can create a new .js
or .json
file, export the RegExp and use the file both in the source code as well as in the Webpack config.
Check out the local-chunks
example for concrete implementation.
You can mix multiple type: 'local'
and type: 'remote'
specs using test
, include
and exclude
to match different chunks:
Use the table below for examples, how the config above would treat different chunks:
Name | type | outputPath |
---|---|---|
student | local | - |
student-extensions | local | - |
components | remote | <projectRoot>/build/output/<platform>/remotes/core |
utils | remote | <projectRoot>/build/output/<platform>/remotes/core |
teacher | remote | <projectRoot>/build/output/<platform>/remotes/teacher |
teacher-affiliations | remote | <projectRoot>/build/output/<platform>/remotes/teacher |
test
, include
and exclude
properties behave in the same way as those in Webpack's loader rules:
test: string | RegExp | Array<string | RegExp>
must match if specifiedinclude: string | RegExp | Array<string | RegExp>
must match if specifiedexclude: string | RegExp | Array<string | RegExp>
must not match if specifiedThe caching mechanism in Re.Pack prevents scripts over-fetching, which helps reducing bandwidth usage, specially since they can easily take up multiple MBs od data.
Providing storage
options to
ScriptManager.shared.setStorage
will enable
caching of downloaded script. The storage
option accepts anything with similar
API to AsyncStorage
's getItem
, setItem
and removeItem
functions.
By default, ScriptManager
will compare the method
/url
/query
/header
or body
returned by resolve
with the values stored in storage
to determine if downloading is
necessary. Skipping the download will only happen, if the values are equal, meaning you can introduce
versioning by changing the url
, for example:
Or by keeping the base URL inside remote config:
Versioning should be use with caution. If you upload a new version of a script and it happens that old main bundle is not compatible with new files, you might end up with broken application or crashes.
Usually cache invalidation happens automatically, but it's possible to invalidate chunk manually as
well using ScriptManager.shared.invalidateScripts(...)
,
which removes the scripts from filesystem and from the storage
.
Be careful what scripts are you manually invalidating - it's possible to remove local scripts from filesystem using this API.