# Module me Harder
## JS Modules: Determinism vs. ASAP
> Author: Domenic Denicola[^1]
[^1]: https://docs.google.com/document/d/1MJK0zigKbH4WFCKcHsWwFAzpU_DZppEAOpYJlIW7M7E/edit?usp=sharing
## Problem Statement
On a given web page, each <script type="module"> generates a module graph. (It is not a tree; circular dependencies are allowed.) The graph has a starting node given by the <script type="module"> itself.
Traditionally the design has been that the module graph has a deterministic ordering, given by a post-order depth-first traversal from the starting node. Given:
|  |  |
| -------- | -------- |
The spec prescribes the evaluation orders:
- Example 1: a.js, b-2.js, b-1.js, root.js
- Example 2: a-2.js, a-1.js, b.js, root.js
Furthermore, as written the spec prescribes a two-stage model of fetching, then top-down evaluation. That is, first all four scripts are fetched, then root.js is evaluated, which in turn recursively evaluates in the given order.
We will call this strategy the **super-deterministic strategy.**
## The problem
This kind of staged fetching-then-evaluation is potentially leaving performance on the table. It leads to situations where the main thread is doing nothing for a long time while fetching occurs, then janks for a long period while all the evaluation happens at once.
Ideally, we would like a system that allows evaluation to be interspersed throughout the fetching process, or otherwise split up, thus leading to less consecutive main-thread work and a quicker time-to-all-JavaScript-ready.
Note that it is possible to allow arbitrary flexibility in evaluation time, by adding multiple <script type="module" async> elements; among these, evaluation is unconstrained. This is discussed in more detail later in [The opt-in vs. opt-out objection]
#### Thus the discussion about "solutions" to this issue is really about what the default behavior should be, within a single graph generated by a single <script type="module">.
## Proposed Solutions
## The weakly-deterministic strategy
One proposal is to allow the browser to start evaluating scripts when they are finished fetching, as long as doing so preserves the canonical evaluation ordering. This has the following impact on our examples
- Example 1:
- If b-1.js or b-2.js is slow to fetch, the browser could evaluate a.js in the meantime
- b-1.js and b-2.js can never evaluate before a.js
- Example 2:
- If b.js is still not fetched by the time a-1.js and a-2.js are fetched, the browser can evaluate a-2.js then a-1.js while it is waiting.
- b.js can never evaluate before a-1.js and a1-2.js
As the examples show, this strategy gives clear benefits for "shallow on the left" graphs like Example 1, whereas it is generally less helpful for "deep on the left" graphs like Example 2.
The reason that the weakly-deterministic strategy is observably different from the super-deterministic strategy is due to error handling behavior. For example, if in Example 1 b-2.js contained a syntax error, or returned a 404, or imported a nonexistent binding, under the super-deterministic strategy this would be detected ahead of time, and nothing would be evaluated. Under the weakly-deterministic strategy, a.js would still be evaluated, since evaluation would occur before b-2.js is even parsed.
> Authors note: Domenic's gut feeling is that the extra complexity of the weakly-deterministic strategy is not worth it, given that it only applies for "shallow on the left" graphs.
## The deterministic-with-yielding strategy
Another proposal is to preserve most characteristics of the super-deterministic strategy but yield to the event loop between module evaluations. This avoids the jank of synchronously executing a lot of script at once. However, it does not help with time-to-all-JavaScript-ready.
There is a variant of this that is based on the weakly-deterministic strategy as well, although that already has some built-in yielding.
## The ASAP strategy
The most radical proposal is to get rid of the conventional ordering of module evaluation entirely, in favor of simply allowing any module whose descendants have all evaluated to itself be evaluated. This would mean, for our two examples, the allowed evaluation orders are:
- Example 1:
- a.js, b-2.js, b-1.js, root.js
- b-2.js, b-1.js, a.js, root.js
- (\*) b-2.js, a.js, b-1.js, root.js
- Example 2:
- a-2.js, a-1.js, b.js, root.js
- b.js, a-2.js, a-1.js, root.js
- (\*) a-2.js, b.js, a-1.js, root.js
(The (\*) configurations could possibly be eliminated by adding stronger constraints, and don't seem to add much value to allow.)
As can be seen, this allows the maximum flexibility in terms of evaluation ASAP during the fetching process: much more than the weakly-deterministic version. In this version, "shallow on the left" graphs are not favored.
We spend extra time on examining the implications of the ASAP strategy since it is the most radical change on table.
### The polyfill objection
The conventional objection to the ASAP strategy is as follows. Consider an example such as

That is, the developer has written the following code in app.js:
```js
import "./web-components-polyfill.js";
import "./super-image.js";
```
> The rest of the app uses <super-image> as a custom element
With the ASAP strategy, a valid evaluation ordering is: super-image.js, web-components-polyfill.js, app.js. This will obviously not work (in browsers which need the polyfill): super-image.js will try to use customElements.define, and blow up.
One objection might be: if super-image.js needs that functionality, shouldn't it explicitly import it? But this ignores how polyfills are meant to be used. Component developers should not need to write their code to depend on polyfills; they just assume the standard API is available. Maybe they have even authored their component in an environment, like modern Chrome, where it always is! It's the developer writing app.js who is responsible for setting up the environment to ensure their dependencies work correctly. They attempted to do so here, but failed, due to the nondeterminism of the ASAP strategy.
A workaround is to use classic scripts for polyfills. But this is a very high cost: it says polyfills cannot make use of code sharing via modules, and cannot benefit from the other benefits of modules (such as a top-level scope contour that does not create globals, or automatic strict mode). It goes against our strategy of hoping for a world where eventually all scripts are module scripts.
In general this objection extends to any kind of manipulation of global state. For example, consider if the above example were expanded to
```js
import "./web-components-polyfill.js";
import "./super-image.js";
import "./rest-of-app.js";
```
Can rest-of-app.js assume that <super-image> is a defined custom element? Or does each file that uses <super-image> need to add its own import "./super-image.js" statement?
### The opt-in vs. opt-out objection
Another issue with the ASAP strategy is that it is impossible to opt out of ASAP behavior if the browser implements it. Whereas, if the browser implements a deterministic strategy, it is possible to opt in to an ASAP strategy by using <script type="module" async>.
To illustrate, consider the following variation on Example 1:
```js
<script type="module" async src="root.js"></script>
<script type="module" async src="b-1.js"></script>
```

Because evaluation order among the <script type="module" async> elements is arbitrary, any of the following evaluation orders is possible:
- a.js, b-2.js, b-1.js, root.js (the normal ordering)
- b-2.js, b-1.js, a.js, root.js
- (\*) b-2.js, a.js, b-1.js, root.js
(The (\*) ordering is only possible if we use the deterministic-with-yielding strategy, as that would allow such interleaving.)
Thus the web developer has recovered the full ASAP-ness of the ASAP strategy, despite the browser implementing a deterministic strategy, by explicitly opting in to nondeterminism.
In contrast, given the ASAP strategy as a starting point, there is no way to recover the deterministic ordering, since the mere presence of two import statements in the same file introduces unrecoverable nondeterminism.
This could be overcome in time, by adding a feature to opt in to determinism inside a default-ASAP world. Something like <script type="module" blocking> and its counterpart, import "./x.js", blocking "true" or similar. (Syntax is a strawman, especially [the import syntax](https://www.google.com/url?q=https://discourse.wicg.io/t/specifying-nonce-or-integrity-when-importing-modules/1861/4&sa=D&source=editors&ust=1671719393457431&usg=AOvVaw1Gx3HFT4Oun-58fDNewMgL).)
### The unreliability anti-objection
An objection to the previous objections is that even with one of the deterministic strategies, there are not actually any evaluation order guarantees, in a world where multiple graphs are involved. That is, consider the following variation on Example 3:
<script type="module" src="app.js"></script>
<script type="module" async src="super-image.js"></script>

As discussed above, the <script type="module" async src="super-image.js"> is not ordered with respect to the <script type="module" src="app.js">, so it could load first. This would lead to the same breakage encountered when using the ASAP strategy in Example 3.
The argument then is that, as a module author, you can never guarantee that one of your dependencies doesn't also appear as a <script type="module" async> elsewhere, and thus even with the deterministic strategy, you don't have any guarantees that the evaluation order implied by your source code will actually be carried out.
While it's certainly true that this provides a proof of concept that belies the supposed guarantees of determinism, it's not clear how realistic of a threat this is to module authors' mental models. Adding such a <script type="module" async> is a deliberate action, unlikely to happen by accident or without knowledge of the implications. And using <script type="module"> at all for "dependency modules" like super-image.js, as opposed to "top level modules" like app.js, is not an anticipated use case---apart from perhaps the previously-mentioned opt-in to ASAP semantics.
## Conclusion
Given the above analysis, we think that nondeterministic strategies are not worth the ecosystem cost they induce. Nondeterministic strategies reduce the module system to being only a recursive-file-loading system, not a true dependency-aware module system. It would be an incredible break from how people are using modules in the browser today (with bundlers, AMD, custom elements/React components, etc.)
Without the ability to impose ordering constraints, we anticipate developers having to reinvent a new true module system inside the ES module system, in order to get the predictability they are used to. Or they may just opt out of ES modules entirely, in favor of their existing CommonJS or AMD module tooling.
Additionally, the ability to opt-in to nondeterminism as appropriate provides a powerful antidote to any potential slowness, to the extent that it is safe in any given project.
Finally, it's worth noting that we are not alone in this process. Any decision here needs to achieve consensus from all vendors---and all three other engines are further along in their module implementation than we are, with Safari's already shipped unflagged in their Tech Preview builds. A switch to nondeterminism this late would be seen as disruptive and unlikely to achieve consensus.
As such the choice ends up being between the different deterministic strategies. We believe that the web-observable differences between them are likely minor enough that we can ship-and-iterate between them, gathering data. In particular the deterministic-with-yielding strategy has some promise as a potential replacement for the default strongly-deterministic strategy. The weakly-deterministic strategy seems like a bad compromise unless real-world data shows that most module graphs are "shallow on the left".