Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Render Dom-tree to string (SSR) #29

Open
njam opened this issue Feb 5, 2020 · 13 comments
Open

Render Dom-tree to string (SSR) #29

njam opened this issue Feb 5, 2020 · 13 comments

Comments

@njam
Copy link

njam commented Feb 5, 2020

I was wondering how one could render a Dom-tree to an HTML string for server-side-rendering (SSR).

Conceptually I see three options:

  1. Render the application in a headless browser with wasm-bindgen-test. This should be quite easy to set up, but is slow and brittle. (example code)
  2. Adding an intermediary runtime layer in rust-dominator, which wraps dom nodes and operations on dom nodes. So Dom would not store a web_sys::Node but some internal struct, which could either contain a web_sys-node or some internal node representation.
  3. Allow to swap out web_sys at compile-time with a virtual dom library that has a compatible API to web_sys, and provides all the functionality needed by rust-dominator.

What do you think about this topic in general? Are there other ways?

@Pauan
Copy link
Owner

Pauan commented Mar 26, 2020

I've thought a lot about this, it's rather tricky because of the need to rehydrate the DOM.

The server rendering part is easy, it can just render to a string, or use jsdom. The hard part is the client.

The client needs to be able to find the existing DOM nodes and then attach events/signals to them. It's hard to do this correctly and efficiently.

@njam
Copy link
Author

njam commented Mar 26, 2020

The server rendering part is easy, it can just render to a string, or use jsdom.

Good idea to run the wasm program in NodeJS instead of a real browser, I guess it should be faster and more robust!

The client needs to be able to find the existing DOM nodes and then attach events/signals to them. It's hard to do this correctly and efficiently.

Agree that would be hard. I was thinking to create the full dom tree again on the client-side, and then just replace the static rendering with it.

@Pauan
Copy link
Owner

Pauan commented Mar 26, 2020

I was thinking to create the full dom tree again on the client-side, and then just replace the static rendering with it.

The problem with that approach is that then there's not much benefit to the SSR: why spend all the time rendering the HTML, sending those HTML bytes to the browser, and having the browser render it if you're just going to throw it away? Search engines understand dynamic websites, so SEO isn't as much of a reason anymore.

SSR adds an incredible amount of extra complexity (both on the server and client), so there would have to be a significant benefit to it. Being able to "pre-render" and rehydrate the DOM might be enough benefit to justify it.

@njam
Copy link
Author

njam commented Mar 26, 2020

The main reason for me to use SSR would be to have a fast "first meaningful paint" on the client side.

I did some benchmarks with a demo application on Chrome/Android.
Rendering the website with rust-dominator takes 250ms until the first meaningful paint (50ms spent in wasm). Rendering the same content as a static page takes 180ms. This is without network throttling.
So while SSR would reduce time to "first meaningful paint" a bit, I agree that it's probably not worth doing it, because the browser would need to render the whole page twice.

Loading wasm-rendered page (250ms):

Loading static HTML (180ms):

So yeah I think you're right, SSR would only really make sense if the page wouldn't need to be rendered completely again..

@kylone
Copy link

kylone commented May 10, 2020

My impression for the main utility of SSR is for SEO benefits over raw performance benefits, for (only) the initial page load.

@Pauan
Copy link
Owner

Pauan commented May 10, 2020

@kylone My understanding is that search engines have supported dynamic pages (created by JS) for decades. So I'd like to see some evidence that static pages have better SEO than dynamic pages.

@Rizary
Copy link

Rizary commented May 19, 2020

In FRP context, I think prerender is needed, although I'm not sure for wasm. For example, in reflex-dom (haskell frp library), there is prerender functionality that might be similar.

there is also documentation about what is prerendering.

Maybe reflex is slightly different than dominator, but iirc, I use prerender when there is block of HTML that need to change based on certain event but we still need that block to be available while waiting for the event happened.

@Pauan
Copy link
Owner

Pauan commented Jun 4, 2020

@Rizary That sounds like pretty standard SSR. Unfortunately we can't use monads, because of our requirement to be zero-cost. So instead the solution will probably involve cfg.

I use prerender when there is block of HTML that need to change based on certain event but we still need that block to be available while waiting for the event happened.

I don't see what that has to do with prerendering, you can do that with regular signals:

html!("div", {
    .children_signal_vec(from_future(some_event()).map(|value| {
        match value {
            Some(value) => vec![render_app()],
            None => vec![render_loading_screen()],
        }
    }).to_signal_vec())
})

This will wait for some_event() to finish. While it is waiting it will display a loading screen, and after it is finished it will then switch it to the app.

@Rizary
Copy link

Rizary commented Jun 4, 2020

@Pauan thank you for the explanation, It seems that I have misunderstanding here. In reflex, the prerender_ fuction is exactly what you explain above. I think I need to explore dominator more.

@njam
Copy link
Author

njam commented Jun 4, 2020

Here's a demo for rendering a rust-dominator app in NodeJS using jsdom:
https://gitlab.com/njam/rust-dominator-ssr-jsdom

How it works

  1. The src/ folder contains a simple WebAssembly application that displays a counter which can be increased by pressing a button.
  2. Using rollup-plugin-rust a browser bundle is created which exposes a JS function createApp() to instantiate and return the WebAssembly-application (see ssr/rollup.config.js).
  3. A NodeJS script sets up a jsdom environment, loads the browser bundle, creates the application (by calling createApp()), and finally serializes the DOM tree and prints the resulting HTML (see ssr/render.js).

@matthewharwood
Copy link

@Pauan

The client needs to be able to find the existing DOM nodes and then attach events/signals to them. It's hard to do this correctly and efficiently.

Hydration can be done pretty simply by wrapping a component in a browser web component and using connectedCallback to register and hydrate. But you still need to write a good hydration fn

@Boscop
Copy link

Boscop commented Dec 1, 2022

Yew supports SSR now, maybe a similar approach would make sense for dominator?

@Pauan
Copy link
Owner

Pauan commented Dec 1, 2022

@Boscop That's just standard SSR, so no I don't think it will work for dominator.

There are a lot of things in dominator that make SSR difficult, in particular signals.

A typical dominator app uses a lot of signals, but how will that work with SSR? There are two possibilities:

  1. Signals are client-side only and don't run on the server at all.
  2. The server runs the signals, and the SSR will return an impl Signal<Item = String> which gives a new string whenever any of the component signals change on the server.

Both of these options have a lot of problems with them.

Hydration is also a difficult topic, because signals can dynamically change the layout of the DOM. So what if you have some dominator code like this...

html!("div", {
    .child_signal(some_signal().map(|x| {
        if x {
            Some(html!("span", {}))
        } else {
            None
        }
    }))
})

On the server x is true, so it sends <div><span></span></div> to the client. But on the client x is false, so it would need to remove the <span>. And vice versa, if x is false on the server but true on the client, then it has to insert a new <span>.

Things get tricky very fast whenever signals are involved, because the signals might not match between client and server. And the signals would have to somehow be synchronized between the client and server.

And if we take the easy approach of just not running signals on the server, then that makes SSR completely pointless, because most dominator apps are going to use child_signal or children_signal_vec, and those won't run on the server.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants