Our underlying transport is not exactly JSON but something like "JSON with holes". It's basically a JSON with placeholder string values like "$1", "$2", that later get filled in the future rows. This lets us have breadth-first streaming where we can show some content as early as possible, but always have intentional loading states.
The choice of JSON is because we need to be able to
* Pass component props (which are JSON) to Client Components
* Reconcile the tree on refetches so that state inside isn't thrown away
HTML doesn't let us have either of these two. However, starting with a richer format, and then converting it to HTML for initial render, works.
Since we can also do this at the build time, you could build a website that does all of this work at the build time, and turns it into HTML. However, as you might expect, it won't be interactive without JS.