Laminar
Laminar copied to clipboard
Pre-rendering Laminar pages [SSR]
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:
- Render page in headless Chrome and save its html
- Save page state in separate json file
- Push both html and json files to CDN
- We now have static pages that are fast and search engine friendly
- Once browser loads static html file, it will evaluate javascript and Laminar will replace static html with interactive version
- 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?
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 whateverobserver
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.
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!
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.
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.
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.