Hacker News new | ask | show | jobs
by chasd00 1526 days ago
This is confusing to me. From the article it sounds like the javascript is run on the server producing markup. That markup is sent to the browser for rendering then the javascript is requested by the browser. When the javascript arrives it is run again on the browser to re-generate the DOM with event handlers attached. If that is correct then why is the javascript run on the server to begin with and not just sent directly to the browser?

Is the idea to take advantage of the server's horsepower to get something on the screen fast by sending pre-rendered HTML and then wait while the browser runs the code to basically re-create the page for interactivity?

(it's been a while since i've done traditional front-end web dev)

9 comments

This post is part of a broader effort among emerging js frameworks to send less code down to the browser. It's not just bandwidth. The time to parse, compile, and execute the JS can take as long on a phone as the download itself

For a decent fraction of applications a large chunk of the code that's written is to generate non-interactive parts of the app (data fetching, wrappers, component markup, CSS) and the actual code for event handling is relatively small. Hacker News, for example, is a pretty common framework demo since it's simple to write. You need a bit of JS on the comments page for the vote buttons and the comment collapse but if you write HN in a natural way in most component oriented frameworks the bundle coming down is considerably larger and includes all the code for rendering the comments (for example) even though the comments never change and most hydration approaches will put the comments in both the markup and embed a JSON/JS chunk in the page so hydration and the client side render can happen with consistent data. This doubles the size of the page with the overhead of the data half running through some subset of the JS code machinery.

In the case of Qwik, the idea is to do a server side render and then lazy load everything on the client as it's needed. At least that's my understanding, I haven't used the framework beyond a toy project. There are other approaches but to use the HN example, you'd never download the comment collapsing code if you didn't click a collapse button.

You could avoid shipping down the JSX templates but only by promising to never render a comment on the client. That's basically the approach taken by this React Server Components proposal: https://github.com/josephsavona/rfcs/blob/server-components/...

Rails has experimented with this sort of thing in the past; it requires some deep integration with the HTTP server so that clients can request updates to specific chunks of the UI rather than just URLs.

Yeah RSCs are another approach I'd say that are playing with these sort of approaches. Other notables include:

Marko: https://www.markojs.com Astro: https://astro.build

Oh my, this is what we did ~15years ago with PHP and jQuery, I was hoping no more. We really did a full circle.
I've written quite a bit of PHP and jQuery but with that approach there's a decision when you consider writing the code if you're going to implement the feature in PHP or in jQuery. The difference with the upcoming generation of JS frameworks is that the server and client are in the same language so the decision can be deferred or it can be determined automatically.

Astro, for example, is very conceptually close to PHP. Your code runs on the server, there's a code section where you grab content from the DB and a markup section where you template out the page, the lifetime of everything is one request, etc. What's changed is that you can put an attr on a component indicating you want that component sent to the client. No attr, it's old PHP, attr it's jQuery, and further the send can be triggered can be when the element enters the viewport or when it's clicked.

In the in-development Marko 6 runtime distinguishes between url/db derived "settled" data and client side/event handler state and only sends the latter to the client. If you have a paged list (for example) you can set up the prev/next page buttons to be links and you wouldn't have any JS sent down to the client despite the app being written with current SPA component ergonomics. If you'd rather have the data fetch and render client side you switch the data declaration from a const (dervied/settled) to a let (state) and all the rendering JS gets sent down.

I'm simplifying/omitting stuff but these are new and useful takes on old ideas. I think better takes on old ideas are some of the more effective advances in the state of the art.

Full circle because it is better, you avoid the unnecessary moving of state, however it is of course zero benefit of using react at this point.
Generally speaking, if you approach qwik.js from a 10,000 mile view like you'd normally approach other frameworks, you're going to miss the trees for the forest. One of the axioms where this framework is coming from is the idea of providing React-like developer experience, so yes, there's going to be templates expressed in JS.

Where it gets technical has to do with how the JS gets delivered to the browser. It starts with a small bootstrapping that sets up event delegation, sort of like `document.documentElement.addEventListener('click', becomeAwareOfClicks)`. This happens literally on the first chunk of HTML being streamed in, meaning that the framework is now already aware of clicks happening anywhere in the app, even before HTML <body> is available in JS.

Eventually some HTML will stream in with an attribute that indicates how a click event on an element should be handled. The framework can then quickly determine whether it needs to "hydrate" that event handler: a) if the event was captured by `becomeAwareOfClicks` and b) the intersection observer deems that element is visible and available to DOM manipulation, then c) it can download the relevant handler, meaning it triggers business logic at latest as soon as the intersection observer downloads the handler.

Note that at this point, no other JS has downloaded yet. Eventually it does download it, like every other framework, but the key point is that it can respond to events with actual business logic before the rest of the JS comes down the pipe.

What you’ve described is a complete and accurate description of the “hydration” approach which the article (also correctly) describes as pure overhead. The approach proposed in the article/implemented in Qwik dispenses with re-running the JS which already ran on the server and just carries on with the state it produced.

The SSR/hydration approach:

- Benefits SEO

- Usually benefits initial content on the screen

- Has a bunch of performance penalties after that as JS is loaded, parsed, and (usually blocking) recreates the data structure representing its initial state

Qwik’s approach (at least conceptually) skips that third point for everything until an interaction needs to respond to some state change based on an interaction, and does that with only the information needed for that event.

In a tweet thread, I described my mental model of this as effectively developing like I’m building an app where all the server/client UI code is shared, but the UX is like I wrote plain HTML with some jQuery or whatever to make a few elements interactive. What Qwik does is determine which parts of the server code need to be the compiled jQueryish code, but only calls it when needed.

An equivalent React codebase would re-render the entire page (well mount point) even if most of it will have no meaningful effect.

I think most importantly the server can cache requests (cache the generated HTML). This is especially important for public, mostly static pages that one might want to do SEO optimizations for anyways.

Servers are often weaker than many consumer computers anyways, so I don’t thin it’s because it can render faster than your own browser.

>Servers are often weaker than many consumer computers anyways, so I don’t thin it’s because it can render faster than your own browser.

Isn't this the other way around? I mean a server is supposed to be fast enough to handle a lot of requests.

An individual server is more powerful than one client, but your client computing power multiplies by your user base.

Same reason DDoS attacks are generally more powerful than DoS attacks.

Power in numbers.

Well, servers often come with more but slower cores.
But servers have a much higher power budget compared to phones or most laptops. That's supposedly the target user base for SSR + hydration.
I don't think the purpose of SSR + hydration is to simply move the wait time from first render to server response, or that servers can somehow render faster. To fully yield the benefits you'd have to enable caching, so that the server spits out something without rendering and the client side can simply hydrate.

Caching is not something individual distributed clients can do, which is why the server is the only way able to reasonably take on this role. You can also easily configure nginx to serve just-in-time caching.

>Servers are often weaker than many consumer computers anyways, so I don’t thin it’s because it can render faster than your own browser.

I'm thinking about that Hacker News article, which had a laptop in the datacenter, because it was much faster on it.

It is correct, except I think the browser generates the DOM from the HTML, and the JS just attaches event handlers to it.

The reason for the double work is that the context here is SSR/SSG:

«The re-execution of code on the client that the server already executed as part of SSR/SSG is what makes hydration pure overhead: that is, a duplication of work by the client that the server already did.»

In client-side rendering (CSR), there isn’t double work in rendering the HTML, since the client only does it.

Yes, the idea is to send pre-rendered HTML (by using SSR/SSG). The client doesn’t need to re-create the page/HTML, simply hydrate it (attach event handlers). But it is duplicate work and turns out to be a bit expensive to always do it on the initial load.

Hope that helps to clarify.

> If that is correct then why is the javascript run on the server to begin with and not just sent directly to the browser?

Just the same ordinary reasons to generate HTML on the server: it allows you to support HTTP caching (in a CDN or even browser caching), it (potentially) lets the browser start rendering content much sooner, and it will be viewable by user agents (bots, scrapers, search engines, etc., but also humans) that don't run JavaScript.

that makes sense, so if you're using http caching appropriately the server side "render" or running of the javascript happens on the first visit and then infrequently thereafter.
One reason I like SSR is that you need some form of SSR for public-facing websites anyway. Website previews (like in iMessage, Twitter, etc.) rely on Open Graph tags in the HTML, and these services expect the OG tags to be available without executing any JavaScript. Since you already need this step, you can make loading pages much faster if you inline any data you might have fetched from the client at view time.
Just one side note here you could also just use SSG, and never need to hit a server unless you rebuild the page in some fashion
This doesn't work if the content is coming from a dynamic source like a database.
You get to at least see the content much faster, it's just not interactive for a bit. The benefit is clearest when rendering pages at build time and then serving the HTML over a CDN

But even then, it's definitely still a problem vs sites that don't have to be hydrated. Some people see this approach as a best-of-both-worlds, but in reality it's still a compromise that has costs

> When the javascript arrives it is run again on the browser to re-generate the DOM with event handlers attached.

Not the DOM, but the virtual DOM. Instead of inserting elements (which is expensive), it can just walk the existing server-rendered DOM and attach listeners.