Is Phoenix Liveview an existential threat to Clojurescript?

For those of you not familiar to what is Phoenix LiveView, done in Elixir, this is a good video to know more: https://www.youtube.com/watch?v=8xJzHq8ru0M
There’s even a port to Clojure: https://github.com/prepor/liveview-clj

The basic idea is to remove the complexity of the JS ecosystem, by building your rich interactive web applications not in an Elixir.js but on the backend and very smartly communicate the changes in the data model and (very rarely) the components views over websockets. At first I thought this is naive and dismissed it, then I quickly realised it wasn’t and that is something that has a lot of advantages: no more overhead with the js ecosystem complexity, but also no more JWT tokens, no more translation files sent to the frontend , no more async/await/promises/go blocks… and so on.

We could of course reproduce the entire things using an atom on the backend, which holds the states for all our clients (even cluster it so we can handle a massive number of clients) but we might not get the same level or performance and most important, resilience that the BEAM (ErlangVM) offers for free.

So then, would it be a good idea for our community to get creative and respond with something that could be even better? For instance I like Hiccup instead of html as text, as well as sometimes js is necessary so maybe we could also integrate with the npm a la shadow-cljs if needed. We have been using websockets to push code on the frontend for years (fig wheel/shadow-cljs) so could we do it at runtime in production?

I don’t know , the idea is to brainstorm alternatives that could make the entire process of writing web apps better going back to the first principle that browsers paint html/css and don’t care where or by whom is generated.

1 Like

I have a project sharing similar ideas with LiveView. It’s called Cumulo and was inspired by React. React diffs virtual DOM to find updates in DOM. Cumulo diffs data tree on server to find data changes on clients. I regard Cumulo as a project for toys and maybe in some tooling since it has severe performance issues cause by diffing.

So the question comes to me: is LiveView fast or slow? If you want find changes in a deep tree, you have to use diff. If you want to make it fast, you need to narrow down the operations on the tree, or even turn operations into small ones maybe like CRDT. What does community of LiveView think about this dilemma?

Another aspect of Cumulo is it’s only a data library. To render DOM, I still need a virtual DOM library. So Cumulo holds the data, which corresponds to global store of a client MVC app, while I still need states. States are maintained in browser memory and instantly responds to user interactions, like popping up a modal after clicking on buttons, holding states or animations during dragging. In the point of view of Cumulo, I can hardly get rid of in memory states if I need instant responses. So, no more js? I don’t think so.

For the performance of WebSockets on prod, in a large scale, I got same wonder too.

1 Like

Is Phoenix Liveview an existential threat to Clojurescript?

Heck no.

Heck no. Seriously no. Nooooooooooooo.

Fundamentally this is all about trade-offs.

Every solution is gonna do some things better than others. Liveview/Livewire/Turbolinks/etc. seems easier to reason about since its not your JS you have to worry about. Instead you hope that whatever JS the “framework” provides solves all the problems you are ever gonna have. Good luck if it doesn’t though.

Sure Liveview looks cool in certain scenarios and the easier your UI is the more sense it makes. If your page is mostly server-rendered and just occasionaly needs to do something dynamit then that is fine.

React or the general SPA-trend is overkill for most things. That is absolutely true and many webapps would benefit by something more liveview-ish. That does not mean React-ish views are never useful.

Chris McCord has this slide in his talk which sort of represents a scale and you have to decide where your app lands on that scale. Sometimes it makes more sense to do one thing sometimes another. There is no framework that covers 100% of all use cases and there probably won’t be. Again … trade-offs …

Finding the right balance is the challenge and it is very difficult especially if you don’t know much about the frontend. IMHO you will never get around learning frontend stuff. A framework might you get you to a certain point quicker but more often than not those frameworks will ultimately be in your way just as much.

In the CLJS world we however could definitely back-off a little and not treat React as the one-solution-to-rule-them-all. :slight_smile:

10 Likes

Jon, Thomas, thank you for your answers. This is exactly what I was looking for, more opinions . And I got not only that but also better ones :slight_smile: And yes, I do know about tradeoffs, and probably a bigger product/project will use the 3 models at the same time (web 1.0, Web 2.0 spas and the LiveView / clones model) each probably used where it’s more suitable. Maybe we just need the 3rd option, and maybe we can do it better then LiveView and the clones.

Deciding where your app lands on that scale is for sure the biggest problem here. Few years ago I chose to do full-blown SPA with React (and rum) for kasta.ua and I’m second-guessing myself regularly: should an ecommerce be an SPA? It probably should not, because of importance of SEO, and because it’s not an app, right?

But it has quite a bit of behavior, and the version we replaced with it was full of jQuery conflicts, which were extremely hard to handle - even to discover.

So here am I with a problem: should I build scalable (feature-wise) and maintainable solution or jump to an abomination of static html with jQuery? I don’t know if LiveView can help me, because some parts are surely 100% dynamic, but I guess I could do them with small separate React apps.

So yeah, I don’t know the answer, but I’m frustrated with initial load time. :slight_smile:

1 Like

I don’t know the answer either but in your case code-splitting should help a lot.

On that webpage chrome coverage reports that >60% of JS bytes are unused. I imagine that your page loads quite a bit faster if you cut out 1.5mb of JS.

88% of CSS is unused too so thats another area that could be improved (although that doesn’t impact load quite as much).

1 Like

I also had a look at Livewire but it seems to both:

  1. send all the state of the component and
  2. receive always all the html for a component

LiveView is not sending unnecessary data through and receiving very little thanks to it’s diffing.

Now I don’t know about the performance of it’s diffing (yet).

1 Like

Yeah, we’ve played with CSS, but it adds complexity and doesn’t mean much.

With modules I guess we should try them again, since last time the “core” was around 60% of all code, so we decided that it is useless. :slight_smile: The thing is, removing a bit of JS code will remove 1s or something, but it will not do wonders: it won’t be like a static site with immediate startup… While with Liveview that’s exactly the value proposition.

The SEO problem will still exist with LiveView, depending how much is rendered after it’s first run.

Well, ideally your server returns results of first render, which should be a complete working page. Which is what is needed by client and Google. :slight_smile:

1 Like

Well, I think the underlying problem here is using React SSR with hydration. That means that all the code you used to render the page initially is still downloaded and then a quite expensive hydration phase kicks in and basically generates it all over again. Combine that with a bunch of JS that still has to be downloaded/parsed even though it is not used makes for a slow page.

Liveview handles that better but you can do the same with plain CLJ(S) and just render things server side and have a little bit of code that attaches a couple event handlers when needed. Full blown hydration is rarely useful IMHO. The “live” aspect of Liveview with state on the server side seems unnecessary as well and reminds me of stateful EJBs which turned out to be not the best idea a long time ago.

Livewire does indeed send the state and HTML but that means the server remains stateless which is generally a good idea. Downloading a static snippet HTML is never gonna be a problem.

I think in Liveview they’re just using websockets because its erlang based and “well suited” for that style of development. I don’t really think thats actually a good way to develop for most sites and more of a technical gimmick than anything else.

But thats exactly the issues with all of this … There are so many options to chose one and its not one-size-fits-all. Making the “correct” technical choices in a project is really still too hard.

1 Like

Yes, there are many problems to solve and thus many possibilities to try (and many that can fail). State on front, sending it back getting modified state/html. State kept mainly on back pushed to front, then when requests/messages are being sent return diffs (eventually html).

My specific case is a B2B application and it doesn’t look like it will ever need to scale like a public website will need to (The specific case here of Alex @piranha ). But some scenarios could need that, one being people chatting to other people where BEAM could help and be more then a gimmick even though I wish I could do everything using Clojure(Script) and the JVM.

And thank you for your answers and your help.

Unless you need literally millions of open connections the JVM will do just fine. Even if you need millions the JVM will do just fine just building/tweaking it will get more complicated. Yes, BEAM will do slightly better with millions of connections but in return some other things suck more … hence … tradeoffs :stuck_out_tongue:

3 Likes

I’d be wary of anything strictly websocket based. We wrote an app at work using them for the reasons people have given, makes things simpler, better response times than polling, etc. Then we found out a lot of proxies don’t support websockets, so if your app is used through one, it’s dead in the water. You basically need to implement both ways, with and without websockets, and try to determine which one to use at runtime. Long polls get a bad rap, but they work about the same as websockets and are immune to proxies.

2 Likes

In the case of LiveView it seems it has a fallback mechanism ( https://youtu.be/8xJzHq8ru0M?t=3682 ) for websockets.

Agree with that, but right now we don’t have any good way to write an app without hydration, do we? Liveview seems attractive only in a context of React hydration. If there was a way to write SPA without that ‘initial’ hydration stuff, that would be the best way to deal with the situation.

What Svelte does is interesting, maybe I have to look further into this direction…

I still write plenty of server-rendered hiccup that just is “enhanced” by some minor JS via the old-school method of document.querySelectorAll and then modifying the nodes as needed. Typically that involves attaching onclick event listeners. That method has been valid for the past 20 years and still works just fine. After all the vast majority of web stuff still uses jQuery.

But this is all just minor stuff like adding some carousel or some “add to cart” kind of links. Nowhere near “App” status. Works fine though, can always render more complex stuff via react when needed. Just not doing it for the whole page most of the time.

Really the mistake I see that everyone takes the deep dive and uses React for everything when using it for one tiny component would be enough and just having the rest of the page as static HTML.

8 Likes

I don’t think Liveview will be competitive long term. It just feels like a huge waste of resources to not leverage the client and have everything handled by your server. It also sounds much more complicated, you need to manage a ton of open connections and keep track of multiple client states on the server side. Handling occasional disconnects, slow networks, bad actors, etc.

In the Erlang world, they might have no choice, short of using JavaScript or ClojureScript in combination with an Erlang/Elixir backend (or maybe Clojerl ;).

Like others mentioned as well, how do you define custom client behavior? Seems you’d be restricted to only what the framework exposes, which does feel kind of limiting.

There’s a lot of practical details I feel might be tricky. How do you A/B test? How do you deploy new releases? How do you roll back?

But hey, maybe I’ll eat my words eventually.

What I think ClojureScript lacks is non SPA options. I really would like to see embedded ClojureScript inside Clojure. So you could do more Rails like web apps, and introduce JS but using ClojureScript without having it be an SPA.

1 Like

I also find the idea of LiveView fascinating. One of the biggest question marks for me is still latency. The LiveView authors implemented this impressive animation example that runs with 60 fps. But in the demos the server ran on localhost.

We develop SaaS products and sell them globally. Our servers are located in Europe. I wonder how the experience for an user in Australia would be. Especially for very interactive elements like silders. It would be interesting to know, if LiveView would make it necessary to have servers across the globe to achieve an acceptable latency for all users.

@didibus @maxweber most web applications, even SPAs have to interact with the backend often, so I think you might wonder why every time? The answer here is that it’s not every time, you can still use js for some things on the frontend using hooks (not React ones): https://hexdocs.pm/phoenix/testing.html#content

For example, the markup for a controlled input for phone-number formatting could be written like this:

<input type="text" name="user[phone_number]" id="user-phone-number" phx-hook="PhoneNumber" />

Then a hook callback object could be defined and passed to the socket:

let Hooks = {}
Hooks.PhoneNumber = {
  mounted() {
    this.el.addEventListener("input", e => {
      let match = this.el.value.replace(/\D/g, "").match(/^(\d{3})(\d{3})(\d{4})$/)
      if(match) {
        this.el.value = `${match[1]}-${match[2]}-${match[3]}`
      }
    })
  }
}

let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, ...})
...

Note : when using phx-hook , a unique DOM ID must always be set.

Just copy pasted it from the documentation, might not be the most relevant, but sliders , inputs with onBlur etc might be handled this way.

1 Like