54) ElixirConf US 2018 – Closing Keynote – Chris McCord

Your approach makes sense if you don’t want to diff on the client, but if you’re going to diff on the client anyway (using morphdom or something similar), I think my approach of sending a list of HTML strings to the client makes more sense. It’s simpler than Drab’s approach (at the cost of requiring a smarter client, of course), and it doesn’t pollute the DOM with custom attributes as much.

1 Like

Why not some type of VDOM solution? I imagine it would be faster than comparing HTML strings, but tbh I have no clue what the performance difference would be.

And what’s the problem with polluting the DOM with custom attributes?

(I am a bit out of my depth here, so these are honest questions!)

I really hope @chrismccord works with you on LiveView. You’ve definitely pioneered and popularized this entire concept within this community.

20 Likes

Morphdom can patch a string representing a DOM into an existing DOM as fast as a VDOM solution (look at their github page here, where they explain how it’s possible: https://github.com/patrick-steele-idem/morphdom). Yes, it has to parse the string into a DOM and diff both DOM trees, but if you’re sending a VDOM from the server you’re not actually sending a JS object representing a VDOM. You’re sending a string representation of a VDOM, which has to be parsed into a VDOM in a way that’s probably slower than the browser parsing a string into a real DOM (this is the thing at which browsers are faster).

As I said above, you’re not comparing HTML strings. You’re parsing the HTML into a DOM and comparing DOMs. Comparing the DOMs is not necessarily slower than using a VDOM.

It’s not that much of a problem, honestly… It’s just something I’d like to avoid if it was I implementing a solution. There was a time when Drab used to pollute your webpage with custom <span> elements, and that would cause some real trouble, but adding attributes is probably very safe and won’t cause problems.

1 Like

Interesting! Sending HTML strings does strike me as a cleaner approach, all else being equal. It’s probably easier to test/debug/extend than any VDOM-type solution that starts on the server-side. Will definitely look into morphdom!

I also feel I must explain the rationale between my solution above (the one which sends only a list of strings into the browser). Having a VDOM computed on the server allows you to send minimal HTML DOM diffs, which the Javascript in the browser can apply to the DOM.

Browsers need the DOM because that’s important for interactivity and to actually render the HTML into pixels on your monitor. But the server doesn’t care about the DOM at all. The BEAM (and by extension Erlang and Elixir) is very efficient at sending bytes through a socket. Dealing with nested structures with backreferences like a DOM is definitely not something for which it is optimized. The main reason why Phoenix templates are so fast is that they do the least amount of work possible besides sending bytes through a socket (see here, for example: https://www.bignerdranch.com/blog/elixir-and-io-lists-part-2-io-lists-in-phoenix/).

My solution (which @chrismccord and @josevalim probably had in mind) intelligently divides work between the client and the server:

  1. The server cares only about pushing bytes around (because there isn’t a GUI to render those bytes). In my solution the server only deals with bytes.
  2. Data transfered through the network is minimized because only the parts of the template that might change are sent.
  3. The browser cares about a DOM, and is highly optimized to process and render a DOM, so all the DOM work is done on the browser.
2 Likes

That all makes perfect sense. I also imagine that your solution is much easier to implement, as it would require additional client-side code instead of significant changes to server-side rendering.

Honestly, the only reason I dislike this whole LiveView/Drab-like approach is that I just recently converted a ‘regular’ Phoenix app, views and templates, to an JSON API-based approach with the usual Rube Goldberg client-side React implementation ;-). I’d much rather have stuck with Phoenix’ server-side rendering…

1 Like

You can optimize data transfer even further at the cost of more memory and CPU cycles on both the server and the client by keeping the dynamic list of strings in memory. When you generate a new dynamic list of strings, you compare the new list and the old one. If strings are the same, you can just send a placeholder instead of the string (null, the integre 0, etc). If you have long strings, this saves some space (a lot of space in some cases, even!).

Then, because you’re also keeping the dynamic list of strings in memory on the client, you can reconstruct the placeholders.

At which step you’re trading CPU and memory for data transfer. Depending on your solution, this might be the wrong choice. For example, while the overhead of keeping part of a webpage in memory is low on the client, if you have millions of users, you now have to keep millions of dynamic lists in memory, which might kill your servers… So it’s not a decision to make lightly.

Trading CPU usage for Network transfers is usually the right choice (the network is very slow), but it might transform an IO bound task (at which Elixir excels) into a CPU-bound task (at which Elixir does not excel)

I don’t think of moving code from the server to the client as “easier to implement” xD The complexity is still there, (someone, maybe you, will have to maintain and debug it); you’re just moving complexity around. In this case, you’re building on top of the morphdom JS package, which has some complexity on its own.

The advantages of moving complexity around are that now you’re spending CPU cycles on the client instead of the server and you’re playing on the browser and the server’s strengths: the server dumps dumb bytes into a socket and the client builds and handles a “smart” DOM

This is the approach I suggested but it doesn’t work with nesting (for example, form_for). So when using LiveView someone would need to reduce the amount of nesting that happens in templates (it is doable though). So different tools will make different trade-offs here and that’s totally fine IMO. Hard to say which one is the best without benchmarking and looking at example applications.

Me too! The repo is out, although very early on: https://github.com/elixir-telemetry/telemetry/ - We are considering already integrating it with Ecto 3.0 though.

3 Likes

Thank You for your amazing tool, cannot wait to see what will come next :slight_smile:

4 Likes

If does work with nesting, it’s just less efficient. If the nesting happens in a big for loop, you simply send the result of the for loop as a dynamic substring. It’s not optimal, though… The user must write the template using as much concrete EEx syntax (no nested templates, no reusable widgets, etc.) as possible. Unless the widgets are macros, in which case we can expand them and isolate the dynamic parts (should be a fun exercise in static analysis). It’s still strictly better than sending the full webpage.

To do better than that one probably has to do some hardcore static analysis (which I believe Drab does).

Exactly! Besides saying that sending less data is always better, I don’t think there are many hard and fast rules here.

But if I had to bet, and without any hard data to look at, of course, I’d bet it will be hard to do better than our (yours and mine) naive approach of going only one level deep with no fancy caching of substrings besides what we get for free when using IOLists.

EDIT (further baseless speculation): going deeper than one level is tempting, of course, but then you need some sort of caching to compare the previous tree to the new one (and congratulations! you’ve just implemented your simplified VDOM on the server!). And caching consumes non-trivial amouns of memory. Maybe one could use checksums? If you checksum a string into a small int (< 32bits, for example) you get a negligible probability of collisions with a very low memory overhead. As long as the checksum function is fast, it could work. You could turn the (nested IOList) into a tree of checksums, which would be very easy to diff agains the old one (like a Merkle Tree with no cryptographic hashes) with a non-cryptoraphic hash. Diffing HTML on the blockchain, yeahhhh baby!!!. I should stop now. But baseless speculation about algorithms is fun!

Unforutnately somebody can do this:

<%= convert_io_data_to_html_and_do_a_string_replace_on_some_tags(for foo <- foos do %>
  <foo>bar <%= foo.id %></foo>
<% end) %>

Plus if we want to support nesting it means we need to generate identifiers, traverse, encode and decode them. So I don’t think it is worth it. If you want to go down this road, then a proper template engine is the way to go.

So that’s the kind of nesting you’re talking about :slight_smile:

Once you compile the template into a quoted expression, isn’t it easy to mark the whole thing as a “dynamic substring”? (I’m not testing it mysef because I’m away from the computer)

Yes, totally. We just can’t do the “smart diff”.

1 Like

One thing I was curious about when it came to input fields is, on video you mentioned on every key press this validation takes place, but do you think we should allow for overriding this behavior with a configuration option to account for “typewatch” delay.

For example if I’m typing out an email address, I really don’t need validation to happen on every single character as I press it. What I really want is the validation to happen 2-3 seconds after I stop typing. This way when I type normally, I might hit the server once or twice instead of 30 times.

This could alleviate a lot of extra round trips. Maybe each form you attach these events to could have an option to enable a typewatch delay which defaults to either a 2-3 second delay or 0, whatever ends up being best after testing this out for real.

Haha exactly this. If LiveView existed a year ago my formerly EEx-based project would be live by now!

But, hey, at least I feel much smarter having spent many months learning enough es6, then vue then react/jsx, nextjs, reasonml, webpack, and absinthe/graphql just to get client-side form validation! I know not all of those were strictly necessary… I could have just sprinkled some js/jquery or maybe used something like unpoly to get the job done but a future feature requires substantial client-side state so figured I’d go “all in”. Needless to say Chris’ Rube Goldberg slide in this talk and how he described it as “absolute insanity” really resonated with me. As he states, LiveView represents a HUGE improvement in time-to-market for a wide variety of use cases in the middle of the continuum.

For me, after that fun rewrite everything works well at this point and it’s just too late to go back. It was a major sunk upfront cost that LiveView addresses but at this point the big unknown is how my future maintenance/change iteration cost differs between a LiveView approach and the React SPA approach. I guess we will see. :grimacing:

Yes, some kind of configurable delay is planned as not everyone needs instant keyup events. My goal is to focus these things around UX rather than saving round trips since we should be able to get away with it. So for keyup events, my goal is to focus the UI updates exactly how you would for a client side app, so you would use whatever makes the best experience for the user. There are of course perf considerations, but we can push a lot further than most folks realize :slight_smile:

1 Like

Yes, this is exactly the limitation I had in mind. Maybe I didn’t explain myself clearly. I think there should be no “smart” and no “diffing”: what is static is static, and what is dynamic is dynamic.

What can help a little here is replacing some (most?) of the functions in Phoenix.HTML by macros (Phoenix.LiveView.HTML, for example). One can use macros whenever the function is not recursive (or at least the recursion is bounded).

That will give us a big quoted expression, from which we can try to separate the static parts from the dynamic parts. That might become hairy but it’s just an exercise of turning a tree inside out, or something like that. As long as you stick to the default widgets or widgets that are implemented as macros you can support a bounded level of nesting.

That would suport form_for in most cases where it’s used.

And not smart diffing required.

I take this back. It is NOT very easy… Chunks like <% x = ... %> require compiling the template into a {:__block__, meta, expressions}, which forces us to deal with variable assignments which might possibly overwrite each other. To split the template into static and dynamic parts it looks like one has to (at least) convert Elixir blocks with variable assignment into single static assignment (i.e. almost the same as compiling macroexpanded Elixir into Erlang). Then it becomes easier to factor variable assignments outside the dynamic strings. I feel like I’m derailing this thread a little, and I might start a new one to discuss these matters.