Hacker News new | ask | show | jobs
by curveship 3367 days ago
For 1) and 2), some frameworks solve the size issue by creating functions for the basic DOM ops, which then get minimized to single character names in production. So in your example, there might be a setNodeAttribute(node, name, val) function, such that the final code isn't `e.setAttribute("id", "bar")` but just `a(e,"id","bar")`, where `a` is the minimized name of setNodeAttribute.

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.

2 comments

In terms of #3, if you're yielding until the next requestAnimationFrame, the browser is telling you when you have the opportunity to do more work. Is there something that isn't covered by that?

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

BTW if you're curious, browser DOM operations have advanced now to the point that doing work in dom fragments is often slower than just doing it right in the main tree. See, for instance, one of the recent optimizations to the vanillajs implementation of js-framework-benchmark: https://github.com/krausest/js-framework-benchmark/commit/2e...
If I understand what Glimmer is proposing, they want to slice a long update process into a set of batches, so that they can pause in the middle to let the browser render a frame. My points were that a) they don't know how long it will take the browser to render that frame, so it's hard to say when to cut off the batch, and b) rendering intermediate states might increase the overall work, sort of a classic throughput vs latency tradeoff.

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).

Well, Glimmer actually compiles opcodes to just numbers. So it wouldn't be `a(e,"id","bar")`, it's actually [1,'id','bar']. Opcodes' wire format is an array. I haven't see it being a tree yet. If this is the case, stream parsing is definitely possible.
You could do even better than this if you wanted. Since there are only between 100-200 dom attributes, you can pack that into the opcode itself. Assuming 6 bits for the opcode (64 ops) and 8 bits for the dom attribute (256 attrs), you still have another 18bits to play with in a 32 bit int. If you wanted to allow arbitrary attributes, you could dictionary encode them and then use the full 26 bits for a total of 67M possible attribute names. Alternatively, you can reduce that entire op down to just the opcode by dictionary encoding the operand and packing that in as well. How many programs have more than 262,000 static strings? :)

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.

Yeah, I was just looking at the Glimmer opcodes format. I'm a bit surprised it's an array of arrays, rather than a flattened array. It looks like it goes `[[1,"id","bar"],[2,<other param>],...]` rather than `[1,"id","bar",2,<other param>,...]`. Wonder why? Monomorphism, or faster dispatch by not having to pass a pc index around? Interesting.

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.

Yea this is sort of a relic of the initial VM architecture we landed originally in Ember 2.10. That architecture was more like Clojure e.g. read -> compile -> execute. So the nested arrays are seen as sub expressions. You are correct this can be linearized.
Also, this particular format is the "wire format", which is the compact representation that we compile templates into to send to the client.

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.

Ah, but you can pass it as json and have it render much faster. (If I've understood this thread correctly. Please correct me if I'm wrong)