Whatever happens with Qwik in the long term, it added important new terminology—"replayable SSR" versus "resumable SSR".
I would go as far as saying that it identified the core limitation of contemporary client side frameworks—SSR reliant on "replayability" which is a consequence of their client side centric design (as opposed to a design that is distributed from the beginning).
Dynamic server rendering pretty much follows this pattern:
Gather the necessary dynamic data.
Render that page state into HTML and ship it of to the client (the template may or may not include JS for interaction)
After parsing the payload from the server the page is ready to go.
This pattern is pretty much guided by doing as much work as possible on the server to minimize the work needed on the client (and taking maximum advantage of the browser's built-in capabilities) which makes sense in a distributed environment where
connection quality is unknown
client computational power is unknown
Extending this to a distributed application leads to resumable server rendering:
Gather the necessary dynamic data.
Render the application's initial state.
Transform that application state into HTML and ship both to the client.
The client displays the HTML as is but joins the delivered initial application state into the application which is then in control of interaction.
The key here is that the "pre-rendered initial application state" isn't just "initial data" that needs to be "played out" inside the application but is the actual internal application state—so the application is ready to go once it's dropped in. Not only is the display shipped as pre-rendered HTML but also the application state is pre-rendered.
Now Qwik adds some other tricks like fragmenting interactivity to the smallest possible granularity in order to aggressively optimize TTI but the fundamental shift in thinking is moving from today's "replayable SSR" to tommorrow's "resumable SSR" (pre-rendered, joinable application state).
It's my sense that "resumable SSR" is going to force a major rethink in terms of client side application architecture (how it's partitioned) which may require significant tooling to get to the necessary level of DX that would ease its adoption.
If you have derived data trying to get back to the original to re-derive is in many cases impossible.
If I remember correctly Qwik initially was trying to store application state in the HTML but I think they have abandoned that idea.
the state seemed to be stored directly in HTML element attributes.
By December it consolidated into a qwik/json script block containing "flattened JSON with reference keys" (with some reference keys being used as HTML element attribute values).
I'm having trouble understanding the difference between 'initial state' vs 'actual internal application state'. If the point is to have the app take over once the initial app is rendered on the client, then isn't initial state, the only state there is? Can you give an example of a difference?
Perhaps replace “initial state” with “initial data”.
Hydration:
Use initial data to “boot up” the application on the server to capture the resulting HTML.
Combine initial data into the HTML and ship it to the browser.
In the browser use the initial data to “boot up” the application again (replay) to create application and framework state.
Create the necessary bindings between the DOM (created from the HTML loaded by the browser) and the framework state.
Resumability:
Use initial data to “boot up” the application on the server to capture the resulting HTML, application state and framework state.
Serialize application and framework state and combine it with the HTML and ship it to the browser.
Initially in the browser there is only one tiny event handler for the entire page so the page is ready to go as soon as the HTML has finished parsing. There is no second “boot up”.
Upon interaction the event handler refers to the serialized framework state to download the required application code and “resume” that part of the application with the associated application and framework state.
Additional parts of the application are activated in the same manner only as needed.
@sebmarkbage@BenLesh@QwikDev We prefetch all interactive elements by default immediately on page load, so they are available immediately when needing to run, just don’t execute (and is not the full component but a subset of components and their code that is needed)
Yeah the reason I didn't focus too much on resumable hydration here is I don't think true resumable hydration exists yet. Qwik does succeed at doing out of order hydration in that you can hydrate the child before the parent. And with that partitioning it is possible to assume that data initializes with initial state from the server at a component level. You have the inputs and you have the outputs, so you can confidently just not do the re-render of that component in the browser at hydration time and only attach the event handlers, or any defined side effects.
However, the reason I don't consider this true resumable hydration is that when any data would update for this component it needs to re-run and redo unrelated work that may have already been done on the server. It's like the motivation for the React forget compiler example if you saw that.. if you have some state that has nothing to do with a list you are rendering and you update the state the list re-renders. This is unsurprising. Picture if that unrelated work triggered an async data request etc..
VDOM based solutions have some challenges with resumable hydration. Qwik's approach is more like skipping hydration and then doing the work when you interact with it. Resumable Hydration should be able to work not only skipping unnecessary hydration but be able to not redo computations and derivations in the browser after the fact when unrelated data changes. This is possible if you can slice up your component logic the other way more similar to the fine-grained graphs that Solid produces. And you are probably unsurprised to hear this is exactly what we are working on for Marko 6.
I am playing with Qwik right now and seeing just how effective their resumable approach is, because I do want to confirm my understanding is correct.
UPDATE: Talking to Misko and the team Qwik is already working on a solution for precisely the concern that I have. They have a runtime based reactive system similar to Solid and are serializing the dependencies at runtime on the server with some new primitives that resemble useEffect and useMemo. When this lands they should be able to do the type of fine-grained resumability I'm talking about.
I don't think true resumable hydration exists yet.
Given your article's introduction the characterization of "replayable hydration" (for the current state of SSR) versus the hypothetical ideal of "resumable hydration" seemed useful.
I had a most excellent chat with Ryan offline and wanted to share my thoughts here, which boil down to "Qwik is truely resumable." Maybe not right this moment, as we have a few more things to implement, but we have designs for all of these.
To be truly resumable, you need to have a few things:
As Ryan pointed out, you need to attach to existing components out of order.
You need to serialize the whole state of the application.
You need to understand which component knows about what state. (Subscriptions)
If you don't have #3, then once the data changes, you don't have a choice but to re-render the whole app because you don't know which components are invalidated and need to be re-rendered. This forces you to download the whole app, and that is expensive.
But Qwik does know the answer to #3. Though Proxys Qwik knows which component cares about which data, Qwik serializes all of this information to the DOM. Then the data gets mutated Qwik can run querySelectorAll on HTML, which will tell it which components are subscribed to the data and hence have to be re-rendered. This is significant because it allows most of the components to stay in their unhydrated state for the duration of the app's life.
I think of this problem as layers in onion:
Most obvious is how to serialize listeners. (If you can't do that, than you have to rehydrate)
How do I render components out of order (If you can't do that that you will be forced to render parent/children)
Know which component is invalidated on state change (If you can't do that, you will be forced to re-render the whole app)
Have ways to watch data without running the code (useEffect equivalent) (If you don't have that you will have to eagerly download code)
Have ways to deal with data that is not Serializable. (if you can't do that, than the kinds of apps you can build will be limited)
On each of the levels, there are interesting problems to solve. Qwik's aim is to solve all of them, and so far, we have been able to do that and have plans for the reminder, which is consistent with Qwik's mental model.
This is why I think that Qwik is (will be) truly resumable.
I appreciate both yours & Ryan's work so this thread is gold. Thank you for listing out the hydration scenarios. Here is my concern as an app & library developer. I really like isomorphic JS & am building around that concept. Up till now isojs hydration has been a black box. Making the isojs hydration process more transparent & even programmable will be appreciated.
I would like to learn more about your plans re: step 4 & 5. Also, do you forsee Qwik or its concepts as being compatible with Solid & other isojs component libraries?
I love Misko's framing here as 4/5 are big part of what we are working on with Marko as well. 4 is a fairly natural extension of 3 in the same way Solid's granular reactivity is over say MobX + VDOM. The way to do this involves serializing the dependencies, then you don't need to run it once. In Marko's case it is a compiler that detects these similar to Svelte, except Marko's works cross file and traces dependencies even hoisted. For Qwik it's runtime similar to Solid, just that if it runs on the server you know the dependencies from the last run and can continue only when one updates. The way they serialize them is simply to take the key off their shared proxy store that all components use (that manages the Dependency injection).
I do want to talk to Misko more about this because there are different types of scenarios. Derivations definitely want to run on the server and be treated this way but effects in most frameworks are also a way to do client only code that you don't run on the server, like include a jQuery chart. In those cases you'd want the effect to run at hydration time so it isn't really a problem since you don't need it to run on the server to collect dependencies. In Marko we make that deliberation that it is effects and event handlers that run at hydration time. The interesting piece for Qwik is do they code split on this too putting the effects in with the event handlers and separate from the other component code.. once you follow this thinking Qwik may actually approach closer to how Marko 6 is breaking up code.
For Marko we actually split it from input/props through Reactive graph so a component consists of many different fine-grained pieces. Which is interesting since instead of things being component level the parent can know based on whether it passes static or reactive data to even import the dynamic child part to run it. And since we know exactly what can be stateful we can eliminate whole subtrees through the component structure.
I'm excited to talk more about this because the implications get pretty interesting. Because even when things are stateful instead of serializing everything we can optimize it further to serialize things based on the leaf nodes of the reactive graph. It's only things read from an DOM, event handler or effect directly that need to be serialized. With one except reactive convergences need each source branch serialized as well so one side can update without needing to run again. In a sense I've shown how Marko today can only serialize data that is used in components based on what is passed in to top level components input/props. With this approach we'd get field level serialization of only what is actually used/usable. Because of convergence we know that and in other cases we know that it would be impossible change what data we are seeing without refetching (ie triggering an async source) so we can be confident we don't need to have it in the browser initially.
Of course this all comes down to what is serializable. I'm interested to learn more about Qwiks approach. With Marko since the compiler sets the boundaries we can always step up one level with serialization if it isn't serializable as long as all root sources are. But we are working on techniques to serialize typically unserializable things like promises or functions and closures.
For further actions, you may consider blocking this person and/or reporting abuse
We're a place where coders share, stay up-to-date and grow their careers.
Whatever happens with Qwik in the long term, it added important new terminology—"replayable SSR" versus "resumable SSR".
I would go as far as saying that it identified the core limitation of contemporary client side frameworks—SSR reliant on "replayability" which is a consequence of their client side centric design (as opposed to a design that is distributed from the beginning).
Dynamic server rendering pretty much follows this pattern:
This pattern is pretty much guided by doing as much work as possible on the server to minimize the work needed on the client (and taking maximum advantage of the browser's built-in capabilities) which makes sense in a distributed environment where
Extending this to a distributed application leads to resumable server rendering:
The key here is that the "pre-rendered initial application state" isn't just "initial data" that needs to be "played out" inside the application but is the actual internal application state—so the application is ready to go once it's dropped in. Not only is the display shipped as pre-rendered HTML but also the application state is pre-rendered.
Now Qwik adds some other tricks like fragmenting interactivity to the smallest possible granularity in order to aggressively optimize TTI but the fundamental shift in thinking is moving from today's "replayable SSR" to tommorrow's "resumable SSR" (pre-rendered, joinable application state).
It's my sense that "resumable SSR" is going to force a major rethink in terms of client side application architecture (how it's partitioned) which may require significant tooling to get to the necessary level of DX that would ease its adoption.
If I remember correctly Qwik initially was trying to store application state in the HTML but I think they have abandoned that idea.
No we have not abandoned it all. We have just moved it to the bottom of HTML for several reasons:
Sorry, I should have been more precise. Back in August
Qwik: the answer to optimal fine-grained lazy loading
Miško Hevery for Builder.io ・ Aug 2 '21 ・ 7 min read
the state seemed to be stored directly in HTML element attributes.
By December it consolidated into a
qwik/json
script block containing "flattened JSON with reference keys" (with some reference keys being used as HTML element attribute values).Yes that is correct, we have moved it to a consolidated place.
I'm having trouble understanding the difference between 'initial state' vs 'actual internal application state'. If the point is to have the app take over once the initial app is rendered on the client, then isn't initial state, the only state there is? Can you give an example of a difference?
From Static to Interactive: Why Resumability is the Best Alternative to Hydration and Hydration is Pure Overhead get into this in detail.
Perhaps replace “initial state” with “initial data”.
Hydration:
Resumability:
Yeah the reason I didn't focus too much on resumable hydration here is I don't think true resumable hydration exists yet. Qwik does succeed at doing out of order hydration in that you can hydrate the child before the parent. And with that partitioning it is possible to assume that data initializes with initial state from the server at a component level. You have the inputs and you have the outputs, so you can confidently just not do the re-render of that component in the browser at hydration time and only attach the event handlers, or any defined side effects.
However, the reason I don't consider this true resumable hydration is that when any data would update for this component it needs to re-run and redo unrelated work that may have already been done on the server. It's like the motivation for the React forget compiler example if you saw that.. if you have some state that has nothing to do with a list you are rendering and you update the state the list re-renders. This is unsurprising. Picture if that unrelated work triggered an async data request etc..
VDOM based solutions have some challenges with resumable hydration. Qwik's approach is more like skipping hydration and then doing the work when you interact with it. Resumable Hydration should be able to work not only skipping unnecessary hydration but be able to not redo computations and derivations in the browser after the fact when unrelated data changes. This is possible if you can slice up your component logic the other way more similar to the fine-grained graphs that Solid produces. And you are probably unsurprised to hear this is exactly what we are working on for Marko 6.
I am playing with Qwik right now and seeing just how effective their resumable approach is, because I do want to confirm my understanding is correct.
UPDATE: Talking to Misko and the team Qwik is already working on a solution for precisely the concern that I have. They have a runtime based reactive system similar to Solid and are serializing the dependencies at runtime on the server with some new primitives that resemble
useEffect
anduseMemo
. When this lands they should be able to do the type of fine-grained resumability I'm talking about.Given your article's introduction the characterization of "replayable hydration" (for the current state of SSR) versus the hypothetical ideal of "resumable hydration" seemed useful.
I think it just did, with Qwik 1.0 just having been released recently.
I had a most excellent chat with Ryan offline and wanted to share my thoughts here, which boil down to "Qwik is truely resumable." Maybe not right this moment, as we have a few more things to implement, but we have designs for all of these.
To be truly resumable, you need to have a few things:
If you don't have #3, then once the data changes, you don't have a choice but to re-render the whole app because you don't know which components are invalidated and need to be re-rendered. This forces you to download the whole app, and that is expensive.
But Qwik does know the answer to #3. Though Proxys Qwik knows which component cares about which data, Qwik serializes all of this information to the DOM. Then the data gets mutated Qwik can run
querySelectorAll
on HTML, which will tell it which components are subscribed to the data and hence have to be re-rendered. This is significant because it allows most of the components to stay in their unhydrated state for the duration of the app's life.I think of this problem as layers in onion:
useEffect
equivalent) (If you don't have that you will have to eagerly download code)On each of the levels, there are interesting problems to solve. Qwik's aim is to solve all of them, and so far, we have been able to do that and have plans for the reminder, which is consistent with Qwik's mental model.
This is why I think that Qwik is (will be) truly resumable.
I appreciate both yours & Ryan's work so this thread is gold. Thank you for listing out the hydration scenarios. Here is my concern as an app & library developer. I really like isomorphic JS & am building around that concept. Up till now isojs hydration has been a black box. Making the isojs hydration process more transparent & even programmable will be appreciated.
I would like to learn more about your plans re: step 4 & 5. Also, do you forsee Qwik or its concepts as being compatible with Solid & other isojs component libraries?
I love Misko's framing here as 4/5 are big part of what we are working on with Marko as well. 4 is a fairly natural extension of 3 in the same way Solid's granular reactivity is over say MobX + VDOM. The way to do this involves serializing the dependencies, then you don't need to run it once. In Marko's case it is a compiler that detects these similar to Svelte, except Marko's works cross file and traces dependencies even hoisted. For Qwik it's runtime similar to Solid, just that if it runs on the server you know the dependencies from the last run and can continue only when one updates. The way they serialize them is simply to take the key off their shared proxy store that all components use (that manages the Dependency injection).
I do want to talk to Misko more about this because there are different types of scenarios. Derivations definitely want to run on the server and be treated this way but effects in most frameworks are also a way to do client only code that you don't run on the server, like include a jQuery chart. In those cases you'd want the effect to run at hydration time so it isn't really a problem since you don't need it to run on the server to collect dependencies. In Marko we make that deliberation that it is effects and event handlers that run at hydration time. The interesting piece for Qwik is do they code split on this too putting the effects in with the event handlers and separate from the other component code.. once you follow this thinking Qwik may actually approach closer to how Marko 6 is breaking up code.
For Marko we actually split it from input/props through Reactive graph so a component consists of many different fine-grained pieces. Which is interesting since instead of things being component level the parent can know based on whether it passes static or reactive data to even import the dynamic child part to run it. And since we know exactly what can be stateful we can eliminate whole subtrees through the component structure.
I'm excited to talk more about this because the implications get pretty interesting. Because even when things are stateful instead of serializing everything we can optimize it further to serialize things based on the leaf nodes of the reactive graph. It's only things read from an DOM, event handler or effect directly that need to be serialized. With one except reactive convergences need each source branch serialized as well so one side can update without needing to run again. In a sense I've shown how Marko today can only serialize data that is used in components based on what is passed in to top level components input/props. With this approach we'd get field level serialization of only what is actually used/usable. Because of convergence we know that and in other cases we know that it would be impossible change what data we are seeing without refetching (ie triggering an async source) so we can be confident we don't need to have it in the browser initially.
Of course this all comes down to what is serializable. I'm interested to learn more about Qwiks approach. With Marko since the compiler sets the boundaries we can always step up one level with serialization if it isn't serializable as long as all root sources are. But we are working on techniques to serialize typically unserializable things like promises or functions and closures.