How do you make new app deployments with live view less jarring for users who are connected?

When your server goes down and the websocket connection is lost, the nprogress bar will show at the top of the page and when your server comes back up, LV will update the page based on its old vs new view of the world automatically.

This can be a very jarring experience for the user because now the page they were actively viewing changed under their feet and it’s out of their control. This is going to be especially disruptive if they are filling out a form, especially important forms like a checkout form to make a payment.

Also I remember Chris mentioning that if you make CSS changes, those changes won’t be diff’d automatically so now you run the risk of having a very broken looking page if you make CSS / JS changes – and the work around for that is to manually change your LV salt and then LV will do a hard refresh of the page.

This type of hard refresh is going to be the ultimate jarring experiencing because it’s like doing a full page reload for the user while they are using your site and it sounds like this would happen even if you changed CSS on a completely different page than what the user is on, since it’s all served from the same bundle.

I think there’s really 2 problems to solve here.

  1. How to make non-CSS changes less jarring for users
  2. How to make CSS / JS changes somehow automated without having to manually reset a salt value but also still have control on which pages get that hard refresh (so can it be done without the hard refresh) or think of something that just works in all scenarios without it being jarring

The way the web usually works without LV is, if someone is sitting on your page and you happen to deploy, they won’t know you just deployed and by the time they click the next link they will get the new version and life is good.

Even with Turbolinks, you can add data-turbolinks-track="reload" to your JS and CSS link tags and it automatically knows to only do a full page transition if it changes, because the file name has a unique ID already (the md5 digest) which means it can get tracked. But Turbolinks isn’t a websocket connection so this works seamlessly in practice in 100% of cases without any special deploy steps.

I don’t think we can apply that same rule to LV because always hard refreshing unrelated pages for a CSS change would be a pretty bad user experience. Imagine changing the CSS to update a contact form’s textarea and having that force refresh the checkout page for 25 connected users.

I think this is a real problem to discuss because a lot of folks deploy to 1 server because it works wonderfully in practice and with a non-LV app it’s very easy to do this without most user connections realizing your server even went down for a few seconds to restart. Even with Turbolinks.

Having to set up a Kubernetes cluster with a load balanced cluster of servers that have flawless connection draining just to deploy the most simple but realistic production ready app is going to raise the bar too high in my opinion.

5 Likes

Hi @nickjanetakis,

Per one of the more recent updates to live view (0.10 I think), forms are automatically recovered based on the form state in the browser. We’ve used this property to store information both visibly in the URL, visibly in active forms, and invisibly in hidden forms to ensure that pages can recover their state entirely on reconnect.

This doesn’t solve your CSS issues though. I too would like to see some solution to prompt the page to load fresh CSS / JS.

2 Likes

A straight-forward solution is to pass a version on “connect_params”. You will compare this version with the one on the server, if they are different, you issue a redirect to the current page. You can bump the version on every new deployment. You can even use the app version itself if you prefer.

1 Like

That definitely works for updating CSS / JS, although it probably kills active forms. Maybe one could phx-hook the style / js links?

1 Like

Right, but wouldn’t they still get that jarring experience of a full page reload if the CSS changes if you decided to bump the LV saltl? Also what happens if they are actively typing into the field and the LV updates? Even with local form state, wouldn’t the text field lose focus? This could lead to all sorts of very bad user experiences.

Look at this discourse forum as an example. They do a VERY good job at minimizing jarring experiences. Instead of just pre-prending new items to the list of threads or appending new replies, it shows you a single message about how there’s new stuff and then the end user who is browsing has the option to opt-into seeing it on their own terms by clicking the link.

The browsing user never loses control of when the primary content of the page changes, regardless of the state of the application server side.

2 Likes

That has not been my experience no. The DOM patching code goes to great lengths to avoid disrupting form entry. focus in particular is fun because it’s both an HTML attribute as well as a DOM property. live_view.js is smart enough to know the difference, and will avoid clobbering the focus property on the appropriate DOM nodes. https://stackoverflow.com/questions/6003819/what-is-the-difference-between-properties-and-attributes-in-html

1 Like

Updating the salt will have implications in whatever application. If the ElixirForum folks update their salts or secret key bases, they will force everyone to logout. For example, if they did it now, you maybe could still write your answer, but as soon as you submit it, it would most likely redirect you to the login page. So do not change our salts/secret key bases unless you strongly have a reason to. This applies to Phoenix, Rails, Django, etc.

Regarding the focus, this is handled by LiveView too.

Even the Discourse experience that you mentioned can be implemented with LiveView easily. Instead of pushing new things to the page, you can show a message that says there are new entries and push the data only when the user clicks on said message.

Of everything raised so far, the JS/CSS changes on new deployments is the only scenario not currently tackled by LiveView.

2 Likes

Ah, yeah I was only going by what @chrismccord said in Slack the other day. He said bumping the live view salt would be one way to force a full page reload when your CSS / JS changes.

Yeah for sure, but the concern I have is more about general app deployments will cause all currently connected users to have their page diff’d under their feet.

Turbolinks fully solved this by using the md5 digest of the CSC / JS bundle as a uniquely trackable ID. No need to adjust app versions or remember to do something special on deployment.

But since Turbolinks isn’t a websocket connection, the end user who is browsing has full control when this refresh happens – which in the case of Turbolinks is when a user decides to transition from page A to B by clicking a link or submitting a form (the next submission will be a full reload).

Do you have any ideas on how this could work with websockets, which wouldn’t involve forcefully reloading the current page when the CSS / JS change happened, but still somehow not break the layout of a page if the DOM changes in the future that was associated to a CSS / JS change?

The main blocker to solve this in LiveView compared to Turbolinks is that IIRC Turbolinks sends the whole page on every navigation. So you are always getting JS/CSS files which you can compare and reload. For LiveView, we would need to either load the whole page on reconnects or use a versioning mechanism.

2 Likes

Here’s a snippet of how it works from the docs:

When you follow a link, Turbolinks automatically fetches the page, swaps in its <body> , and merges its <head> , all without incurring the cost of a full page load.

In practice, it means you don’t need to re-parse the CSS / JS on every page transition.

But they have the property I mentioned in the original post to force a full page transition (including the <head>) if the md5 digest of the bundle changes. But that works beautifully for Turbolinks because it’s not a web socket connection that forces that reload while the user is idling on the same page they are on. It only happens when the user explicitly transitions pages – which means the user gets a full CTRL+ r style page transition speed instead of the super speedy <body> swap.

If you want to see how Turbolinks works in practice, I’m using it on my podcast site at https://runninginproduction.com/. If you browse the site with JS enabled, it’s only 1 XHR request to transition from any of the links. If you disable JS, it will load everything on every request. The neat thing is it didn’t require changing anything on my site. All I added was 1 line of JS. The markup is the same and it falls back to working without JS seamlessly and also works with crawlers.

1 Like

I was part of the Rails Team when we first introduced Turbolinks, so I am mostly aware of how it works (except for when my memory fails me). :slight_smile:

8 Likes

Ah ok. I wasn’t sure if you were still around then. Turbolinks has changed a decent amount over the years.

In any case, do you think this issue with LV will be solvable in a way that won’t require manually having to do something on deploy time depending on if your assets change, and also not force a full refresh on all connected users who are just idling on a page?

Maybe we can use the same strategy as Turbolinks by tracking the digest of the CSS / JS and if it changes, live_redirect, live_patch and events that change the DOM will turn into full page transitions? And for current page views without CSS / JS changes, things would work nicely as of today. So the last problem to solve would be people idling on a page with CSS / JS changes?

It’s a tricky one. I know Webpack’s watcher can do something where it can swap your old bundle for a new bundle without having to reload your page. It’s like the CSS changes take immediate effect by some form of diffing. Is that something we can do too?

This sound a lot like the complexities involved in making hot code updates for release work. Trying to stitch together grown state with some update logic to transform it to a new state. Just instead of only in a process you’re doing the same on the client.

Unfortunately it’s the common case here tho. This issue will affect everyone deploying to production.

Having a CTRL+r style full page refresh enforced by the server for someone idling on a page whenever any bits of CSS / JS change is a very unfriendly user experience.

Imagine you’re deploying a few times a day and making minor CSS / structural updates to various pages. Every single one of those deploys would cause every connected user to have their entire page / world pulled out from under them without them taking any action to cause it.

That would be catastrophic for someone on a checkout page ready to buy an item, but it’s very disruptive for anything really.

1 Like

True, I fully understand your point. But if you maintain a stateful system on the server you’re in that situation and there’s simply more to do than when not having that stateful system. Same would happen if e.g. you’re running game processes on your servers. You wouldn’t want to just stop those with each deploy and start fresh. You’ll need to keep some things around even for new deploys to let old games finish. This could be doing hot code updates, or persisting stuff to a database.

For liveview this might mean having multiple versions of css/js around and storing an version identifier in the session data. Then only new connections could be served the new assets, while old ones are not disrupted.

To be clear, you have successfully made your point: https://github.com/phoenixframework/phoenix_live_view/issues/834

1 Like

What does “refresh instruction” mean in this case? That sounds like it would do the most invasive thing and CTRL+r refresh any connected users if the CSS / JS changes (even if they are just idling).

Without keeping multiple versions of your assets around there’s no other way. How would you serve your clients old code without this code being in your new deploy?

Edit: Keep in mind that elixir/liveview doesn’t really know if “just the js/css” changed. You might have updated the liveview and the js and they need the changes from each other.

If it were up to me, I would solve it like this:

No CSS / JS changes

Let LV do its thing and diff everything as usual.

CSS / JS changes

If a user takes a link clicking DOM changing action (live_redirect, live_patch), instead of diffing the DOM, it would do a full page transition (similar to a non-LV app without Turbolinks). This is exactly what Turbolinks does btw and it works very well.

But now we have a problem because other DOM changing things can occur. Maybe a user does a phx-click on a button to update a paragraph somewhere. But it could also be a phx-change on a form. I don’t know what to do in those cases.

If a user is idling on the page doing nothing, perhaps reading a blog post, or reading some testimonials on a checkout page before buying something. Then do nothing by default but add a feature to LV to allow us developers to perform an action when this event happens.

So the event could be “if digest changed” or something, and now we can choose what to do. We can choose to do a full refresh, or maybe a full refresh with a black listed set of LV modules that won’t get refreshed, or maybe send a toast notification suggesting the user to reload their browser with an OK button to do the refresh and a cancel button to dismiss it.

Realistically I have no idea how the last one could be implemented. I’m hoping someone smarter than me can figure something out.

Edit: I don’t want to sound like I’m overreacting but honestly if the only option were a forced refresh for idling users and nothing else I wouldn’t use LV. That breaks how the web works. Imagine developing a Shopify site and having thousands of people on a checkout page get refreshed because a developer updated 1 line of CSS in the terms and service page.

1 Like

Bumped into this thread today while researching how to make new feature deployments in Liveview as seamless as possible for connected users.

I noticed that the LV team added static_changed?/1 a few days after the last message here. Details here: https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#static_changed?/1

Would it be fair to say that this addresses most/all of the above mentioned concerns (which I share)?

Any experience working with this approach?

Thank you.