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

Pre-rendering Laminar pages [SSR] #60

Open
uosis opened this issue Aug 22, 2020 · 5 comments
Open

Pre-rendering Laminar pages [SSR] #60

uosis opened this issue Aug 22, 2020 · 5 comments
Labels
discussion hard problem needs sponsor Pretty sure I can't do this unless someone wants to sponsor this work.

Comments

@uosis
Copy link

uosis commented Aug 22, 2020

I am looking for options to pre-render Laminar pages, and would like to see if there is any prior art or best practices.

My current hypothetical plan is as follows:

  1. Render page in headless Chrome and save its html
  2. Save page state in separate json file
  3. Push both html and json files to CDN
  4. We now have static pages that are fast and search engine friendly
  5. Once browser loads static html file, it will evaluate javascript and Laminar will replace static html with interactive version
  6. Any further navigation can be done client side using json state files from CDN

I did some preliminary testing of this scheme, and it appears to be working. My main concern is the lack of hydration support in Laminar, which necessitates double render on first hit. Are there any plans to add hydration support to Laminar? It seems hard - this seems like one of the few areas where DOM diffing has advantage. I am also not sure how much double render matters in practice - hydrating isn't exactly cheap either.

Any thoughts on what is the best way to do this?

@raquo
Copy link
Owner

raquo commented Aug 22, 2020

There was some gitter discussion about this, linked in #46. Basically I agree, headless chrome is the way to go.

And you're right, hydration is much harder in Laminar than React. Architecturally, for hydration React can just apply the same diffing logic as before, it just needs to read previous state from the real DOM rather than from the previous virtual element. In Laminar on the other hand, the problem is that modifiers can contain arbitrary executable instructions, not just key-value pairs, and modifiers don't have any built-in reconciliation logic.

  • Setters like attr := value are idempotent, so it should be safe to call them on hydration
  • Event prop binders like onClick --> observer don't affect the HTML output directly so they can be executed on hydration too (the safety of whatever observer does needs to be evaluated separately)
  • Reactive inserters like child.text <-- $text would be problematic as they reserve a spot in the DOM, and would need to be able to pick it up from HTML somehow.
  • Custom modifiers with user-defined executable logic would be a problem too, but I can think of some solutions, allowing users to declare whether their modifier should run on hydration or something like that.
  • Elements themselves would be the most problematic. As soon as you call div() we create the corresponding HTML element. There's no way around it, being able to refer to it with .ref is core to Laminar's design. However, when div() is initialized it doesn't yet know where it will be mounted, so there is no way to hijack this initialization to fetch an existing HTML element from the DOM instead of creating a new one.

So, bottom line, I don't think true virtual DOM style hydration will be possible in Laminar.

However, as you said, we can achieve much of the same by pre-rendering the page and on page load simply replacing the contents of the app container with the live Laminar app. Async stuff like ajax requests aside, the initial rendering should happen synchronously as soon as the DOM becomes available, so at first glance I think performance would be the only concern. Other potential concerns would be – what if the user starts to interact with the website before we load the interactive Laminar app, like, what if they focus on an element and start typing into it. But I think the browser should be blocked while loading the live Laminar app, as it always is while executing synchronous Javascript.

In terms of performance, the browser will do the unnecessary work of parsing a bunch of HTML and instantiating all those elements. But the elements would be inert, with no subscriptions defined on them, so it should be slightly less work than initializing the real Laminar app. For the vast majority of applications I don't think the difference will be noticeable – even if it's close to 2x increase, the base time is usually so small. Much of real life loading time is spent parsing JS, loading resources, etc. rather than initializing the DOM.

If you only care about this for SEO, you can further improve this by not serving the JS to google bot and by not serving the HTML to real users. I assume google won't punish for such antics but I haven't checked.

What kind of data are you planning to save in json? Stuff like ajax responses? That would be another way to improve performance where the data is not user-specific.

Overall I think this is a legit approach, I haven't done it myself yet but planning to try it out eventually. If any of what you've done is shareable as a gist or a blog post, quite a few people would appreciate I think.

@uosis
Copy link
Author

uosis commented Aug 23, 2020

Other potential concerns would be – what if the user starts to interact with the website before we load the interactive Laminar app, like, what if they focus on an element and start typing into it.

Yeah realistically the pages would have to be parameterized to hide/disable inputs during pre-render.

For the vast majority of applications I don't think the difference will be noticeable – even if it's close to 2x increase, the base time is usually so small.

I mostly agree. I am just somewhat worried about big pages (e.g. >5k elements) on mobile devices. Double render might have some impact in that case. I am still trying to verify this.

What kind of data are you planning to save in json? Stuff like ajax responses? That would be another way to improve performance where the data is not user-specific.

Yeah, our use case is similar to a classic e-commerce site example where you have lots of public pages with same layout but different content. So client side navigation (after the first load) would just load the content from those json files rather than loading prerendered html on each navigation, or querying the backend.

I am basically trying to accomplish what Next.js does (that's an interesting read about current state of the art of serving things fast btw) with their static generation / hydration.

If any of what you've done is shareable as a gist or a blog post, quite a few people would appreciate I think.

At this point I am just running puppeteer manually to dump html and overwriting stub page with that, but if I get something more advanced going, I will definitely share.

Thanks for your inputs!

@raquo
Copy link
Owner

raquo commented Aug 24, 2020

Sounds about right. Good article about Next.js, thanks.

By the way, make sure to try your scala.js app with es2015 output disabled on mobile safari. In at least a couple ScalaJS-React apps es2015 output is causing a massive slowdown in parsing of the application bundle, with the browser taking several seconds to parse a 1mb bundle. I looked and couldn't find any evidence that Mobile Safari is in general slow to parse es2015 so it must be something specific to the Scala.js es2015 output that it doesn't like.

@raquo raquo changed the title Pre-rendering Laminar pages Pre-rendering Laminar pages [SSR] Nov 28, 2020
@yzia2000
Copy link

Hey. Was wondering graalvm can also help in the ssr runtime debate. Seeing some articles with people evaluating the js code using graalvms polyglot features with relatively good performance to node.

@raquo
Copy link
Owner

raquo commented Jun 23, 2022

Maybe, if you can make graalvm run jsdom. Running it on a node.js server seems simpler and a more travelled path (not for Laminar but in general), but I guess both could work, just a matter of which runtime you're more familiar with.

@raquo raquo added the needs sponsor Pretty sure I can't do this unless someone wants to sponsor this work. label Jan 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussion hard problem needs sponsor Pretty sure I can't do this unless someone wants to sponsor this work.
Projects
None yet
Development

No branches or pull requests

3 participants