diff --git a/blog/async-render-in-web-components.md b/blog/async-render-in-web-components.md index 288095fe..679bdc37 100644 --- a/blog/async-render-in-web-components.md +++ b/blog/async-render-in-web-components.md @@ -23,7 +23,7 @@ class UserProfile extends WebComponent({ uid: String }) { UserProfile.register('user-profile'); ``` -That is it. No effect, no loading flag, no spinner, no `this.setState`. `render()` is allowed to be `async`. Writing `await` makes the function return a promise by the normal JavaScript rule, and every render path in WebJs already awaits a promise-returning `render()` automatically. There is no flag to set. A plain synchronous `render()` stays the zero-cost default, so you only pay for async where you actually reach for it. This is issue #469 if you want to read the history. +That is it. No effect, no loading flag, no spinner, no `this.setState`. `render()` is allowed to be `async`. Writing `await` makes the function return a promise by the normal JavaScript rule, and every render path in WebJs already awaits a promise-returning `render()` automatically. There is no flag to set. A plain synchronous `render()` stays the zero-cost default, so you only pay for async where you actually reach for it. # Why this is not just useEffect with nicer syntax @@ -74,9 +74,9 @@ That co-location is the other quiet win. The fetch lives in the leaf component t Two optimizations make this cheap, and both are on by default. -A display-only async component gets **elided** (#474). If a component just fetches and renders, with no click handlers, no signals, no interactivity of any kind, then its server-rendered HTML is already the complete and final output. So WebJs drops its JavaScript module from the page entirely. The data is in the HTML, the component has nothing left to do in the browser, so nothing ships. A docs page or a content card built this way costs zero client JavaScript. +A display-only async component gets **elided**. If a component just fetches and renders, with no click handlers, no signals, no interactivity of any kind, then its server-rendered HTML is already the complete and final output. So WebJs drops its JavaScript module from the page entirely. The data is in the HTML, the component has nothing left to do in the browser, so nothing ships. A docs page or a content card built this way costs zero client JavaScript. -For a component that DOES ship (because it is also interactive), **SSR action seeding** (#472) kills the redundant re-fetch. Every `'use server'` action result computed during SSR is serialized into the page. When the component hydrates in the browser, its first RPC call reads that seed instead of hitting the network. So `getUser(this.uid)` runs once, on the server, and the client reuses the result on its first render. No hydration flicker, no wasted round trip. A later re-fetch or an argument change still goes to the server as normal. +For a component that DOES ship (because it is also interactive), **SSR action seeding** kills the redundant re-fetch. Every `'use server'` action result computed during SSR is serialized into the page. When the component hydrates in the browser, its first RPC call reads that seed instead of hitting the network. So `getUser(this.uid)` runs once, on the server, and the client reuses the result on its first render. No hydration flicker, no wasted round trip. A later re-fetch or an argument change still goes to the server as normal. # When to reach for something else diff --git a/blog/bun-serve-vs-node-http.md b/blog/bun-serve-vs-node-http.md index 6beea82b..d9f62076 100644 --- a/blog/bun-serve-vs-node-http.md +++ b/blog/bun-serve-vs-node-http.md @@ -49,7 +49,7 @@ That is the entire tradeoff. You give up one preload-timing feature and you get The listener choice is the headline, but a buildless server has to earn throughput in the small places too, because there is no build step ahead of time to absorb overhead. So the Bun request path got its own passes. -Brotli compression on the Bun listener runs through `node:zlib` (#518), so a Bun-served response gets the same compression a Node-served one does, no native gap there. And the per-request overhead got trimmed directly (#773): one pass removed a full request-object clone that existed only to stamp the client's IP address onto every request, and another removed an extra stream hop that every compressed response was being bridged through. Neither was large alone, but they are per-request costs, so they multiply by your traffic. On the hot path, a clone you do not need and a stream hop you can collapse are exactly the kind of thing worth cutting by hand. +Brotli compression on the Bun listener runs through `node:zlib`, so a Bun-served response gets the same compression a Node-served one does, no native gap there. And the per-request overhead got trimmed directly: one pass removed a full request-object clone that existed only to stamp the client's IP address onto every request, and another removed an extra stream hop that every compressed response was being bridged through. Neither was large alone, but they are per-request costs, so they multiply by your traffic. On the hot path, a clone you do not need and a stream hop you can collapse are exactly the kind of thing worth cutting by hand. # You pick the runtime diff --git a/blog/component-suspense-streaming.md b/blog/component-suspense-streaming.md index 3038101b..f091b83c 100644 --- a/blog/component-suspense-streaming.md +++ b/blog/component-suspense-streaming.md @@ -76,4 +76,4 @@ One implementation note for the curious. A streamed component that uses `static # The takeaway -WebJs blocks SSR by default so your data is in the first paint, which is the right call for fast queries and keeps the page working without JavaScript. For the one slow panel that would otherwise stall the whole first byte, wrap it in ``. The fallback flushes immediately, the slow content streams in when it resolves, multiple boundaries fetch concurrently with no server waterfall, a throwing component stays isolated, and it all works on soft navigation too. It is plain HTML streaming, no RSC and no Flight, so the browser's own parser carries it. Fast first byte for slow data, without giving up progressive enhancement. This shipped in #470 and #471. +WebJs blocks SSR by default so your data is in the first paint, which is the right call for fast queries and keeps the page working without JavaScript. For the one slow panel that would otherwise stall the whole first byte, wrap it in ``. The fallback flushes immediately, the slow content streams in when it resolves, multiple boundaries fetch concurrently with no server waterfall, a throwing component stays isolated, and it all works on soft navigation too. It is plain HTML streaming, no RSC and no Flight, so the browser's own parser carries it. Fast first byte for slow data, without giving up progressive enhancement. diff --git a/blog/full-stack-type-safety-no-build.md b/blog/full-stack-type-safety-no-build.md index eee6c82a..ad379276 100644 --- a/blog/full-stack-type-safety-no-build.md +++ b/blog/full-stack-type-safety-no-build.md @@ -71,7 +71,7 @@ This is why the type safety is honest and not just optimistic. The type says `Da Server actions are the headline, but the type flow reaches the routing layer as well. -Run `webjs types` and the framework generates `.webjs/routes.d.ts`, an overlay with one entry per route in `app/`. It gives you a typed `Route` union (`navigate('/blog/anything')` is fine, `navigate('/nonexistent')` is a type error) plus per-route params. `webjs dev` regenerates it on startup and after each route rebuild, so the editor always has fresh route types (this is #258, WebJs's no-build answer to Next 15's `typedRoutes`, done through interface declaration-merging instead of a bundler). +Run `webjs types` and the framework generates `.webjs/routes.d.ts`, an overlay with one entry per route in `app/`. It gives you a typed `Route` union (`navigate('/blog/anything')` is fine, `navigate('/nonexistent')` is a type error) plus per-route params. `webjs dev` regenerates it on startup and after each route rebuild, so the editor always has fresh route types (WebJs's no-build answer to Next 15's `typedRoutes`, done through interface declaration-merging instead of a bundler). Three exported helper types read that union. `PageProps` types a page's `{ params, searchParams, url, actionData }`, `LayoutProps` adds `children`, and `RouteHandlerContext` types a `route.ts` handler's second argument. Pass the route literal and `params` narrows to the exact shape. diff --git a/blog/import-only-pages-zero-js.md b/blog/import-only-pages-zero-js.md index 20945500..36d14f52 100644 --- a/blog/import-only-pages-zero-js.md +++ b/blog/import-only-pages-zero-js.md @@ -23,7 +23,7 @@ So a page's own module usually has nothing to do on the client. It rendered its # What "import-only" means -Here is the subtle case, and the one this post is really about. It landed in #605 and #609. +Here is the subtle case, and the one this post is really about. A page rarely sits there doing nothing. It imports things, in particular the interactive components it wants to render: a ``, a ``, a ``. Those components DO hydrate, so their modules genuinely need to reach the browser. @@ -48,10 +48,10 @@ That last one is sneaky, because a class-name helper looks innocent. But if it r The rule of thumb is short. `page.ts` and `layout.ts` should never appear in the network tab or in the boot `