CC 4.0 License

The content of this section is derived from the content of the following links and is subject to the CC BY 4.0 license.

The following contents can be assumed to be the result of modifications and deletions based on the original contents if not specifically stated.

Code splitting

Rspack supports code splitting, which allows splitting the code into other chunks. You have the full control about size of generated assets, which allow you to gain performance improvements in loading time.

There are three general approaches to code splitting available:

  • Entry Points: Manually split code using entry configuration.
  • Prevent Duplication: Use SplitChunksPlugin to dedupe and split chunks.
  • Dynamic Imports: Split code via inline function calls within modules.

Entry point

This is the simplest and most intuitive way to split the code. However, this approach requires us to manually configure the Rspack and contains some pitfalls that we will address. Let's start by looking at how to split multiple Chunks from multiple entry points.

rspack.config.js
/**
 * @type {import('@rspack/core').Configuration}
 */
const config = {
  mode: 'development',
  entry: {
    index: './src/index.js',
    another: './src/another-module.js',
  },
  output: {
    filename: '[name].bundle.js',
  },
  stats: 'normal',
};

module.exports = config;
index.js
import './shared';
console.log('index.js');
another-module.js
import './shared';
console.log('another-module');

This will yield the following build result:

... Asset Size Chunks Chunk Names another.bundle.js 1.07 KiB another [emitted] another index.bundle.js 1.06 KiB index [emitted] index Entrypoint another = another.bundle.js Entrypoint index = index.bundle.js [./src/index.js] 41 bytes {another} {index} [./src/shared.js] 24 bytes {another} {index}

As mentioned earlier, there are a some pitfalls to this approach:

  • If there are some shared modules between the import chains of multiple entry points, these shared modules will be repeatedly added to each entry chunk. The code of shared.js will be bundled into both index.bundle.js and another.bundle.js at the same time.
  • It isn't as flexible and can't be used to dynamically split code with the core application logic.

The first of these two points is certainly a problem for our example, and in the next section we will talk about how to remove duplicate modules.

SplitChunksPlugin

The SplitChunksPlugin can extract shared modules into a new generated chunk. Let's use this plugin to remove the duplicated shared.js module in the previous example:

rspack.config.js
/**
 * @type {import('@rspack/core').Configuration}
 */
const config = {
  mode: 'development',
  entry: {
    index: './src/index.js',
    another: './src/another-module.js',
  },
  output: {
    filename: '[name].bundle.js',
  },
+  optimization: {
+    splitChunks: {
+      chunks: 'all',
+      minSize: 1,
+    }
+  }
};

module.exports = config;

You should now see that the duplicate modules have been removed from index.bundle.js and another.bundle.js. Note that the plugin split shared.js into another~index.bundle.js and removes it from index.bundle.js and another.bundle.js.

Build Results:

Asset       Size         Chunks             Chunk Names
      another.bundle.js   3.27 KiB        another  [emitted]  another
        index.bundle.js   3.27 KiB          index  [emitted]  index
+ another~index.bundle.js  462 bytes  another~index  [emitted]
Entrypoint another = another.bundle.js another~index.bundle.js
Entrypoint index = another~index.bundle.js index.bundle.js
[./src/index.js] 41 bytes {another~index}
[./src/shared.js] 24 bytes {another~index}

Dynamic import

Rspack use the import() syntax that conforms to the ECMAScript proposal for dynamic imports.

Inconsistent behaviors with Webpack
  • Rspack doesn't support require.ensure.

Before we begin, let's remove the redundant entry and optimization.splitChunks from the configuration of the above example, as they are not needed for the rest of the demonstration.

rspack.config.js
/**
 * @type {import('@rspack/core').Configuration}
 */
const config = {
  mode: 'development',
  entry: {
    'index': './src/index.js',
-    'another': './src/another-module.js',
  },
  output: {
    filename: '[name].bundle.js',
  },
-  optimization: {
-    splitChunks: {
-      chunks: 'all',
-      minSize: 1,
-    }
-  }
};

module.exports = config;

Now, instead of import shared.js statically in index.js, we will import it dynamically via import(), thus split it into a new chunk.

index.js
- import './shared'
+ import('./shared')
console.log('index.js')

Let's run build command and see that shared.js is split into a separate src_shared_js.bundle.js

...
                  Asset       Size         Chunks             Chunk Names
        index.bundle.js   12.9 KiB          index  [emitted]  index
+ src_shared_js.bundle.js  245 bytes  src_shared_js  [emitted]
Entrypoint index = index.bundle.js
[./src/index.js] 42 bytes {index}
[./src/shared.js] 24 bytes {src_shared_js}
build: 67.303ms

Prefetching/Preloading modules

Using these inline directives while declaring your imports allows Rspack to output “Resource Hint” which tells the browser that for:

  • prefetch: resource is probably needed for some navigation in the future
  • preload: resource will also be needed during the current navigation

An example of this is having a HomePage component, which renders a LoginButton component which then on demand loads a LoginModal component after being clicked.

LoginButton.js
//...
import(/* webpackPrefetch: true */ './path/to/LoginModal.js');

This will result in <link rel="prefetch" href="login-modal-chunk.js"> being appended in the head of the page, which will instruct the browser to prefetch in idle time the login-modal-chunk.js file.

INFO

Rspack will add the prefetch hint once the parent chunk has been loaded.

Preload directive has a bunch of differences compared to prefetch:

  • A preloaded chunk starts loading in parallel to the parent chunk. A prefetched chunk starts after the parent chunk finishes loading.
  • A preloaded chunk has medium priority and is instantly downloaded. A prefetched chunk is downloaded while the browser is idle.
  • A preloaded chunk should be instantly requested by the parent chunk. A prefetched chunk can be used anytime in the future.
  • Browser support is different.

An example of this can be having a Component which always depends on a big library that should be in a separate chunk.

Let's imagine a component ChartComponent which needs a huge ChartingLibrary. It displays a LoadingIndicator when rendered and instantly does an on demand import of ChartingLibrary:

ChartComponent.js
//...
import(/* webpackPreload: true */ 'ChartingLibrary');

When a page which uses the ChartComponent is requested, the charting-library-chunk is also requested via <link rel="preload">. Assuming the page-chunk is smaller and finishes faster, the page will be displayed with a LoadingIndicator, until the already requested charting-library-chunk finishes. This will give a little load time boost since it only needs one round-trip instead of two. Especially in high-latency environments.

INFO

Using webpackPreload incorrectly can actually hurt performance, so be careful when using it.

Sometimes you need to have your own control over preload. For example, preload of any dynamic import can be done via async script. This can be useful in case of streaming server side rendering.

const lazyComp = () =>
  import('DynamicComponent').catch(error => {
    // Do something with the error.
    // For example, we can retry the request in case of any net error
  });

If the script loading will fail before Rspack starts loading of that script by itself (Rspack creates a script tag to load its code, if that script is not on a page), that catch handler won't start till chunkLoadTimeout is not passed. This behavior can be unexpected. But it's explainable — Rspack can not throw any error, cause Rspack doesn't know, that script failed. Rspack will add onerror handler to the script right after the error has happen.

To prevent such problem you can add your own onerror handler, which removes the script in case of any error:

<script
  src="https://example.com/dist/dynamicComponent.js"
  async
  onerror="this.remove()"
></script>

In that case, errored script will be removed. Rspack will create its own script and any error will be processed without any timeouts.