solidjs / solid

A declarative, efficient, and flexible JavaScript library for building user interfaces.
https://solidjs.com
MIT License
32.02k stars 914 forks source link

SSR Specification #109

Closed ryansolid closed 3 years ago

ryansolid commented 4 years ago

I've had many people ask me about SSR. In fact, it's probably the most requested feature. So far I've made it mechanically possible to do rehydration in the client for non-asynchronous rendered cases. Using JSDOM or similar we can run Solid on the server to generate fake DOM nodes that can be stringified.

I don't know enough about the topic of SSR and Isomorphic Libraries to know beyond that how they are expected to work. Nor am I the best suited to spearhead this as I have no reason, outside of requests, to look into this. It's just not an area where I do work. I've been working hard to improve clientside performance to a point that TTI speed and Service worker caching makes the whole thing mute for high-performance Web Apps and PWAs. But I know there are other use cases out there, and that the research especially being done with React and Next.js is impressive. But I'm much more incentivized to show far we can optimize SPAs.

Currently, I am not really clear even what the essential building blocks are. Routing obviously comes to mind. But I'm unclear on isomorphic patterns there. How are lifecycles and asynchronous requests handled? Lazy loaded components? How do things differ between static site generation and otherwise? What sort of tooling is useful? If anyone has experience in these areas it would be invaluable.

I realize people are looking for libraries and frameworks for opinions here, but I haven't formed them. I will work with anyone interested to make Solid be able to handle the mechanicals. Unless others help take it on though I don't see SSR progressing in the next 6 months. I have a lot of other work to get done to make Solid better.

What can I do to Help?

Join the conversation. I'm free to chat at length about this on gitter. Post in this thread what you'd like to see. Or any Solid specific insights you have from your experience with other SSR libraries. If you think you might have answers to any of my questions above please shed some light on the issues.

Thank you.

schtauffen commented 4 years ago

I would love to donate some time towards this feature of solid. I have an approach for bootstrapping onto SSR html in mind that we might be able to use but have to really dig in deep to find if it is plausible.

ryansolid commented 4 years ago

@schtauffen That sounds great. I imagine some pieces will require me to adjust the approach Solid currently takes a bit. If you have any questions or want to bounce an ideas off me could be beneficial. There are some particulars that come with the library that differ from most similar looking ones.

schtauffen commented 4 years ago

As I've began playing around with solid SSR the first thing that comes to mind is ease of use. Right now it requires separate config values for hydrate/ssr when ideally it could just be separate entries (hydrate and renderSSR/renderToString). Haven't looked into what it would take to change this. Not the end of the world if this requirement has to remain.

Second, when I attempt to run the ssr code, I hit "window is not defined" and other undefined errors from solid-js. Ideally window and the DOM and etc would be variable within the codebase and renderSSR could use a stand-in like jsdom for the browser functions. Worst case this could just be a library that users have to call at the top of their entry file (ie: require('solid-js/ssr')()).

To get it functioning I had to provide the following script at the top of the server bundle:

(function() {
  const { JSDOM } = require('jsdom')
  const { window } = new JSDOM(``);
  global.window = window;
  global.document = window.document;
  global.Node = window.Node;
  global.requestAnimationFrame = cb => setTimeout(cb, 0);
  global.cancelAnimationFrame = frame => clearTimeout(frame);
})();

I'm unsure if there are other variables down other code paths that my code just didn't happen to hit.

Lastly, the ergonomics of using renderSSR is a bit wonky. We are just trying to render to string for rendering on the server, so having to pass in a dom node and having it pass back a dispose function isn't the most useful.

I would implement renderToString which could utilize renderSSR under the hood which just takes the component to render and returns the resulting string.

import { renderSSR } from 'solid-js/dom'
import { App } from '../client/app'

export const renderToString = code => {
  const root = document.createElement('div');
  renderSSR(code, root);
  return root.innerHTML;
}

console.log(renderToString(App));

My next step is going to be implementing the Preact test suite for hydrate/ssr-rendering and see what functionality solid-js currently has by comparison.

ryansolid commented 4 years ago

Thanks, that's a lot of good stuff::

  1. Are you suggesting just importing a separate Babel Plugin instead of messing with the babel-plugin-jsx-dom-expressions.. How about a preset? I have babel-preset-solid, I could open up the configuration there, or make a new preset. I think that makes more sense. The babel plugin isn't unique to Solid and I use it in my other libraries.

  2. Yeah I took this for granted and I shouldn't have. The few tests I had done were in jest environment which had globally been configured. A few options here. We could package something that gets imported. Main Solid shouldn't have those refs. I did have requestAnimationFrame but I've since removed it in the latest version of my scheduler module. I'm more careful doing detection and fallbacks now on the new scheduler. That being said solid-js/dom definitely uses document a ton. I guess the question is whether we should just choose one like JSDOM and go with that. If so I suppose I could have the babel plugin add import statement to patch these globals. I guess the other option is to export a configuration method that writes globals used by the library. Ok this seems like something I could do. Do you have a preference on which way to go on this?

  3. Ok If renderSSR isn't right I'm fine changing it to whatever makes sense. I guess my question is should this method return the whole HTML document string or just precisely those elements? I just looked I guess just the elements. If so I will just get rid of renderSSR and use the renderToString which seems to be the way that React/Preact name it. It is important to understand server rendered strings are a bit different than how I'd render in the browser since certain information is present only at creation time when I have only DOM nodes to look at. So I add additional markers (comment nodes) to break up text and dynamic blocks. Which is also why Hydrate is different. There was just no straightforward way to make these processes as performant as normal Solid as they need more runtime overhead. I suppose this detail isn't that important mind you.

ryansolid commented 4 years ago

Ok having spent some time with it some more:

  1. Yeah given that this is compile-time I don't see how we avoid it. I checked again and I already setup the preset to take the argument. In all cases it still means another bundler config whether different setting, plugin, preset etc.
  2. I've done some work to better shield feature detection on feature use. That being said document, and Node are still required for renderToString. Not sure what is the best way to get that in. It's awkward as a peerDependency on any library along the chain except an SSR specific one. At which point should we care much about replacing node globals versus internal module aliasing. Not sure it's interesting since VDOM libraries generally don't have this issue. Now that I understand the synchronous nature of this sort of rendering I do see a lot of simplifications that can be made here. It's also possible this DOM approach can be avoided in the future if I ignore Web Components. There are some other trickinesses here like attribute/prop handling. Like say classList uses DOM API's. So if I made an SSR specific Runtime and compiler we could probably get there. But let's not count on it for now.
  3. This was easy to do. I've gone over to renderToString. I also expose an isSynchronous which lets you know that this is a synchronous only render, so it should be possible to support specific SSR only code paths. I intend to release this in v0.15.5 so watch for it.
schtauffen commented 4 years ago
  1. Best case it could be handled runtime and be a flag that renderToString sets within the library for isSSR, and the hydrate function calls would set one for isHydrating. I'm not sure what sort of performance impact this would have adding extra if checks but can see how this would be undesirable. I think having the babel option is a fine compromise.

  2. Potentially optionalDependencies instead of peer? Async: While Preact doesn't support async rendering, React does. Personally I haven't used async SSR at all, but I could see it being desirable if you wanted components to own their own fetches and data instead of a single store approach. It would be similar except everything has to be "hooked up" until it can resolve the current html string when either

    1. All paths have rendered (could use suspense semantics as the driver for knowing when a component is done?)
    2. A timeout occurs
  3. Hooray!

I hope to spend more time with solid in the coming weeks in order to better understand it and be of more use to the project.

ryansolid commented 4 years ago
  1. I actually generate different code. The normal version can just do firstChild nextSibling to walk a predetermined path to set up bindings. For SSR I use similar code but I generate additional marker nodes. A big part of Solids performance is it writes optimal DOM code that is just run. I basically flatten iteration (unwind loops so to speak). Once I'm calling methods and checking conditions might as well be using a VDOM. Not that I'm against that in certain scenarios the core runtime experience shouldn't be affected.
  2. Yeah optionalDependencies don't work the way one would expect. To be fair I have this issue with my hyperscript and tagged template literal versions of the library. I could just make a solid-js/ssr without explicitly putting dependencies. It could also just be a set of utilities. Like provide a method to patch Node with whatever DOM implementation you want.Or it could just be blueprint for an option or 2.

More interesting is the renderToString as an async function. You are correct. I could use Suspense in combination of checking after next microtask to know when things are done. Really the only challenge is resolving different order resolutions for client side hydration. My current approach is naively using execution order. If I could give each reactive graph node a predictable ID the async approach could be solvable.

ryansolid commented 4 years ago

If I ignore async rehydration, for now, I can get async SSR pretty easily using Suspense. Probably still useful for static site generation. I can have renderToString return a promise. I've already implemented and am in the process of testing it.

schtauffen commented 4 years ago

I did some more tinkering last night (with 0.15.5) and think we will have to look at fragments .

Also is it expected that For work with js arrays or should only signal -wrapped arrays be passed in? I was getting "not instance of Node" error when trying to hydrate with a For.

ryansolid commented 4 years ago

@schtauffen Can you give an example of JSX code you are referring to? Truthfully I don't have any end to end scenarios testing. But basic fragment stuff seemed to work. https://github.com/ryansolid/dom-expressions/blob/master/test/hydrate.spec.js.

For should take an unwrapped array. So either should be fine:

<For each={signal()}>

<For each={state.list}>

<For each={[1, 2, 3]}>

Obviously the last will never update but all forms should work.

schtauffen commented 4 years ago

I currently have:

App.tsx

import { createState } from 'solid-js'
import { For } from 'solid-js/dom'

const counter = () => {
  const [state, setState] = createState({ count: 0 });
  const setCount = count => setState('count', count)

  return {
    state,
    actions: {
      increment: () => setCount(x => x + 1),
      decrement: () => setCount(x => x - 1),
      reset: () => setCount(0),
    }
  }
}

const HelloMessage = () => {
  const { state, actions } = counter()

  return <div>
    <span>{state.count}</span>
    <div>
      <button onclick={actions.decrement}>-</button>
      <button onclick={actions.increment}>+</button>
      <button onclick={actions.reset}>reset</button>
    </div>
    <List />
  </div>
}

const LIST_ITEMS = [
  'A',
  'B',
]

const List = () => (
  <ul>
    <For each={LIST_ITEMS}>
      {item => (
        <li>
          <button onclick={() => console.log(item)}>{item}</button>
        </li>
      )}
    </For>
  </ul>
)

export const App = () => <HelloMessage />

index.tsx

import { hydrate } from 'solid-js/dom'
import { App } from './app'

const root = document.getElementById('root')

hydrate(
  App,
  root!
)

The counter section correctly hydrates but I get the following for the List:

index.js:312 Uncaught TypeError: Failed to execute 'createTreeWalker' on 'Document': parameter 1 is not of type 'Node'. at hydration (index.js:312) at app.tsx:37 at List (app.tsx:37) at index.js:257 at sample (index.js:246) at createComponent (index.js:257) at app.tsx:21 at hydration (index.js:317) at app.tsx:21 at HelloMessage (app.tsx:21)

ryansolid commented 4 years ago

Thanks.. something is really wrong with hydration right now. I think when I cracked the solution I didn't properly update the compiler for nested cases. There was code that was being generated unnecessarily (and incorrectly from older approach). Fixed in Babel JSX DOM Expressions v0.14.14. You will probably need to blow out package locks etc.. to make sure to get the right version.

ryansolid commented 4 years ago

So looking around the web I see async clientside hydration has actually been a problem for React not just the approach I've taken. Mostly that fallback content messes with everything. Reading through a bunch of issues and old Twitter posts, I believe the proposed approach is treat Suspense as hydration boundaries. Assume everything inside executes synchronously and ignore all initial fallback states when hydrating. This actually isnt at complete odds with my approach as long as I can come up with a unique id scheme. Pure execution order won't cut it. It will locally within a synchronous block but I need a way assign Suspense instances to node sections. Not sure how to do that yet. But it seems possible.

mduclehcm commented 4 years ago

SSR is all about generate html string. Why don't we transpile jsx expression to template string.

function template$(template, ...textNodes) {
  return {
    cloneNode: () => {
      return template.reduce((result, part, index) => {
        result += part;
        if (textNodes[index]) {
          result += textNodes[index].data;
        }
        return result;
      }, "");
    }
  };
}

class TextNode {
  constructor(data) {
    this.data = data;
  }
}

const textNode$ = new TextNode();
const textNode$2 = new TextNode();
const template$ = template$`<div id="${textNode$}">message: ${textNode$2}</div>`;

function TestComponent(props) {
  return (() => {
    textNode$.data = props.id;
    textNode$2.data = props.message;
    return template$.cloneNode();
  })();
}

console.log(TestComponent({ id: "root", message: "Hello world" }));
// <div id="root">message: Hello world</div>
schtauffen commented 4 years ago

@mduclehcm I think most of the complexity of this feature comes from hydrating back onto the html client-side.

Though now I'm wondering if the best approach isn't to just render SSR as you've alluded too (without special regard for rehydration) and then completely rerender via normal Solid rendering path client-side (as if into an empty node, but replacing the pre-rendered content).

You still get the fast initial render, and replacing the content entirely can't be much more expensive than inserting the SPA rendered app into an empty node (especially since we won't have any cleanup to do since the server rendered html won't have any bound event listeners, etc).

ryansolid commented 4 years ago

@mduclehcm Yeah.. I did consider using strings as the server-side method. And ultimately it can be the approach that can be the most optimized. The main reason for me using DOM environment patch right now are as follows:

  1. Not having to be concerned about DOM behavior. Like attributes vs properties. Basically DOM interaction has some interesting behaviors and I'm basically banking on JSDOM or the like to cover that. That might not be a thing I can bank on mind you but I know people are working on it. Custom Elements are also in this range of things which were very important to me when I started the project (although less so now).
  2. Things like refs and the fact that in these libraries const div = <div />. I wasn't sure what was standard for SSR so it was easier for me to execute the code that end users could be writing the same way on the server and client. This may be completely unnecessary. I just was never clear what was expected. I think I recently read React doesn't resolve refs in Server environment. I wasn't sure what decisions made sense here.
  3. It was simpler. Hydration and Server rendered generate and read the same HTML form (vs normal mode which is more optimized). I basically had to add a function different a couple global variables and presto I had both approaches.

A bit of a whole new world here as we have real DOM nodes here, but with JSX we are interacting with them directly in JavaScript. We don't have that layer of the string template (like Svelte) or the Virtual DOM (like React) in between. It's raw like jQuery raw. Super low level yet it feels like working with React. How would SSR with jQuery work? I typically would just say lets not worry about supporting certain things at first. But what makes this all really interesting is with Suspense tracking asynchronicity in seamless way it could be ok for the "mount" step to happen and let it all play out since we can still know when we are done. But that could just be a crazy idea.

That all being said there is a lot of room for improvement here. There is a lot of unnecessary stuff happening on the server rendering part. I wasn't particularly interested in spending months here myself so I took the pass once I had a working mechanism. If someone wanted to help me build out the string version I think that would be amazing. We just need to figure out how to address the concerns I laid out above, and make decisions on what is reasonable. Right now I have a working SSR part but it's hydration that needs work. But with people willing to work on it I'd definitely be into making SSR better as well. People interested in the compiler portion(SSR string rendering etc) should continue this conversation: https://gitter.im/dom-expressions/community

@schtauffen Yes it's arguable. It's also a bit of a cop-out. Libraries use this as a reason not to bother with Hydration. I think we could do this and it would be fine-ish except for Suspense. Since it would need to reload content and there are fallbacks to consider. Ideally you'd want to show the existing content until Suspense resolved and then replace. Of course being server loaded you can preload most of the content. But lazy components are still a thing. And basically you end up with the same app state refresh problem where you have dead stuff you are waiting on that if you interact with will get overwritten and newly generated.

I think the reason people are working on Hydration is to try to make it seamless. Now picture you take a Hydration approach. Page is rendered and there is a small script that executes inline on the page that attaches global event handlers for all events the server-rendered code identified it would need to handle. From this point the page is collecting user interaction like button clicks and input field entry. The main javascript bundle loads which is small with code splitting. Hydration starts, but whenever it hits a Suspense boundary it creates a new Hydration Context that may not have completed since it is waiting on something async. Instead of showing Fallback content we just do nothing. When the initial hydration completes for the main bundle (synchronous) any DOM nodes in that context that had fired events since the page loaded get synthetically replayed updating the state of the newly hydrated content. When Suspense resolves those separate hydration contexts pick up, complete hydration, and replay suitable DOM events.

In theory after this huge web of resolution it should feel as if the page was actually interactive at the same time as first meaningful paint. I have no idea if this is true. And it's largely a research area. It's debatable if it's all worth it.

And that's really what I feel about most of this stuff. At what point is a SPA just faster than SSR/Hydration. Obviously first meaningful paint is a thing, but when I consider service workers after that first load. I'm working a lot on Suspense right now because I want to see if I can get Solid SPA to beat Sapper (Svelte's SSR) Real World Demo Lighthouse Scores by doing fetch-as-you-render approaches. It's close right now when I'm actually waiting on lazy-loaded components to do the fetching. Solid being arguably the fastest client side library has a chance of giving these other approaches a run for their money. But we won't know anything until we try.

Personally I play to my strengths and focus on the client first because that is where I know I can make a real run at it. But I'd love to build out this stuff into a fuller set of complementary features.

davedbase commented 4 years ago

I was wondering if there was any way (even temporary) to handle SSR in Solid right now? I'm deciding which front-end library to use and Solid would be my pick but there's a bit of an SSR need. Even if it's basic and doesn't fully support true/proper hydration/suspense etc.

I wrote the Queue Stream SSR implementation for Inferno.js a while back and it wasn't nearly as complicated as Solid. I however like a lot of the ideas you guys have been throwing down as options.

Thanks for your hard work.

ryansolid commented 4 years ago

Yeah as long as you use JSDOM and patch the node environment as mentioned earlier in the thread: https://github.com/ryansolid/solid/issues/109#issuecomment-569517621. From there you should be able to use renderToString. From there you need to follow https://github.com/ryansolid/solid/blob/master/documentation/rendering.md#server-side-rendering-experimental. I've been doing a decent amount of hydration testing but I don't have a demo of the SSR part yet. I know @schtauffen did get some basic SSR example working using the steps he mentions above, and hopefully its a bit better now with the recent release.

The intention is at some point there will be an easy method to setup the environment but there is a enough stuff changing in this area right now I'm working through some of the more technical details before I solidify on the "How To".

ryansolid commented 4 years ago

Ok I've made some progress and fixed some bugs. I've created a repo: https://github.com/ryansolid/solid/tree/master/packages/solid-ssr that exposes a Node DOM patch (currently trying basichtml). So that makes things a lot easier. There is a simple example in the project as well. This is just the starting point but I'm hoping this can start people trying stuff. Any thoughts or feedback would be helpful.

Thanks again to @schtauffen. Really helped me figure out is needed here so far.

ryansolid commented 4 years ago

Hmm.. there is one big shortcoming of this approach so far. Global state is shared by requests and since rendering can be async I cannot guarantee concurrency. On the server one render could be off doing async operations while another starts rendering. This can mess with identifier generation for hydration (which is just a shared global counter). Given a render can span multiple files and same page can be rendered on different concurrent requests there is no compile time solution.

I opted into keeping a pool of child node processes to handle these requests in isolation. Updated solid-ssr.

ryansolid commented 4 years ago

Ok I've made the example much better. Fully functional with Async Rendering and Hydration using Lazy Components and Suspense on the server. I've written some instructions on how to set it up. It is probably a decent time to get feedback again. https://github.com/ryansolid/solid/tree/master/packages/solid-ssr

matthewharwood commented 4 years ago

I would love this to get to the point where it's Like a Gatsby or Sapper. SSG pages than spa with partial hydration going forward... This interaction is just so nuanced and tricky

ryansolid commented 4 years ago

@matthewharwood Yeah partial hydration should definitely be on the list. Although I'm still unclear a bit how it would work. I guess it's more selective hydration? The idea being parts of the page just never get rehydrated? If you know more about how this works I'd be interested. I honestly always have a hard time picturing it. I've been a web application dev for 15 years, and haven't made an "actual" website really for decades.

Right now I have:

Lacking:

matthewharwood commented 4 years ago

This is the best article I could find on it https://github.com/LukasBombach/next-super-performance ... The reality is that Partial/Selective Rehydration is nuanced AF and there is no best practice yet.

I'mma make it my duty to research more.

ryansolid commented 4 years ago

@matthewharwood Hmmm.. Thanks for this. I think I get what they are doing. But I don't see how this approach lets them not ship the javascript, as far I can see this approach isn't tree-shakeable.

Almost want separate entry points per hydratable components but that isn't the way you statically render it on the server. Given the way an application executes top down it isn't clear how to automatically detect those entry points. You don't want to even pack the top down code. This is different than code splitting via which is focused on a single entry point. I've actually used WebComponents to do something similar to this in the past but it was obvious because they also act independently.

With a library like Solid nothing stops us from having multiple roots (It's how Solid Element works, each Web Component is its own root, yet completely participates in the graph). Everything can update independently so that part of the problem is relatively straightforward. But it's a bit incongruent with how you'd architect the app. This feels like a static analysis not a runtime problem. You'd almost want the compiler to output a differently. I think wrestling with the bundler might be the trickiest. Like if you built an app with a centralized data store and router each with a page with components on them. You only want the js code for a couple components based on the page they are on. Even with a single file we need to selectively skip components/modules.

It's most that at render time you'd want it to construct a build config. At which point it isn't the Components you want to intercept but the modules. More like lazy. The other approach I suppose is name patterns like myComp.hydrate.jsx. That is easier and doesn't require runtime consideration for that part.

A final consideration is Solid already needs to compile differently for hydration auto wrapping things etc. It would require a bit of tweaking to handle selectively doing so from a runtime perspective. To be fair it is already a hybrid approach since I rely on the server side to construct the unique identifiers. It's just a matter of setting the write entry points.

In any case thanks for the perspective. I think we have a lot of tools at our disposal between the compiler, runtime during static render, and how easily Solid supports multiple roots. Putting it all together in the best way sounds like an interesting challenge. I think I will start with simple static general (non-incremental initially) and go from there.

matthewharwood commented 4 years ago

@ryansolid Well now! Thanks for the Awesome Update! I reached out to DevelopIt from preact about Selective Hydration! And he pointed me to this amazing article... There are so many directions it can go from here but try sinking your teeth into this one: https://markus.oberlehner.net/blog/building-partially-hydrated-progressively-enhanced-static-websites-with-isomorphic-preact-and-eleventy/

Obviously this doesn't quite would with progressive routing, so I'll have to research more.

Update: The missing part to all this is how to get the progressive routing.

That's the trick we're (preact) still coming up with solutions for. In an ideal world you could just build apps as if they'd be shipped all-at-once, with routing and all that. Currently, there's no easy way to break those apps apart. If you're working in an environment that has full SSR with data hydration, it should be possible to build a small client-side page loader (similar to Turbolinks, maybe even just Turbolinks) that would use the server's router to re-render on the client. Without that, we need something more complicated - that's what myself and others have been working to bring to Preact core.

So it seems like an unsolved problem today. Solid could be the first!

Update Hugo with turbo links https://youtu.be/o1VciSA_9oA

“Turbolinks: SPA-like Experience Without The SPA-framework Hassle” by Ronney Bezerra https://link.medium.com/HaQ4Lxbtc5

Update: The dream is to make stuff like this easier svelte-travel.netlify.com/place with the utmost optimization. This is all super speculative and there seems to be more discussion around more complex approaches than turbolinks.

Router Animations tend to be a bit of a nightmare for DOM state - video playback continuing, images appearing and then swapping src, etc. Certainly possible to build around, but often trickier than it appears.

That said, when thinking holistically about a ssg/ssr/routing, animations needs to be considered also.

ryansolid commented 4 years ago

Turbolinks makes me think of the new browser feature portals. https://web.dev/hands-on-portals/.

This stuff gets pretty complex just to avoid shipping some incremental javascript. If you aren't going to hydrate most stuff and if the content on the next page is static I am pretty confused why I wouldn't just make those pages static html. If it's the transitions then you kind of need to be running JavaScript to handle those anyway.

So what is the case we are trying to solve?

  1. We want to write our application once.
  2. We want the initial load to be fast and responsive.
  3. We want continued navigation to be as well.

Thoughts.. This could be wrong but it's just where my head is at. Keep in mind I have no experience with this and am used to building completely dynamics apps where all content is catered per user so I've never even considered even SSR having much value. And I'm a bit miffed if content is actually dynamic what value is there in statically rendering those portions if real data is going to be different.

  1. Static Rendered sites without much dynamic content or the need for dynamic parts to be SSR'd. Just use static html with some widgets small javascript entry points. Don't waste your time with SSG.

  2. Static Rendered sites with a moderate amount of dynamic content. Dynamically loaded HTML might win out consider Portals + Selective Hydration.

  3. Static Rendered sites with a lot of dynamic content. Progressive hydration with Code Splitting is going to win out as eventually, you need that javascript anyway best to keep it client-side after the initial load.

So.. the first case is definitely real and outside of the scope of anything special here. Wrap Solid in a Web Component, write a small script and you are good.

The last case is not that far from what we are already doing. Improving progressive hydration through deferring loading seems like a possible area of research. Mind you if enough is dynamic, dynamic rendering is probably better here.

The middle is definitely the most complicated and is interesting in what range does it make the most sense? I suspect there is a desire bias where people might opt for this solution even if the first is better. And all the work we do to improve client-side rendering is designed to reduce the gap. The weirdest thing about this scenario is it is the only one that is inconsistent. The first recognizes the different entry points and the latter is responsible for the whole app anyway.

So I guess I'm wondering why are we trying to write code on the server the same way we do on the client when there is no real benefit. This reminds me when .NET webforms did the opposite. In fact this whole thing reminds of that which from my perspective was the dark ages of the web. Why don't we just build apps with multiple roots using shared context? It would render the same on the server and client. When a page loads it loads the core js bundle which is the same on all pages (so it caches) and then loads separate js for each component(section) which registers and looks up instances on the page to hydrate independently. On the server it basically works the same way except renders those sections instead. Things like fancy page transitions like using browser portals are just more Components. It's something maybe worth trying anyway. DX is a bit different but one thing it doesn't have to be is complicated.

matthewharwood commented 4 years ago

For more context, Sites like this are my wheelhouse: https://www.madeleinedalla.com/. I am okay with the progressive enhancement strategy by itself. As long as the initial footprint is small, the router is robust with named and Multiple Outlets/Auxiliary Routes for granular animation control, and it content can be prerendered for SEO. I feel like you only ever get 2 of 3, Angular Router, Universal, and Animations has been one of the best but comes with a huge page cost.

I feel like I'm conflating this issue with more than just SSR specifications. To tackle number 3 first I think next.js had an amazing api that maybe you can borrow from https://nextjs.org/blog/next-9-3

ryansolid commented 4 years ago

That's a great example. I love how the flow works. Visuals continue through the navigation. Yeah I mean while this is sort of widening the conversation I'm fine with that. I honestly don't know the scope of the desired outcomes or solutions. I just haven't built sites like that. So when I make statements like the last it is just me trying to understand the problem. I tend to think that if things are too complicated something fundamental must be misaligned. That was the tact I took with Solid in the first place. Recognizing that our assumptions of what the solution had to be (both on the reactive and non-reactive side) weren't serving the problem anymore. Part of this is coming from the perspective of understanding the struggle to develop these front end technologies in the first place. There were clear decisions made that differed from the requirements for application building on the server. So when I see client SSR on the server I see a lot of wasted effort. Obviously if it is simple enough there are some good benefits. But I wonder if its time to look at the whole thing holistically from a client front end perspective ignoring, for a moment, assumptions that come with the tools we currently are accustomed to.

So from my perspective this all helps.

ryansolid commented 4 years ago

If it's worth mentioning Solid's website is being made with the current 0.17.0 release of Solid. Using Statically Generated Site and progressive hydration with code splitting. Basically where ever you enter or load the page it is served statically, and then all navigation from that point is 100% client side. It's pretty nice. Might start as the basis for a SSR skeleton.

I'm also working on string rendering to improve performance. The first version will be in 0.18.0. It is more or the less the same as the DOM version but the next area this goes into is looking at changing the hydration approach so we can look at streaming. Just thought I'd give you all an update.

matthewharwood commented 4 years ago

Yay this is great news! Grats on the progress!

For a bit of an update on partial hydration heres my repo: https://github.com/matthewharwood/deno-preact

The partial hydration parts are:

  1. https://github.com/matthewharwood/deno-preact/blob/master/src/client/app.js
  2. https://github.com/matthewharwood/deno-preact/blob/master/src/client/components/component-map.js

My repo is a mess right now but i've opt'ed to do kind of a turbolinks for a router. I've kind of made a dirty data structure to kind of control the links data and it's animation sequence. Needs another pass; that said, here's the gist of it:

const Link = ({link}) => {
return (<a href="https://localhost.com/foo"
      onMouseenter={(e) => preFetchAndRenderOffScreen(e, link)}
      onClick={(e) => beforeRouteChangeAnimation(e, link)}
>
  {link.text}
</a>
)
}
const offScreenSelector = '[data-outlet=aux]';
const innerTemplateTagSelector = 'main';

const preFetchAndRenderOffScreen = async (e, link) => {
  const isCurrentLocataion = e.target.href.split("/index.html")[0] === location.href
  if(isCurrentLocataion) return;

  const f = await fetch(e.target.href);
  const text = await f.text();
  const doc = await new DOMParser().parseFromString(text, "text/html");
  const template = document.querySelector(offScreenSelector);
  template.innerHTML = doc.querySelector(innerTemplateTagSelector).innerHTML;
};

// Animation
function beforeRouteChangeAnimation(e, link) {
  e.preventDefault();

  link.onLeave.map(({selector, keyframes}) => {
    if (isServer) return;

    document.querySelector(selector).animate(
      keyframes,
      {
        easing: "cubic-bezier(0.4, 0.0, 0.2, 1)",
        duration: 350,
        fill: "forwards",
      },
    );
  });
  history.pushState(null, null, link.href);
}

Or the route.js: https://github.com/matthewharwood/deno-preact/blob/master/src/client/routes.js

ryansolid commented 4 years ago

I just had a thought of an approach that I think would be perfect for Solid. v0.18.1 brings support for async data hydration although I haven't outlined the process. But for non-SSG, SSR I think I have a better idea:

Basically after the initial render on the server before the stream is complete we hand off to client. Like look at this Realworld Demo timeline from throttled lighthouse audit run with current client only rendering:

Solid Timeline

Solid might already have the fastest loading timeline. The first JS loads around 75ms, and the Component chunks come in around 175ms. We're able to fire the API calls around 100ms. But what if we could start them at 20ms instead. Those avatars that load at 300ms could be pulled forward 80ms too. Not only faster first paint with contentful HTML but the whole thing would load in about 300ms. Even if the server-side rendering cost an extra 50ms.. still faster and with a quicker first paint.

Compare that with Sapper (Svelte) using static SSR and then loading data in the client. This is the same implementation that tops Realworld Comparison every year: Svelte Timeline

Those API calls aren't til 400ms mark. 600ms total time until all resources load. This streaming approach could be a game-changer compared to typical SSR.

lastonoga commented 4 years ago

@ryansolid I think i did enough research with partial hydration to try it out.

There are 2 ways how we can make partial hydration.

  1. Separate render function (Babel Plugin JSX DOM Expressions) into
    • Dynamic render (adds event listeners and subscribers to make dynamic props and textNodes)
    • Static render (Create DOM nodes with static attributes and textNodes)

Well.. It's closed to be done if we look on compiled code of babel plugin.

  1. Make 2 not connected functions for render and hydration

First way is more flexible, because some methods (loops. if-statements) can be used either for rendering or hydration.

Second way might have about 15% bundle weight save.


Also there are some important points:

  1. We need to skip first subscriber initialization, because its made by server (SSR) to prevent hydration of stateful data that has already made.

  2. We need to pass initial data from server to client (or generate it from DOM) to make loops and statement initial hydration.

I mean, we have 100 items that was rendered on server and we need to hydrate it and render diffs. Thats why we need to know what items was rendered on server.

  1. We need to understand is Property stateful or not on compilation step. That why we need type system for props and new type: Observable (stateful what ever).

Thats why components with dynamic props should be separated from stateless props. Why does its matter? Because there are tons of component that in 90% of time uses with stateless props: rows, columns, headers, buttons and etc.

  1. Basically, we need to understand is variable stateful on not on compilation step. But i think its not that hard (depends on architecture).

Some questions

Does solid has slots functionality?

ryansolid commented 4 years ago

Part A: Yeah we basically get number 1 for free since that is how it works generally. Static parts are copied into a template that is cloned in the first step. When the compiler is set to hydratable the clone call is replaced with a getNextElement or clone call essentially. Depending on the hydration state it decides whether to clone or fetch the pre-rendered nodes.

I think that makes the first scenario highly desirable. I think while in some niche single time cases maybe approach 2 is smaller, in general because of different helpers etc it would actually be much larger than the first option. I know Svelte uses the second option and it's part of the reason that it's components don't scale as well. MarkoJS conversely prefers the first option for this very reason.

I guess it really comes down to the ultimate goal here. I think there are a lot possible solutions in this space. Am correct that the partial hydration approach assumes that routing is happening on the server? Most of my work so far as has been around assuming the client would eventually fend for itself. Ie.. I'd need to load the client JS eventually (what if I went to a different page and came back).

Part B:

Hmm.. I see what you are trying but I think there are a few snags.

  1. Computation execution is how dependencies are determined. There are explicit syntaxes but we don't do this for bindings. And since all bindings are also computations we basically need to run a version of them for them to update later. It isn't automagical but my thinking here is isomorphic data loading should initialize to the stored data if present, otherwise the defaults and that would have to be in user code, whether in a store or not. We could wrap this at the global store level so we could apply data blob and initial values. But computations pretty much need to run first go or they will never update. This means for complete render hydration data has to be present at the onset of hydration. For streaming we aren't transferring the async HTML at first anyway so it's fine.

  2. Here is an example I made for Solid with data hydration. Super simple: https://codesandbox.io/s/solid-hydration-ri3jn?file=/index.js

  3. The compiler does know what props are dynamic because it does additional wrapping. createComponent call looks more like:

    createComponent(MyComp, { id: "main", onClick: e => /*...*/, name: () => state.name }, ["name"])

    I mean if someone passes a signal directly I don't know but if they use binding syntax the way recommended I know that name is dynamic. The compiler uses a heuristic based on property access or function calls to determine dynamic properties. There is also a comment indicator to say something is definitely not dynamic /*@once*/. For actual DOM nodes its similar since I have to decide what to wrap in computation or not. Part of Solid's performance is not over-wrapping.

But what I'm not following is the data type bit. Sure a signal can be a typed. But it can be wrapped and transformed at will. In the end we just have functions. If we force explicit types at binding we are over-wrapping at the Component level. This fundamental to performance. Nothing is bound to a Component. Which I suppose is what this makes this a bit tricky. A component might pass something reactive all the way through it without actually doing anything else.

We want to do partial hydration to prevent shipping extra component code. We could skip certain static templates though. We could even do this at the compiler level. What if we could identify which templates were not dynamic and just skip them. I guess they'd need to have no event handlers or dynamic bindings. Instead of trying to remove "Components" we just generate smaller code that only has the hydration code for things that need it? If something is static the end user shouldn't wrap it in a signal. We have the compiler here to take advantage of. Basically I'm thinking that it might easier to leave the components in place but they be essentially empty conduits. But can we do more if Components have no dynamic props, at which point we don't even need to call them. However, I see it being difficult to arbitrarily pick up code downstream without knowing ownership. Nothing says we can't use separate roots but how do we even know what random node 70% down the tree is connected to. If the code of its parent isn't in the client how do we even know to be executing 4 copies of that component there.

It's easy to see huge savings in the leafs by just skipping whole components but a lot harder if the leaves are what is dynamic (which tends to more often be the case with reactive since we try to flatten the graph for performance). In any case potentially having the compiler generate smaller code could be a tact here.


I'm just trying to sort in my head the different scenarios to support. Currently I have this:

  1. SSG - Fully generate pages ahead
  2. SSR - Generates pages at runtime, waits for data

I'm also looking at this:

  1. Streaming SSR - Generates pages at runtime, stream data as it comes in

Part of me wonders if we can just skip 2 if 3 is viable and basically have 2 options. Either build ahead statically or streaming. The benefit of streaming is we don't need the server rendering to be reactive as it doesn't need to wait to resolve changes. It can statically render and the reactive system only belongs on the client as it fills in the holes.

I realize though that there is another dimension at work here for each.

  1. The intention for the client to pick up the pieces. I've been very interested in this. Sure the server has advantage initially but afterwards I love the idea of cutting it out. Sure you can refresh on any page and restart. But code splitting and pre-emptive loading pretty much removes any SPA downsides after the first load and you get all the benefits. This has been my area I've been looking into.
  2. Conversely there is partial hydration which I guess suggests the server is going to be doing more of the work even after first render? Since we don't ship the necessary code to re-render the page if we ever navigated back. This begs the bigger question I suppose is who maintains state, data cache etc. Does is go back to the domain of the server if we are doing full page loads?

Ideally I'd like to support both. But I guess that means there are really 6 (maybe 4) different combinations of scenarios here I think.

EDIT: Oh @lastonoga as for slotting. It works like React. We pass through props.children. Those are lazily evaluated and the child component places them in the view as desired.

lastonoga commented 4 years ago

@ryansolid

Am correct that the partial hydration approach assumes that routing is happening on the server...

Yes. If we are talking about SPAs, you are absolutly right. But what if we return to 10 years back..

Old apps/websites works good, just because of

  1. Render of the page was on server (raw html)
  2. Client gets raw html and hydrate it (jquery or whatever) even with history API

Huge static computation was on server side (that can be cached easily) but now everything is made on client side

Basicly, we might use same algorithm to create SSR with fast client.


  1. Well, then it's not really important. We can wrap up a hydration function to skip first call with something like this

    if(first) {
    first = false;
    return;
    }
  2. Got it. But there is still a problem:

Component C with prop Name Component A that pass dynamic Name to C Component B that pass static Name to C

in B component C should not be hydrated (name is static) in A component C should be hydrated (name is dynamic)

So we can do something like: (observable vars always with $o prop)

if(hydration && itemId.$o) {
    _$insert(_el$2, itemId);
}

We want to do partial hydration to prevent shipping extra component code...

Absolutely, we can skip static component and that gives huge performance speed up. But as you mentioned there is a problem with dynamic props. I have not tried checking props in runtime, because i choosed typed props, but i can try.

That solutaion might work good, need to check it up. (should be places on top of render function)

shouldHydrate = hasComponentState;
for(let prop of props) {
    if(hydration && prop.$o !== undefined) {
        shouldHydrate = true;
    }
}

if(!shouldHydrate) {
    return;
}

_$wrap, _$insert and etc goes here
  1. Conversely there is partial hydration which I gu...

Thats true, if first render depends on application state we need to store that state somewhere. And there is a problem.

So... I see only one way to try partial hydration for now: its to make it in runtime. We are wrapping actions with stateful data in

if(itemId.$o) {
    _$insert(_el$2, itemId);
}

And all props in

if(hydration && itemId.$o) {
    _$insert(_el$2, itemId);
}

Adding shouldHydrate part on top

I don't know how its gonna work with solid because i haven't looked into it, what do you think?

UPDATE:

I did a simple demo with templates and it works crazy fast for static. https://github.com/lastonoga/sinuous-components/blob/master/src/template-test.js

For 10 000 static components Render - 327ms Hydration - 27ms

It simple without a lot of functions, but overhead adds maximum 30-40ms. So its nothing even for 1000 components.

ryansolid commented 4 years ago

Keep in mind this is all coming from a place where I designed Solid to maximize SPA/client interaction and SSR is an afterthought I'm sort of still working out. My first goal was to attempt to make SPA faster than SSR even under throttled conditions. I've mostly succeeded. Solid's size/speed + code splitting is even giving SSR pages a run for their money on First Contentful Paint. So my interest here has been mostly to see if I can eek out the tiniest bit of extra performance from starting earlier on the server. That being said I know there is a lot of interest in the other side of things.

We can wrap up a hydration function to skip first call with something like this....

I sort of already do this internally in the render insert methods. I basically evaluate the value to be inserted (to bind to it) but skip any of the DOM operations other than text. Attributes are a bit trickier. I could prevent then from assigning without too much effort I suppose. Basically things are individually wrapped to a degree (although not everything since there is performance overhead) so we'd need to intercept the setters in those areas. It's doable. Right now I do a bit of extra work but that can be improved over time. Although that doesn't help us with code size.

Component C with prop Name Component A that pass dynamic Name to C Component B that pass static Name to C

Yes I see the problem. Hmm.. identifying something as observable is tricky especially when you consider proxies that hold primitive values at their leaves. You can't just mark them. But I suppose the problem is the other way around. Component C has to decide that Name is always treated as dynamic. I have gone out of my way to unify the API's so the consumer didn't have to worry about this. I think it produces very poor DX to have be conscious of this from both the creator and consumer standpoint.

It could be possible I suppose to look if the prop is being accessed in a dynamic context. But it isn't trivial given compositional patterns. It might be accessed this way in a different file. We could give explicit annotations to the compiler though I suppose. Basically how could I ever know that prop.name should be treated as dynamic it should never be treated as a prop type. I can't just wrap everything otherwise we'd never be able to remove a component. I'd almost need to look at all the call sites of this component. Modern tools/bunders don't have this capability easily. I know the MarkoJS does this sort of analysis since they are basically their own bundler. Maybe that is how they achieve this.

I do watch your Sinuous examples with interest. Sinuous is all runtime so it might be easier to test things there. Solid is sort of a hybrid. It means the combination of Solid's more performant reactive system, and precompilation results in better performance. In many places actually fairly significantly (it's why I went this way, and didn't just stop at my HyperScript version). But it is harder definitely to come in and get that benefit since you can only touch the runtime to a minimal extent since the compiler depends on it. However, when we do figure it out the solution the ceiling is way higher.

My challenge is understanding how to best model the problem. So I find these conversations very helpful to understand what is needed. The problem is it is harder to just whip up an example. But I think I'm following what you are saying mostly. So I'm going to approach this more from a compiler standpoint I think since the runtime piece isn't going to be the end goal and mostly just produces a bunch of extra code checks. Whereas the compiler lets us actually ship less code instead of more. There is always a runtime part undeniably. However I think we can learn from the runtime approach about what is feasibly skippable and possibly just not ever go about writing some of those pieces.

So crazy idea. What if we annotate Components at the call site as a compiler hint. Ie:

<MyComponent $stateless />

Basically add a manual do not include. In so the bundler will tree-shake it out of the output if there are no dynamic props. If it is used in another location not marked or with a dynamic prop then it needs to be included. So be it. I know a bit of a pain right. But props are never going to be sufficient to know if it is dynamic given internal state or any internal child state down the tree and the call site can not know about that at compile time with current bundler technology. In an ideal world we could include some metadata on the Component that could be derived up the tree and analyzed statically.

See I already have the ability to skip non-dynamic expressions and if we detect none we could choose to just skip hydration on the whole component. However, unless you have an idea here, I think we don't get to remove Components that have dynamic children somewhere down the line. We still need to find where to insert them. Even if we remove practically all the other code someone needs to walk the tree with an idea of its structure. It seems logical to be the owning Component. However we can avoid shipping any other code and completely prune stateless branches. This approach is relatively cheap to implement.


I made a Solid implementation for Marko's Isomorphic Rendering Benchmark. As expected Solid is doing very well on the client tests (in the color-picker it's like 3x Inferno, the next fastest library). But its SSR is the worst of the bunch. It's not much worse than Preact but I was hoping my approach would be much faster. I think it's my async by default and possibly tagged template literals on the server so I'm going to probably focus on SSR performance first.

lastonoga commented 4 years ago

In example that i made i check props in runtime and there is no problem with performance at all.

Sinuous reactivity use a little bit different approach thats why its available to "mark" observables and check it in runtime.

Remember part of "method first call"? I've compared dynamic slots with NuxtJs and to be honest its quite fast. It takes about 250ms for innerHTML in NuxtJS, while my code 600ms, just because of calling functions to bind observables to dependences.

So if we do that by hand it gonna take less then 30ms to make hydration.

I understand that its not that easy to change reactivity system in whole project. And I am not trying to make you work, I just sharing my remarks and some performance points that might be important and helpful.


If its possible can you share a link of fork with solids benchmark, please?

ryansolid commented 4 years ago

@lastonoga It's not a problem. I guess I'm trying to say in the kindest possible way I've already been there. I expect it won't be long before you are looking for some of the things Solid solves. I don't expect everyone to agree on the tradeoffs. There is a lot more than I am writing here especially when we get into benchmarks but on the surface consider the following:

I don't mean the check is expensive, but rather the pattern it enforces. In simple cases it is no additional cost since you already dealing with these primitives directly. As soon as you are doing something more complicated you are no longer directly holding them. This is an inane example but consider the difference between:

const [number, setNumber] = createSignal(1),
  doubleNumber = createMemo(() => number() * 2);
return <div>{doubleNumber}</div>;

// And:
const [number, setNumber] = createSignal(1),
  doubleNumber = () => number() * 2;
return <div>{doubleNumber}</div>

They are functionally the same except the first case makes 2 computeds instead of 1. Ie in 10k rows, 20k computeds instead of 10k. However the second case can no longer identify what is being bound is strictly reactive at runtime but it's way more performant. Now consider composition patterns like those "newly"(cough) popularized by React Hooks. Should end-users be required to make special calls to wrap everything.

I go a bit further than this with Solid compilation too. Instead of promoting syntax that passes functions that can cause ambiguity say for event handler functions to a component, as functions are valid props without being reactively executed. I instead expect the expression to call the observable and use the heuristic of function call or member access as a baseline to determine reactivity. I still wrap in a function but it is simply that. Now I could add a marker to that I suppose but universal property access that comes out the other side of Components is no longer dealing with those functions directly. This is more questionable but not having to deal with mixed types is a blessing. This decision is also to support the no isObservable approach.

As for native DOM elements, what if the template has 8 dynamic bindings? Bringing back our silly 10k rows, 80k computations that's horrendous for creation. What if I told you the compiler looks at the structure and combines them into a single computation with a simple diff where it makes sense. Not for structural inserts or places with heavy reconciler overhead. But maybe we only create 20k instead of 80k.

That's what I mean by this being very intentional. Most reactive libraries work the other way with explicit wrappers. I spent years with Knockout JS doing that dance. Avoiding it is a big part of what makes this approach so performant.

The main benefit of fine-grained reactivity isn't that it does super granular updates. It's that the granularity is controllable beyond the constructs we use to modularize our code. Boundaries are expensive as they usually lead to synchronization. I've looked at combining computations cross components to reduce that overhead. There is a sweet spot and I felt it required more research into deciding where to use it.

The compiler is the other part since there is no need to constantly top down execute code so we can use it to carefully reduce creation overhead. Remove the guesswork and runtime checks. Otherwise I mean we've been here before. You remove those 2 aspects and you are more or less back in 2012, before React and reactive libraries roamed the web. There is a reason these libraries went extinct. We've made some other improvements (batching comes to mind). We can do better this time around.


As for the benchmark. The client numbers are great as expected. I want to do another pass before revealing SSR numbers. I was able to triple the score by doing a couple simple things, but I can do much better.

The problem is it isn't apples to apples. I built my SSR solution assuming async resolution off the bat. String SSR renderers like Svelte and Marko simply don't. I was using strings but was setting up a reactive graph to handle updates on the server. These libraries just don't. The VDOM libraries are closer performance but I haven't optimized the graph creation as well as in the client and without the DOM cost overshadowing everything it just has more overhead.

So I could basically take a page from Svelte and optimize for no updates. This has the upside of feeding into streaming solutions which do first render this way as well and probably makes partial hydration simpler. However wait for asynchronous approach on the server necessary for like SSG diverges which ironically is my current primary use case doing JamStack type approaches. Of course performance matters less there since it's precompiled.

It is also a longer path back to async SSR since I'd need to implement streaming. I wasn't prepared for this until I saw how poor the performance is here. At least hydration is fast as I already via compilation skip walking any of the truly static parts.

ryansolid commented 4 years ago

I made some progress. I manipulated the compiled output to be static and basically matched Marko/Svelte performance coming out nicely ahead of React, Vue and Inferno. So I know exactly what the compiler could output to achieve that goal. It isn't that hard. But it makes other future things much harder. This is just going to take some more time. I need to weigh the cost of all hybrid approaches. I feel this needs to be focus before I get back to hydration (a problem better suited for fine grained).

ryansolid commented 4 years ago

Just thought I'd update this issue. Technically what I found earlier was not quite true. Svelte and Inferno did not perform as well as I thought in the more basic(raw benchmarks). Svelte's performance was decent but not exceptional, and Inferno looked quite impressive until I realized they weren't escaping attributes.. Something that I wasn't doing perfectly in my modifications. Basically Marko was double the speed of everyone else and not taking any shortcuts.

So further examination has helped me finally get to the same level of performance in a raw vanilla prototype but I'd need to have the compiler output it. On the positive the sentiment from the previous post still holds up. We can do this. The trick is that we need to remove all reactivity from the server, which only works with no async. That requires special thought. I want to support async for SSG to handle code splitting statically and at that point why wouldn't we just do the whole thing reactive as perf doesn't matter much here. For real time SSR there are other solutions where if we force streaming we can look at still removing reactivity but making specialized behavior around resource loading to basically send async data over the wire and let the client handle it.

The challenge is that while compilation helps here there still needs to be different mechanisms in the runtime. At minimum I'd need to have a non-reactive runtime that proxies most of Solid's APIs things like createSignal and createMemo with a minimal version that can be bundled in via import renaming in end-user builds. But the trickier part is can I keep it at 2 or will there be significant differences for some of the reactive SSR techniques we wouldn't want in the normal reactive runtime. I think it boils down to:

Static SSG or SSR and streaming SSR -> set babel-preset to { generate: "ssr", reactive: false } - uses different runtime Async SSG or SSR -> set babel-preset to { generate: "ssr", reactive: true } - and just keep in mind this has a performance implications.

And then I bias the compiler for generate: "ssr" towards reactive false in the way it structures the code and reactive true will just do the wrapping and add some extra code.

So with that in mind steps are:

  1. Update compiler code to support most optimal paths in terms of code gen. Keeping reactivity as is for now. Completes Async options.
  2. Add reactive argument to skip extra work on compilation.
  3. Create new non-reactive run-time and configure babel-preset to seemlessly include import renaming based on input options. Completes static options
  4. Update Resource API for non-reactive runtimes and for hydration in reactive runtimes to support streaming. Completes Streaming SSR
  5. Add option to compiler for {generate: "dom", hydratable: true } to skip static code creation for people working on MPA. Completes Partial Hydration

With this in mind I think we end up with 4 main configurations:

  1. Ultimate Performance - Streaming SSR + SPA

The key here is after initial load its all on the client. Super fast and responsive. This leverages Solid's performance and small code size and code splitting. The benefit of even doing SSR is not just First paint but that we can start fetching the data sooner on initial load. Perfect for dynamic applications that care about performance and can't justify initial loading time.

  1. JAMStack - Async SSG + SPA

This is how we're building the Solid website. Good for mostly static content. Loads quick and works quickly. Doesn't get to leverage partial hydration but that has never been a problem for a small library and limited amount of dynamic parts anyway. Really the only hit you are taking is that if there is any data loading it is going to start later.

  1. eCommerce - Streaming SSR + MPA

Good for large companies where SPA are hard to manage. It definitely has a performance hit with a decent amount of wasted work on page transitions but will have fast first load times. Can support Partial Hydration which means smaller bundles per page which can help offset the fact you are constantly loading pages. Stuff like Turbolinks can help here too. I suspect this case is better left for traditional server technologies, but it always trades off UX for all but the simplest apps.

  1. 90s Retro - Async SSG + MPA

The least performant option if there is data loading. But for mostly static content where people stay on the same page for a long time and don't navigate much this solution makes sense. Things like blog sites.

I think the most interesting thing about this SSR + SPA while really performant initially might not be the best experience for applications with continuous usage like Mobile PWAs. If we are dynamically rendering we are wasting a lot of effort communication on something that could be statically cached by a Service worker. So while 1 might be a benchmark killer, I'm not sure it's actually as practical for the use case where I'd just use a SPA. And 3 seems like you should be using something from Basecamp unless you need the absolute best performance in which case why aren't you using 1. 4 is cute but you could be using Wordpress or 2. Which brings me to, is 2 the best use of SSR?

Knowing that is all this work a bit in vein? It definitely makes Partial Hydration seem less useful and streaming like a lot of work. I mean we will get there eventually but maybe from priority standpoint there are other things to improve the experience around SSR that can be improved before.

So I guess I'm asking does it seem reasonable to think that realtime SSR is overrated for most things (outside of dynamic eCommerce, like Amazon, eBay), and the best options really just pure SPA, or SSG + SPA if you need SEO for the vast majority of things?

matthewharwood commented 4 years ago

@ryansolid great job above man... super elegant. Here is a list of my thoughts in no particular order ..

So I guess I'm asking does it seem reasonable to think that realtime SSR is overrated for most things (outside of dynamic eCommerce, like Amazon, eBay), and the best options really just pure SPA, or SSG + SPA if you need SEO for the vast majority of things?

Scale matters. SSG of 100k pages is gonna be time intensive or expensive. More about that below.

I want to support async for SSG to handle code splitting statically and at that point why wouldn't we just do the whole thing reactive as perf doesn't matter much here

  1. JAMStack - Async SSG + SPA
  1. Ultimate Performance - Streaming SSR + SPA. The key here is after initial load its all on the client. Super fast and responsive
    • In my experience, the big problem with real time ssr are slow blocking network calls.
    • I'm not really fully aware of "streaming SSR" but what are the implications, in regards, to SEO?

This is how we're building the Solid website.
lol what is the solidjs domain? And what are you using to build it?

Stuff like Turbolinks can help here too.

Another addition to this is the useTransition lifecycle hook (hopefully soon) coming to React. It could afford the oppertunity to animate routing components in and out.

Static SSG or SSR and streaming SSR - Async SSG or SSR ->

Sometimes we have a large code base under a single domain where we want to declare this is prerendered this is SSR. Router seemingly is the easiest spot for this. I'm sure it could get more granular (by component) but I'm also sure there are some big implication here. Just something to consider.

4 is cute but you could be using Wordpress or 2

::shutters:: Thou shall not speak of php >_<. 2 can have scaling problems.

SSR + SPA while really performant initially might not be the best experience for applications with continuous usage like Mobile PWAs

Jason (the author) said this hasn't been proven out at all but might be relavent (i don't even understand it) https://developers.google.com/web/updates/2019/02/rendering-on-the-web#trisomorphic

ryansolid commented 4 years ago

SSG

I was thinking eventually incremental approach. I imagine not every page gets regenerated that often. That being said I guess there is the change the header problem. It's not surprising I suppose that they have an on-demand static generation approach.

It's hard for me to picture the 100k scenario simply because I'm used to dynamic content which always lends to there being less conceptual pages. Likely by an order of magnitude if not more. But I guess SSG leads to more pages since you want to reduce dynamism to maximize the benefit of pre-rendering. Even SSR potentially reduces the number of conceptual different pages. At that scale, SSG seems like a caching mechanism not a solution to itself. So I think at that point it becomes something else. It isn't like there 100k pages developers are independently maintaining. It's that via parameters we are statically rendering different pages from a much smaller number of page templates.

Regardless of ultimately being separate MPA or SPA, SSG just falls apart in that range. So I'm just hearing that SSR ultimately needs to be the solution for large sites. You will be spending all your time re-rendering anyway. Good to recognize the split isn't so cut and dry.

SSR

What I mean by streaming SSR is rendering the initial page and streaming it immediately, forking async requests, and rendering placeholders as you hit them. As the async data comes in sending patches to the client to fill in the gaps over the same stream. Basically you get ultra fast TTFB, and FCP since you send the header immediately without even waiting. Async requests happen as soon as they would for typical server rendering and because you keep the pipe open it all finishes around the same time as if you waited to complete rendering. For Solid I was thinking I could patch with data instead of HTML.. to leverage the reactive system on the client to render the updates, avoiding the overhead server side. From there you can take 2 ways (SPA vs MPA) but I liked the idea of going to SPA at that point so any future navigation would be snappier.

Trisomorphic is interesting but I don't see the immediate advantage over Client side rendering at that point.


Also when I lament about performance around SSG and reactivity on the server. It's still ok. Faster than React/Preact.. Slightly ahead of Vue, but slower than Svelte or Marko. To match Marko we need to remove all reactivity from the server.

ryansolid commented 4 years ago

As I've been working through this I've found a better way perhaps to organize the solutions. While the 4 use scenarios above I believe are still true and what I'm catering to here. Implementation wise I think there are really only 2 solutions.

There is no real difference between SPA/MPA. Was thinking that I could more aggressively prune stateless components. While that is true it's because of structure not because of some inherent difference. Everything under control flow must be included (atleast in a code split bundle). I was thinking this was only a routing concern but even MPA have conditional rendering in places. In a SPA (client side routed) the control flow is almost top level so you need everything, but in an MPA there more top-level prunable Components. So same mechanism to prune will work for both. It's just a matter of being able to detect or mark Components. Passing structural metadata is beyond my capability currently, so I think simple indicator on components where used that it is non-client will do the trick. Not elegant but we can then leverage something like tree shaking to just determine whether it's needed (ie.. needed at one location but not the other still gets included).

Unified SSR

Standard

Async

Either can be used for SSG or SSR. Depends on how much work you want to push off to the client. To be fair you could engineer a solution to load data upfront before doing Standard rendering to get the same benefits of Async without the overhead but that isn't a solution I can generalize because it's outside of the scope of the runtime.

Important:

Standard always looks like it is loading anything async(including code split routes) on refresh since it doesn’t wait for things to resolve. This ensures the fastest initial render of the app skeleton but acts like a SPA. Key benefit is that data fetching happens earlier with streaming so this approach can outperform a typical SPA on initial load and handle all the SEO stuff.

Async is going to be the heavier process as it requires reactivity on the server, and has the double data problem on the client. But if statically generated it doesn’t need to wait on anything. Great for static sites, with simple interactivity. You can use this for SSR but it will be fairly low performance on the server (standard Preact/React range). This approach hides the loading visuals of code-splitting so it gives you that classic server-rendered impression.

Considerations:

Should we always just require a unique identifier to just unify the developer experience and error at runtime when the name isn't unique. Like:

const [read, load] = createResource(uniqueName, initialValue);
const [state, loadState, setState] = createResourceState(uniqueName, initialValue);

I'm heavily leaning towards a yes here. There has been some desire to do this for signals in general for debugging but that is optional I suspect the resource API where it wouldn't be optional would need to call it out more directly so I'm not sure it's unifiable. It's also important that it be unique. So that might mean that it's a string identifier plus a model id, or essentially the "key" for that component.

ryansolid commented 4 years ago

Ok got some preliminary performance numbers comparing the SSR approaches:

Legend:
L - Load Event
FP - First Paint
FCP - First Contentful Paint
FMP - First Meaningful Paint
LMP - Largest Meaningful Paint

standard:
L: 103ms
FP: 107ms
FCP: 107ms
FMP: 107ms
LMP: 674ms

streaming:
FP: 104ms
FCP: 104ms
FMP: 104ms
L: 552ms
LMP: 588ms

async(current):
FP: 596ms
FCP: 596ms
FMP: 596ms
LMP: 596ms
L: 601ms

The scenario is a isomorphic routed app with lazy loading on the component and a simulated 500ms load on nested route data. It is set up this way as no matter which route is loaded it seamless transfers to client-side routing once loaded. It makes the scenario more challenging because I'm doing lazy loading via code splitting and data fetching on the same route. Admittedly I should probably pull data fetching out of the lazy to stop waterfalls. They aren't visible but it definitely delays loading time. However it's the same for all versions.

The current published version is the async on the server which waits to resolve everything before sending it. Standard does everything synchronous on the server and then does all data loading in the client like a typical SPA, and streaming streams the synchronous data, while fetching on the server and streaming it over as it comes in to be rendered on the client.

Early observations reflect what I was expecting. Important markers I think are FMP and LMP.. All the first paints are more or less the same because the example is fairly simplistic. And Suspense makes it only have 2 states (initial loading state with header and nav rendered, and fully loaded).

So understanding that we can see that where the data is loaded on the server we have about a 70ms improvement on total load time. It makes sense since it can start fetching sooner. Serving back synchronously is clearly a winner on speed coming back around 100ms. That being said I wonder if I can change things to get streaming to start coming back even sooner. Maybe not since you need to run the code to render before sending the head over I think since you need to grab some of that to inject into the head.


I ran Solid through Marko's Isomorphic Bench

Server Side:

Running "search-results"...

Running benchmark "marko"...
marko x 4,641 ops/sec ±3.00% (81 runs sampled)
Running benchmark "preact"...
preact x 473 ops/sec ±2.87% (78 runs sampled)
Running benchmark "react"...
react x 598 ops/sec ±7.06% (75 runs sampled)
Running benchmark "vue"...
vue x 1,282 ops/sec ±14.53% (58 runs sampled)
Running benchmark "inferno"...
inferno x 2,057 ops/sec ±1.38% (85 runs sampled)
Running benchmark "solid"...
solid x 5,989 ops/sec ±1.06% (91 runs sampled)

Fastest is solid

--------------

Running "color-picker"...
Running benchmark "marko"...
marko x 20,445 ops/sec ±2.56% (84 runs sampled)
Running benchmark "preact"...
preact x 3,019 ops/sec ±2.66% (87 runs sampled)
Running benchmark "react"...
react x 3,925 ops/sec ±1.47% (88 runs sampled)
Running benchmark "vue"...
vue x 5,700 ops/sec ±5.02% (75 runs sampled)
Running benchmark "inferno"...
inferno x 17,181 ops/sec ±4.89% (90 runs sampled) ***
Running benchmark "solid"...
solid x 12,751 ops/sec ±1.28% (93 runs sampled)
Fastest is marko

--------------
*** Inferno skips escaping an important attribute. Solid skipping the same scores: solid x 20,326 ops/sec ±0.70% (91 runs sampled)
Real Inferno score probably about 9000 ops/sec.

I'm suspect of Solid's score being so high on search but I'm not missing escaping. I'm going examine more.

I also grabbed Svelte's fork and ran on the server:

Running "search-results"...
Running benchmark "svelte"...
svelte x 3,392 ops/sec ±2.86% (80 runs sampled)

--------------

Running "color-picker"...
Running benchmark "svelte"...
svelte x 7,978 ops/sec ±1.01% (81 runs sampled)

--------------

Unfortunately Marko doesn't run client side for me currently in the repo. Now that I'm on the team I'm probably going to fix that. Our official results were slower than Inferno there though. On the client it looks like:

Client Side:

Running "search-results"...
Running benchmark "preact"...
preact x 90.09 ops/sec ±3.16% (46 runs sampled)
Running benchmark "react"...
react x 164 ops/sec ±1.05% (54 runs sampled)
Running benchmark "vue"...
vue x 105 ops/sec ±1.45% (52 runs sampled)
Running benchmark "inferno"...
inferno x 184 ops/sec ±1.13% (53 runs sampled)
Running benchmark "solid"...
solid x 201 ops/sec ±0.80% (55 runs sampled)
Fastest is solid

--------------

Running "color-picker"...
Running benchmark "preact"...
preact x 4,512 ops/sec ±2.30% (27 runs sampled)
Running benchmark "react"...
react x 5,563 ops/sec ±4.40% (57 runs sampled)
Running benchmark "vue"...
vue x 3,163 ops/sec ±3.33% (54 runs sampled)
Running benchmark "inferno"...
inferno x 12,862 ops/sec ±0.77% (60 runs sampled)
Running benchmark "solid"...
solid x 14,533 ops/sec ±0.59% (62 runs sampled)
Fastest is solid

--------------

All in all, I'm satisfied enough with Solid's numbers. I did have a version that could match Marko in the Color picker but it made dangerous assumptions. In order to do it I only escaped known expressions. For a library like Vue, Svelte or Marko they can do that since the template only allows inserts of strings. Solid's JSX supports the full breadth of JavaScript like the VDOM libraries. I need to guard against some random JS code imported from a different module which I can't statically analyse. I don't know if would return strings or DOM elements etc..To solve this to prevent escaping HTML templates and breaking it I wrap them in an object that won't be escaped. But this adds an overhead on every template as I need to wrap them with a function call to process all the holes. With Marko or Svelte it could be just a string concat. Still I learned everything I could from Marko here and have fairly optimal SSR,

Client Side nothing touches Solid but we already knew that.

ryansolid commented 3 years ago

Thank you everyone that participated in this thread and helped Solid define its SSR behavior. I think this thread has run its course and new individual threads can be made for bugs or enhancements.