Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 119 additions & 12 deletions doc/project/module.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,19 +78,20 @@ class Foobaz {
exports.Babar = Foobaz;
{% endhighlight %}

## Module splitting
## Module Splitting

When emitting modules, the Scala.js linker is able to split its output into multiple JavaScript modules (i.e. files).

There are several reasons to split the JavaScript output into multiple files:

* Share code between different parts of an application (e.g. user/admin interfaces).
* Load parts of a large app progressively
* Create smaller files to minimize changes for incremental downstream tooling.
* Load parts of a large app progressively (not supported yet, see [#4201](https://github.com/scala-js/scala-js/issues/4201)).

The Scala.js linker can split a full Scala.js application automatically based on:

* The entry points (top-level exports and module initializers)
* Entry points (top-level exports and module initializers)
* Dynamic import boundaries (calls to `js.dynamicImport`)
* The split style (fewest modules or smallest modules)

### Entry Points
Expand Down Expand Up @@ -151,15 +152,72 @@ Further, importing `b.js` will call `AppB.main`.

Note that there is no public module `main.js`, because there is no entry point using the default `moduleID`.

### Module Split Styles
### Dynamic Imports

Warning: Dynamic imports in Scala.js 1.4.0 are affected by [#4386](https://github.com/scala-js/scala-js/issues/4386), see the issue for a workaround.

So far, we have seen how public modules can be configured.
Based on the public modules, the Scala.js linker generates internal modules for the shared code between the public modules.
Dynamic imports allow a Scala.js application to be loaded in multiple steps to reduce initial loading time.
To defer loading of a part of your Scala.js application to a later point in time, use [`js.dynamicImport`]({{ site.production_url }}/api/scalajs-library/latest/scala/scalajs/js/index.html#dynamicImport[A](body:=%3EA):scala.scalajs.js.Promise[A]):

**Example**:

{% highlight scala %}
import scala.scalajs.js
import scala.scalajs.js.annotation._

import scala.concurrent.ExecutionContext.Implicits.global

class HeavyFeature {
def doHeavyFeature(x: Int): Int =
x * 2
}

object MyApp {
@JSExportTopLevel(name = "onClick")
def onClick(input: Int): Unit = {
val resultPromise: js.Promise[Int] = js.dynamicImport {
new HeavyFeature().doHeavyFeature(input)
}
for (result <- resultPromise.toFuture)
updateUIWithOutput(result)
}

private def updateUIWithOutput(i: Int): Unit = ???
}
{% endhighlight %}

The `js.dynamicImport` method has the following signature:

{% highlight scala %}
def dynamicImport[A](body: => A): js.Promise[A]
{% endhighlight %}

Semantically, it will evaluate `body` asynchronously and return a Promise of the result.
More importantly, it acts as a border for the Scala.js linker to split out a module that will be dynamically loaded.
The above program would generate
* a public module `main.js` containing `onClick` and its direct dependencies
* an internal module `MyApp$$anon$1.js` containing `HeavyFeature`
* an internal module `main-MyApp$$anon$1.js` containing common dependencies of `main.js` and `MyApp$$anon$1.js`.

Internal modules allow the Scala.js linker to split code internally.
Unlike public modules, internal modules may not be imported by user code.
Doing so is undefined behavior and subject to change at any time.

The linker generates internal modules automatically based on the dependency graph of the code and `moduleSplitStyle`.
You can change it as follows:
In the example above, the `js.dynamicImport` is replaced by `import("./MyApp$$anon$1.js")`, followed by an invocation of the main entry point in `MyApp$$anon$1.js` (the `body` passed to `js.dynamicImport`).
Therefore, when `main.js` is loaded, we do not need to load, nor download `MyApp$$anon$1.js`.
It will only be loaded the first time `onClick` is actually called.
This reduces the initial download time for users.

Dynamic imports and entry points can be arbitrarily combined.

### Module Split Styles

So far, we have seen how public modules and dynamic import boundaries can be defined.

Based on these, the Scala.js linker automatically uses the dependency graph of the code to generate appropriate internal modules.

However, there are still choices involved.
They can be configured with the `moduleSplitStyle`:

{% highlight scala %}
import org.scalajs.linker.interface.ModuleSplitStyle
Expand All @@ -170,33 +228,82 @@ There are currently two module split styles: `FewestModules` and `SmallestModule

#### `FewestModules`

Create as few modules as possible without including unnecessary code.
Create as few modules as possible
* while respecting dynamic import boundaries and
* without including unnecessary code.

This is the default.

In the example above, this would generate:
In the entry points example above, this would generate:

* `a.js`: public module, containing `AppA` and the export of `start`.
* `b.js`: public module, containing `AppB`, `mutable.Set`, the export of `start` and the call to `AppB.main`
* `a-b.js`: internal module, Scala.js core and the implementation of `println`.

This also works for more than two public modules, creating intermediate shared (internal) modules as necessary.

The dynamic import example above already assumes this module split style so a module listing is omitted.

#### `SmallestModules`

Create modules that are as small as possible.
The smallest unit of splitting is a Scala class.
The smallest unit of splitting is a Scala class (see [Splitting Granularity](#splitting-granularity) below for more).

Using this mode typically results in an internal module per class with the exception of classes that have circular dependencies: these are put into the same module to avoid a circular module dependency graph.

In the example above, this would generate:
In the entry points example above, this would generate:

* `a.js`: public module, containing the export of `start`.
* `b.js`: public module, containing the export of `start` and the call to `AppB.main`
* many internal small modules (~50 for this example), approximately one per class.

In the dynamic import example, this would generate:
* `main.js`: public module, containing the export of `onClick`.
* many internal small modules (~150 for this example), approximately one per class.

Generating many small modules can be useful if the output of Scala.js is further processed by downstream JavaScript bundling tools.
In incremental builds, they will not need to reprocess the entire Scala.js-generated .js file, but instead only the small modules that have changed.

### Splitting Granularity

Scala.js only splits modules along class boundaries.
It is important to be aware of this when structuring your application to avoid unnecessary grouping.

For example, the following structure likely leads to poor splitting (if `FeatureN`s are not always used together):

{% highlight scala %}
object UI {
def renderFeature1(): Unit = ???
def renderFeature2(): Unit = ???
def renderFeature3(): Unit = ???
}

object Calc {
def calcFeature1(): Unit = ???
def calcFeature2(): Unit = ???
def calcFeature3(): Unit = ???
}
{% endhighlight %}

For better splitting, group code that belongs to the same feature:

{% highlight scala %}
object Feature1 {
def render(): Unit = ???
def calc(): Unit = ???
}

object Feature2 {
def render(): Unit = ???
def calc(): Unit = ???
}

object Feature3 {
def render(): Unit = ???
def calc(): Unit = ???
}
{% endhighlight %}

### Linker Output

With module splitting, the set of files created by the linker is not known at invocation time.
Expand Down