Attendees:
- Shu-yu Guo (SYG)
- Yulia Startsev (YSV)
- Guy Bedford (GB)
- Jack Works (JWK)
- Rob Palmer (RPR)
- John-David Dalton (JDD)
- Bradley Farias (BFS)
- Richard Gibson (RGN)
- Leo Balter (LEO)
- Daniel Ehrenberg (DE)
- A new way to speed up startup perf without losing readability
- Define startup perf: user experience
- Readability: ergonomics
- Children inherit laziness
- If shared between lazy and eager parents, child is eager
- TLA modules are eager
- Problem is polyfills that install something on globalThis - would need to disallow this - but analysis is hard
- Laziness is a feature of the edge or the node?
- Making it part of the Node
- Maybe have pure assertions
- Enables auto laziness
BFS: If it were a feature of the module AND out-of-band expectation may mismatch between user and author. User loses control. It would prevent user from loading lazily if node was missing the marker.
YSV: If we keep it on the edge, you can just lazy load.
BFS: JESSE removes state by freezing the exports. Means classes are not allowed because extends requires evaluation.
YSV: Class with static block. That must eval. Breaking this invariant is useful. That the graph is evaled in order. Reason: lazy subgraphs are a new concept.
BFS: I would argue partially against. Harder to reason if partial evaluation happens. IT's worse than either extreme. So allowing static blocks to eval in isolation (eager, on first use) it becomes too hard to understand when things are going on.
YSV: Deferred module evaluation would defer everything. You defer the subgraph almost as if it's a dynamic import.
RG: Is it recursively lazy? It seems like that is effectively the same as partial evaluation from the perspective of top-level code.
YSV: It's still predictable.
RP: Two use cases. Internal via an edge. Babel transform.
BFS: Will not allow partial evaluation of a single source text with circular modules.
YSV: I propose linking only vs eval everything. No partial evaluation.
SYG: Pure assertion. How serious is this? My gut reaction is that it is won't work.
YSV: It is a serious suggestion. Assertion, for node case with a directive, engine can see it and detect pure module. Idea is that it is stateless. Only constant expressions. Could take this from SpiderMonkey. Gets weird with classes (static blocks, extends). Only export functions.
DE: Function modules seem plausible. Skeptical of constant expressions. Not sure we want to encourage this style. It is opinionated.
SYG: If it's sufficiently inexpressive, just functions, not a sub-language, could be open to it. The thing you want to opt into is different timing. Maybe to use defer-import, all usages require you to think through side-effects.
YSV: TLA complicates this. Solution is to eager eval them.
DE: For function modules, naturally you ban TLA.
YSV: We experimented with deferred modules. What happens if you remove all side-effects? Take mutable JSON module. Pair it with functions. This seems orthogonal to defer-import.
SYG: Less amenable to side-effect-free subset. More amenable to a subset that's easy to recognize.
RP: Would want defer-eval to contain classes.
YSV: That's fine. It's only the pure functions that might limit classes.
YSV: I argue that deferred modules can have the same syntax as other modules should remain. For TLA, it's a breaking case. Technique to mitigate it is eager eval of the TLA module. So full syntax can be used. The invariant that children eval before parents remains, except for lazy children.
SYG: V8 feedback: unfortunate that TLA gets thrown out at the first sign of inconvenience. But I think TLA is maybe just special. Ideally we should hold onto "one JS". ServiceWorkers tried to ban it too.
YSV: TLA is not banned. It's just eagerly loaded.
JW: Is it possible to limits TLAs via module assertions?
YSV: I played with this. The problem is you need both (pure, no async). Or have a permissive mode. Could have "assert: sync". Pair of assertions (assert sync) and attribute (defer-eval).
JW: Use-case for assert: sync is to keep things fast prevent async arising, e.g. for polyfills.
RP: With assert: sync, the old guarantee that you can silently introduce TLA goes out the window. It's now a breaking change if one of your dependencies becomes async. So may lead to fragile parent code.
SYG: V8 team is ok with changing load order. What are points of contention?
BFS: Laundry list:
- TLA eagerly eval'd means parents no longer have right-of-first-access so anything that needs to modify it won't necessarily have access to it anymore. We have things that depend on this for patching, custom elements. So partial eval, even eagerly for TLA, means you would also need to make all parents of the TLA eager.
YSV: Could make permissiveness broader. Could bailout and have TLA make everything eager.
BFS: You can queue up list of modules that need first-access.
YSV: Interesting.
BFS: Just need to wait in-order. If TLA loaded lazily, dependency is unable to resolve until that lazy first access is resolved and done. So can queue up dynamic import, but first access is preserved.
YSV: I will open an issue.
BFS: If we do this, the goal of proposal is eager parsing. Linking is needed to pull in modules. I'm curious what we'll do if we have infinite recursion? You can't stop this. We talked about this with https imports. A server can make it infinite. If a deep thing changes parents, or needs full resolution, it becomes odd. Requires full parse, full link. Move more towards await import. So is this a performance improvement? You still have to do a lot of work.
YSV: This is not a guaranteed improvement. It's heuristic based. 54% of time spent on loading and parsing even on an SSD in Mozilla. Google reported similar. . This is the result with no TLA. Came from Shubee.
SYG: Should talk to Leszek. A missing piece is pipeline loading and exploration. We want to pipeline and stream all the things. Sometimes you have spare time. Without pipelining, then 54% is a pure win. In our architecture it's unclear if it will be that big. But it's still cycles saved.
context: whatwg/html#4400 (comment)
DE: We've been talking about this in WHATWG. I think this should be opt-in. Maybe an attribute on the script tag. Chrome Tokyo haven't found a speedup yet from pipeline. It's quite a different thing from defer-eval.
SYG: Agree. I am pushing back against the JSM loading example demonstrating big wins.
YSV: I will implement this internally. We'll be more aggressive for internal code that defer-eval. But multiple organisations cannot move to ESM with current semantics.
DE: We know that is Chrome ships pipeline, it won't speed things up because people don't ship large module graphs. This stuff can't be used on the web until we have a bundling solution. I think it's good to bring these ideas out into the open. So "another idea brewing" is not an argument against what Yulia is presenting.
YSV: Numbers were just to give an idea of the Firefox use-case. Potentially useful for non-Browser use-cases. I expect this to work with bundling. I liked Dan's unevaluated flag. They are floating in space.
GB: It was always part of the loader design to have a pre-loader. So go as far as you can, but leave out evaluation.
YSV: Initially dynamic import did eager parsing, until it was changed to defer everything.
DE: Even before native ESM, the language feature will be useful in tooling that is emulating ESM. JSON modules have a similar benefit.
YSV: I want to continue investigating performance. To Bradley, you raised two interesting points. Right of first access - let's talk in an issue. Other about parsing - do we need to parse everything? I will engage with you on that. Let's do email with V8.