And second, generating imperitive code makes it harder to perform runtime optimizations. For example during initial render Glimmer could note if a property lookup yields an Immutable object and use that knowledge to disregard change tracking for properties off that object. There are some fun things we can do here.
It turns out JavaScript engines are very good at iterating flat arrays and running small, highly optimized functions and that's what Glimmer is doing at its core. When your compiler output generates functions, you're creating lots of functions specialized for each template which means a larger surface for the v8 JIT to worry about optimizing. By instead shipping lots of small, hot functions to be re-used (instructions fired via opcodes), we can get a better optimization result from v8 and other engines.
1) Size - turning each op into a couple of bytes means that the size of your template is significantly smaller. If each instruction is 4 bytes, I could get ~20 instructions in the space of just one function with a setAttribute call:
function t(e) {e.setAttribute("id", "bar")}
Size is much more important on the web than it is in other places especially as the next billion people start using the mobile web.2) Parsing speed - given the size of the JS that templates produce, you start running into JS parsing performance. Just by volume you're going to eat an insane amount of time not just downloading the JS but then trying to turn it into something executable. A correctly implemented bytecode VM could easily beat the cost of parsing.
3) Scheduling - If you just produce raw JS code, you don't have much room to dictate how it executes. Since glimmer's goal is to never miss a frame, they're going to have to take control of the work that gets executed to make sure that they always pause at a frame boundary. That's a much more straightforward thing to do in a VM, where pausing work is just a matter of yielding the interpreter loop. This gives you complete control over how you schedule the work from the ground up. Have some huge dom tree to render? Split it across 10 frames without doing a bunch of control inversion.
In terms of cost, I haven't looked at their implementation, but I assume these guys did their homework. You can implement interpreters that execute instructions in a few nanoseconds without too much effort. If you really put in the effort, you can do it subnano, but that's outside of the scope of handwritten JS. Even if the overhead was 20x a normal call, the cost of the operations this interpreter is running makes that a rounding error. The DOM is slow and the other benefits almost assuredly outweigh whatever tiny cost they're paying at the per instruction level.
There are lots of other potential benefits as well: opportunities for specialized optimizations over the bytecode (you could basically do your own domain specific jit), ease of implementing the base VM for different targets, and so on. There are relatively few times when writing your own interpreter probably makes sense, but it seems like this architecture would give them a ton of headroom to do some great stuff down the line.
There's really not much noise in that expression, just the "(,,)", so maybe 4 chars that an opcode could save.
As for 3), is that really possible? If you profile most modern frameworks, they're already fast enough that most of the time is spent in rendering, not in javascript DOM manipulation. So even if you cut short your js before 16ms (60fps), you have no idea how long the browser is going to take to render your changes. Plus, the browser will be doing extra work, since it needs to render all the frames in which you've only done part of your updates.
Compared to the string encoding above we went from 14 chars (1-4 bytes each, we'll just say 2) at 28 bytes to 4 bytes for our 32 bit int.
The `a(e,"id","bar")` format is what other frameworks produce. It sounds like in Glimmer it would be `[1,"id","bar"]`. So that's only a single char savings.
The client then compiles that representation into flat opcodes, in part by specializing the template based on runtime information (like the exact identity of the components in question).
The runtime opcodes are binary (128-bits apiece at the moment) and optimized for reasonably fast iteration. The wire format is, as chadhietala1 said, not as flat or compact as it could be, but still much more compact than our earlier representations (or the representations of competing rendering engines).
We plan to improve the wire format representation in the near future.
> they're already fast enough that most of the time is spent in rendering, not in javascript DOM manipulation
That hasn't been my experience, but it's been awhile since I've benchmarked any of the frameworks in common use. Change tracking, diffing, and then the dom calls have all been the bulk of the work in large updates. Assuming you're doing those in a dom fragment I'm not sure how "rendering time" (I'm taking that to mean compositing and painting?) could be the bottleneck in that scenario.
Paint and composite are usually fast, but calculate styles, layout and hit test may not be. It totally depends on the complexity of the DOM and CSS, of course, but as an extreme example, the js-framework-benchmark tasks are often 90+% time in render. That's why the results converge on 1.00: 0.95 of that is time spent in the browser rendering the DOM, and the time spent in javascript between a framework at 1.00 and one at 1.05 may be 2x difference (0.05 vs 0.10).