stimulusreflex / stimulus_reflex

Build reactive applications with the Rails tooling you already know and love.
https://docs.stimulusreflex.com
MIT License
2.26k stars 171 forks source link

LiveView .leex inspired payloads #232

Closed leastbad closed 3 years ago

leastbad commented 4 years ago

Feature Request

We frequently get requests / inquiries about whether SR supports intercalated micro-payloads, similar to https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.Engine.html and as described here: https://youtu.be/8xJzHq8ru0M?t=1378

Is your feature request related to a problem?

This is an admittedly clever innovation that would drastically reduce the wire size of Reflex updates, which could be very important in low-bandwidth regions eg. 3G networks. Programmers have a natural resistance to serving 3k of HTML to update a 0 to a 1 on the screen.

Describe the solution you'd like

To some degree, the need for this approach is offset by the partial updates proposed in #211, which allows the developer to morph a small subsection of the page. It's also true that the various tools available in CableReady might allow for surgical updates of values in a way that is more flexible than what is offered by LiveView.

Realistically, this is likely the solution we're going to be working with unless something drastic changes. The amount of work required to implement something like .leex is non-trivial.

Additional context

However, I would love to be wrong about this and it's possible that a clever person with more opportunity and ability to concentrate could pull off an implementation in a few hours or days, as Julian did with his message_bus PoC. I've been sketching out ideas and making lists of helpful links for six months, and I wanted to share them in a practical place just in case they might be helpful to someone looking to 💪 .

As you will quickly understand from watching the video, this technique involves breaking down your templates into two intercalated arrays: static, unchanging strings and dynamic bits. You also want to be able to recognize when you can do optimized list comprehensions. The complexity ramps up quickly, however, when you start to realize that Rails devs are used to having branching logic in their views that will blow up attempts to normalize the structure of a page between render operations.

My idea for how to best approach this would be to have SR register a new template handler in Rails that would process ERB templates that agree to follow some strict rules about (no) branching logic. You'd have full ERB functionality, but also a "code of conduct" the developer would have to accept that precludes them from doing things that are guaranteed to fuck up their intercalation integrity.

Things get fuzzy from here. If you're a more confident developer than I am, perhaps you attempt to parse the ERB template to establish where the static and dynamic segments start and end. Another approach that is far less elegant but possibly easier to get working is to preprocess the ERB file for tokens that signify the beginning and end of dynamic content areas + list comprehensions. Sort of like a really ugly heredocs:

<div>My name is >>DYNAMIC<%= @name %>DYNAMIC<<.</div>

Yes, that burns my eyes, too. Part of the reason this initiative never got off the napkin. Obvious some kind of ERB parsing is the only sane way forward... but I stress that something like the above would allow you to get to testing other parts of a solution quickly, without burning time trying to build an ERB parser or get an existing one working. Sometimes a quick and dirty early win is the best way to hit a confident stride with a new project.

Some links you might find helpful:

Erubis .erb parser (old + dead): https://github.com/kwatch/erubis

Erubi "small ERB implementation": https://github.com/jeremyevans/erubi - I emailed this fellow in 2019 to test the waters for collaboration and he was extremely polite while conveying that he couldn't be less interested in helping ;)

State management with LiveView and LiveEx https://medium.com/grandcentrix/state-management-with-phoenix-liveview-and-liveex-f53f8f1ec4d7 - probably interesting to @joshleblanc given his recent work on view-component-reflex

LiveView Engine source: https://github.com/phoenixframework/phoenix_live_view/blob/master/lib/phoenix_live_view/engine.ex

List comprehension discussion: https://elixirforum.com/t/phoenix-liveview-list-comprehension-and-conditional-evaluation/23581

Refreshing assigns across templates: https://elixirforum.com/t/live-view-refreshing-assigns-across-templates/21107

Optimization possibilities discussion: https://elixirforum.com/t/phoenixundeadview-lets-discuss-optimization-possibilities-for-something-like-phoenix-liveview/16533

Leex template improvements: https://elixirforum.com/t/livecoding-eex-template-improvements-twitch-livestream/18116

leastbad commented 3 years ago

Closed due to too much enthusiasm. 😁

rickychilcott commented 3 years ago

I actually read through this whole issue yesterday, watched the video, read through some of the linked sources, and have been contemplating how this might happen in Ruby. I'm not sure I'm sufficiently skilled enough to pull together a PR together myself without sitting down with someone else to really think through it.

I think people are afraid to raise their hand or discuss because a) SR is just now starting to get some public mind share (so people haven't seen or thought about it) and b) it feels like a big gnarly problem that should be possible but will require lots of work to figure out if it really is in ruby.

I poked around for a few minutes just now and found that https://github.com/ruby/ruby/blob/master/lib/erb.rb#L642 ERB::Compiler#compile_content gives us some interesting stuff and the ERB::Buffer objects @line instance variable has some of what I think we'd want. Since ERB's templates, once compiled, shouldn't change, I wonder if finding a way to expose the lines within the Buffer instance and finding a way to know if the Binding object that previously rendered the template has changed could give us some form of what we want?

It's a big topic with unknowns, would you be interested in pairing on a small aspect of this at some point?

leastbad commented 3 years ago

Thanks for the thoughtful comment, @rickychilcott.

@hopsoft and I have gone back and forth a few times on whether this would be great or meh, and right now Nate's excited and I'm meh. My perspective at the time of this comment is: the confluence of brotli compression shrinking full-DOM payloads by 80% or more and the availability of selector morphs means that a successful implementation of this feature is a lot of added complexity for decidedly diminished returns. Even in the case of Phoenix LiveView, implementing something like morphs instead of boiling the ocean with an admittedly sexy approach like .leex might have been a smarter use of Jose Valim's time. Only time will tell, but I have a hunch that the true gains of .leex come out in the wash. The true sources of slowness are usually inefficient SQL calls, failing to cache and going through the Rails stack. Morphs really moot all of that.

Now, @hopsoft does have a partial implementation of a server-side diff functionality in the works, and I'm excited to see where that goes. He's super busy at the moment, but he's probably the best person to talk to about that endeavor.

rickychilcott commented 3 years ago

Understood @leastbad. I'm not sure the "juice is worth the squeeze" either, but it's a fun thought experiment. It's also a good marketing tool in the fight to get SR out there. While small templates wouldn't likely benefit, larger templates with small changes could see massive speedups. I wonder if there is a more naive implementation than how Phoenix LiveView handles it that might get us there with less work.

A few things that I kept hacking/thinking about after I saw your last message:

ERB Parsing to determine dynamic vs static bits.

I tried to play a bit more with shimming in different functionality to the standard ERB parser and I think it's flexible enough to at least surface the correct interesting parts of the ERB template that are more likely to be dynamic. However, given that the ERB template handler renders using a Ruby Binding object to pass in functions, instance variables, etc. I'm not sure there is going to be a clear way to "track changes" to that underlying Binding.

We could certainly track when a new instance variables are set, but given that functions need to be executed with their ERB template derived arguments (which themselves could be dynamic based upon the template), I don't see that approach working out.

Moreover, with some cursory investigation, Binding is written in C and is not easily subclassed to add more functionality, logging, etc.

ERB Patching

As I thought about this more, I wondered if maybe the better approach would be to just render the ERB template normally, and then provide some way in Stimulus Reflex or Cable Ready to diff the previously generated template with the latest template. Perhaps that's what @hopsoft has mocked up in your last comment @leastbad.

My thoughts on this are that we could then use something like https://github.com/google/diff-match-patch (currently no Ruby implementation) to generate a text diff server-side of the HTML and then use Cable Ready to send down a diffed_morp(...) command which would be received client-side, look up the previously cached template, apply the patch in JS-land, and then use morphdom to take us the rest of the way. This will likely require keeping state both server-side and client-side in order to work and creates complexity in both implementations.

Whether this ultimately lives in Stimulus Reflex, CableReady, or a combination of the two is yet unclear. Based on my explanation above, it seems like Cable Ready would need the diffed_morph method and it can be responsible for keeping track of the previously rendered template vs the latest rendered template, or maybe it's just responsible for creating the diff and sending it down the wire. We would also need a Ruby implementation of the diffing algorithm (would be most performant to create a Ruby Extension using google's CPP implementation since the benchmarks for interpreted language implementations of diff-match-patch) are generally abysmal.) Google's diff-match-patch appears nice because it is a) simple API across many different languages, b) has line and character-based diffing, c) is reasonably fast, and d) is Apache-licensed.

That's all I've come up with for now. @hopsoft let me know if you want to take this further in any way.

leastbad commented 3 years ago

That link to diff-match-patch is really interesting; it seems like where there's Python (or JS) there could be Ruby very quickly. I've been noticing a lot of cool articles and developments happening in the realm of CRDTs lately eg. https://josephg.com/blog/crdts-are-the-future/

@hopsoft's implementation makes use of Nokogiri so it's in the same ballpark as what you're talking about in the ERB Patching section.

My position is that if it can be done without a ton of pain and doesn't add undue complexity to the library, awesome!

leastbad commented 3 years ago

Thanks to @marcoroth it turns out that https://github.com/kalmbach/diff_match_patch is a thing.

marcoroth commented 3 years ago

There is even a "more modern" fork of that said library. https://github.com/ezkl/dimapa

rickychilcott commented 3 years ago

@hopsoft do you mind skimming my "ERB Patching" section and see if you have any initial thoughts on the overall approach? Perhaps you want to share what you've spiked on already or maybe layout an alternative approach. If we can divvy up the work, or at least get conceptual agreement on the approach, we can take advantage of this tiny bit of momentum.

leastbad commented 3 years ago

@rickychilcott are you on Discord? If not, you should be! That's where 97% of the magic unfolds...