Context
Three.js is not as tree-shakable as it could be. In my tests, a simple console.log(Vector3) weighs ~10KB after minification and DCE. When Vector3 is rewritten with tree-shakable methods, that drops to ~0.2KB.
I recently presented at TC39 on the large and growing cost of dead JS on the web. I believe we could encourage substantial performance benefits for end users by evolving JS to make tree-shakable coding patterns more attractive. A significant source of non-tree-shakable code is unused class methods. Since Three.js is popular and uses classes a lot, I wanted to reach out to your community to get your feedback on our current ideas!
Ideas
I think all of these ideas:
- can support optional chaining and autocomplete.
- would be a runtime performance improvement even beyond tree shaking thanks to static dispatch rather than dynamic/prototype dispatch.
- can be transpiled without performance penalty or bloat
- can support a backwards compatibility layer for users of the existing prototype-based API.
Note:
- Assume we have to choose only one or none.
- The exact spelling of these operators is not set in stone.
This-based chaining (..)
Aka “extension methods” aka “call-this operator”
/** @this {Vector3} */
function set(x, y, z) { this.x = x; this.y = y; this.z = z; }
new Vector3()..set(1, 2, 3);
obj..method() invokes method from lexical scope with this === obj.
-
Main pro: Immediately useful with all existing class methods for defensive coding, performance, pre-migration to tree-shakable methods, etc:
-
Main con: Leans into the
thiskeyword, which many consider to be a confusing concept.
For more details, see https://github.com/tc39/proposal-call-this
Topic-based chaining (|> ##)
Aka “Hack pipelines”:
/** @param {Vector3} _this */
function set(_this, x, y, z) { _this.x = x; _this.y = y; _this.z = z; }
new Vector3() |> set(##, 1, 2, 3);
|> provides the left-side expression as ## (the “topic” token) to the right-side expression.
-
Main pro: General-purpose. Compatible with all existing standalone functions and other expressions.
-
Main con: More verbose (explicit topic token).
For more details, see https://github.com/tc39/proposal-pipeline-operator.
Arg-based chaining (::)
Aka “Elixir-style pipelines”:
/** @param {Vector3} _this */
function set(_this, x, y, z) { _this.x = x; _this.y = y; _this.z = z; }
new Vector3()::set(1, 2, 3);
obj::method(...args) invokes method from local scope with obj inserted into the first argument position.
-
Main pro: Compatible with many existing standalone functions since it doesn’t depend on this.
-
Main con: The argument list no longer contains all the arguments, which might be surprising/confusing.
The Elixir-style pipeline doesn’t have an existing proposal, but a few of us are open to it if you find it substantially more attractive than any other option.
Thoughts?
My main questions:
-
Which of these, if any, would you definitely adopt, if standardized?
-
Which of these, if any, would you refuse to adopt, even if standardized?
-
Any other ideas for how we might improve the language to make JS more tree-shakable?
Looking forward to hearing from you!
