I’ve been experimenting with all kinds of things and ideas for making a Rust-powered web frontend system that is designed to be completely functional with server-side rendering, with scripting on the frontend being deliberately optional, bringing things back closer to the old-style non-isomorphic server-side rendering, doing things like wrapping all the buttons in forms so that they will, in the absence of local scripting to intercept it and do it on the client side in a potentially more efficient way or with transitions or such, leave it to the server to render the result as it would have been after that button was clicked.
In conjunction with this, I’ve long been thinking about a new style of isomorphic rendering: where instead of running on the server and in the DOM, you run the server code on the server and in a service worker on the client side. (Aside: Svelte has been a major inspiration in the last few months; learn from it that SSR doesn’t need to mean VDOM: if you’re a compiler you can take different approaches.) Cloudflare Workers, when it came along, brought clarity to what I had been thinking at about that time, that it might work to have a pure API backend, and an HTML renderer that speaks to that API and can run either on the server or locally in a service worker on the client: truly use the same interface for both. (To clarify: this approach does not preclude additional client-side scripting for interactivity; but it would lend itself to a light hand on client-side interactivity.)
Now to the relevant point here: a couple of weeks ago I started toying with the idea of doing just about all of the client-side scripting (I mean the stuff for interactivity without full page loads) in workers, leaving the work done on the UI thread being purely applying UI changes that were even calculated in a worker, and event dispatch. This might be able to be slotted into the service worker in some way, or it might need to be another worker.
The essence of what I have in mind is that rendering in the worker would, instead of applying changes to the DOM, emit a byte code (I’ve been looking a very little into Glimmer.js’s), which can then be fairly efficiently passed back to the UI thread through one ArrayBuffer or SharedArrayBuffer, to be applied by a small unit of code (probably JS rather than Rust) to the DOM. In the last few days I’ve been playing with events, and adding the listeners on the document root and doing dispatch manually, through components more than through elements, in a way that lets the framework deal with hierarchical ownership (very Rusty, allowing you to skip GC/RC types) rather than something closer to the ECS style (what is mostly done for UI things in Rust), and I think the approach has promise. (Apart from the hierarchical ownership aspect, this is basically what our framework Overture that we use for FastMail does—and I should clarify at this point that these experiments of mine are personal and nothing whatsoever to do with FastMail, where we have no workers at all, like almost all sites—though I wrote one this very day that might be deployed in the coming week).
Events would be serialised on the UI thread and passed through to the worker. All events would thus need to be passive (i.e. no preventDefault()); though there will doubtless need to be some alternative channel for events that need to preventDefault, most notably clicking on links that should route instead, and form submit; I’m not sure how that will work.
Some parts of this I’ve written code for, to experiment with ideas, but especially the parts involved with workers have almost entirely been thought experiments. I’ve been thinking about the approaches SwiftUI takes too. Lots of interesting stuff to learn from it as well.
I’ve written all of this purely for thinking about. Maybe others will find it interesting. (I shan’t be able to respond to anyone that replies for the best part of a day.)
> there will doubtless need to be some alternative channel for events that need to preventDefault, most notably clicking on links that should route instead, and form submit; I’m not sure how that will work.
Naively I would expect that to be part of the rendered DOM("<a prevent-default='click'>"), or otherwise in a declarative datastructure available to the UI thread. Are there many events that need conditional preventDefault, such that the JS handling code would grow nontrivial?
I thought about it a bit longer after I wrote it and decided that what you need is some sort of pure function that takes the event target DOM element only, and decides whether the default should be prevented. For example, in FastMail we use a function to intercept links that will essentially check whether the href is routable (fiddlier than you might guess, unfortunately) and not target=_blank; that could be reduced to such a pure function, though various routes would now need to be specified in two places. Either that, or just always pipe generated <a href> elements through some function in the worker that annotates them—I like your idea, thanks for the thoughts!
Either way, I can’t think of anything where you need fully conditional preventDefault. Definitely something closer to declarative than imperative is good for these sorts of things.
In conjunction with this, I’ve long been thinking about a new style of isomorphic rendering: where instead of running on the server and in the DOM, you run the server code on the server and in a service worker on the client side. (Aside: Svelte has been a major inspiration in the last few months; learn from it that SSR doesn’t need to mean VDOM: if you’re a compiler you can take different approaches.) Cloudflare Workers, when it came along, brought clarity to what I had been thinking at about that time, that it might work to have a pure API backend, and an HTML renderer that speaks to that API and can run either on the server or locally in a service worker on the client: truly use the same interface for both. (To clarify: this approach does not preclude additional client-side scripting for interactivity; but it would lend itself to a light hand on client-side interactivity.)
Now to the relevant point here: a couple of weeks ago I started toying with the idea of doing just about all of the client-side scripting (I mean the stuff for interactivity without full page loads) in workers, leaving the work done on the UI thread being purely applying UI changes that were even calculated in a worker, and event dispatch. This might be able to be slotted into the service worker in some way, or it might need to be another worker.
The essence of what I have in mind is that rendering in the worker would, instead of applying changes to the DOM, emit a byte code (I’ve been looking a very little into Glimmer.js’s), which can then be fairly efficiently passed back to the UI thread through one ArrayBuffer or SharedArrayBuffer, to be applied by a small unit of code (probably JS rather than Rust) to the DOM. In the last few days I’ve been playing with events, and adding the listeners on the document root and doing dispatch manually, through components more than through elements, in a way that lets the framework deal with hierarchical ownership (very Rusty, allowing you to skip GC/RC types) rather than something closer to the ECS style (what is mostly done for UI things in Rust), and I think the approach has promise. (Apart from the hierarchical ownership aspect, this is basically what our framework Overture that we use for FastMail does—and I should clarify at this point that these experiments of mine are personal and nothing whatsoever to do with FastMail, where we have no workers at all, like almost all sites—though I wrote one this very day that might be deployed in the coming week).
Events would be serialised on the UI thread and passed through to the worker. All events would thus need to be passive (i.e. no preventDefault()); though there will doubtless need to be some alternative channel for events that need to preventDefault, most notably clicking on links that should route instead, and form submit; I’m not sure how that will work.
Some parts of this I’ve written code for, to experiment with ideas, but especially the parts involved with workers have almost entirely been thought experiments. I’ve been thinking about the approaches SwiftUI takes too. Lots of interesting stuff to learn from it as well.
I’ve written all of this purely for thinking about. Maybe others will find it interesting. (I shan’t be able to respond to anyone that replies for the best part of a day.)