Skip to content

Commit ee152d3

Browse files
authored
Merge pull request #538 from gzm0/document-dynamic-module-splitting
Document dynamic module splitting
2 parents 018090c + 1183101 commit ee152d3

File tree

1 file changed

+119
-12
lines changed

1 file changed

+119
-12
lines changed

doc/project/module.md

+119-12
Original file line numberDiff line numberDiff line change
@@ -78,19 +78,20 @@ class Foobaz {
7878
exports.Babar = Foobaz;
7979
{% endhighlight %}
8080

81-
## Module splitting
81+
## Module Splitting
8282

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

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

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

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

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

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

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

154-
### Module Split Styles
155+
### Dynamic Imports
156+
157+
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.
155158

156-
So far, we have seen how public modules can be configured.
157-
Based on the public modules, the Scala.js linker generates internal modules for the shared code between the public modules.
159+
Dynamic imports allow a Scala.js application to be loaded in multiple steps to reduce initial loading time.
160+
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]):
161+
162+
**Example**:
163+
164+
{% highlight scala %}
165+
import scala.scalajs.js
166+
import scala.scalajs.js.annotation._
167+
168+
import scala.concurrent.ExecutionContext.Implicits.global
169+
170+
class HeavyFeature {
171+
def doHeavyFeature(x: Int): Int =
172+
x * 2
173+
}
174+
175+
object MyApp {
176+
@JSExportTopLevel(name = "onClick")
177+
def onClick(input: Int): Unit = {
178+
val resultPromise: js.Promise[Int] = js.dynamicImport {
179+
new HeavyFeature().doHeavyFeature(input)
180+
}
181+
for (result <- resultPromise.toFuture)
182+
updateUIWithOutput(result)
183+
}
184+
185+
private def updateUIWithOutput(i: Int): Unit = ???
186+
}
187+
{% endhighlight %}
188+
189+
The `js.dynamicImport` method has the following signature:
190+
191+
{% highlight scala %}
192+
def dynamicImport[A](body: => A): js.Promise[A]
193+
{% endhighlight %}
194+
195+
Semantically, it will evaluate `body` asynchronously and return a Promise of the result.
196+
More importantly, it acts as a border for the Scala.js linker to split out a module that will be dynamically loaded.
197+
The above program would generate
198+
* a public module `main.js` containing `onClick` and its direct dependencies
199+
* an internal module `MyApp$$anon$1.js` containing `HeavyFeature`
200+
* an internal module `main-MyApp$$anon$1.js` containing common dependencies of `main.js` and `MyApp$$anon$1.js`.
201+
202+
Internal modules allow the Scala.js linker to split code internally.
158203
Unlike public modules, internal modules may not be imported by user code.
159204
Doing so is undefined behavior and subject to change at any time.
160205

161-
The linker generates internal modules automatically based on the dependency graph of the code and `moduleSplitStyle`.
162-
You can change it as follows:
206+
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`).
207+
Therefore, when `main.js` is loaded, we do not need to load, nor download `MyApp$$anon$1.js`.
208+
It will only be loaded the first time `onClick` is actually called.
209+
This reduces the initial download time for users.
210+
211+
Dynamic imports and entry points can be arbitrarily combined.
212+
213+
### Module Split Styles
214+
215+
So far, we have seen how public modules and dynamic import boundaries can be defined.
216+
217+
Based on these, the Scala.js linker automatically uses the dependency graph of the code to generate appropriate internal modules.
218+
219+
However, there are still choices involved.
220+
They can be configured with the `moduleSplitStyle`:
163221

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

171229
#### `FewestModules`
172230

173-
Create as few modules as possible without including unnecessary code.
231+
Create as few modules as possible
232+
* while respecting dynamic import boundaries and
233+
* without including unnecessary code.
234+
174235
This is the default.
175236

176-
In the example above, this would generate:
237+
In the entry points example above, this would generate:
177238

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

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

245+
The dynamic import example above already assumes this module split style so a module listing is omitted.
246+
184247
#### `SmallestModules`
185248

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

189252
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.
190253

191-
In the example above, this would generate:
254+
In the entry points example above, this would generate:
192255

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

260+
In the dynamic import example, this would generate:
261+
* `main.js`: public module, containing the export of `onClick`.
262+
* many internal small modules (~150 for this example), approximately one per class.
263+
197264
Generating many small modules can be useful if the output of Scala.js is further processed by downstream JavaScript bundling tools.
198265
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.
199266

267+
### Splitting Granularity
268+
269+
Scala.js only splits modules along class boundaries.
270+
It is important to be aware of this when structuring your application to avoid unnecessary grouping.
271+
272+
For example, the following structure likely leads to poor splitting (if `FeatureN`s are not always used together):
273+
274+
{% highlight scala %}
275+
object UI {
276+
def renderFeature1(): Unit = ???
277+
def renderFeature2(): Unit = ???
278+
def renderFeature3(): Unit = ???
279+
}
280+
281+
object Calc {
282+
def calcFeature1(): Unit = ???
283+
def calcFeature2(): Unit = ???
284+
def calcFeature3(): Unit = ???
285+
}
286+
{% endhighlight %}
287+
288+
For better splitting, group code that belongs to the same feature:
289+
290+
{% highlight scala %}
291+
object Feature1 {
292+
def render(): Unit = ???
293+
def calc(): Unit = ???
294+
}
295+
296+
object Feature2 {
297+
def render(): Unit = ???
298+
def calc(): Unit = ???
299+
}
300+
301+
object Feature3 {
302+
def render(): Unit = ???
303+
def calc(): Unit = ???
304+
}
305+
{% endhighlight %}
306+
200307
### Linker Output
201308

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

0 commit comments

Comments
 (0)