@funstack/static 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -3
- package/dist/bin/skill-installer.d.mts +1 -0
- package/dist/bin/skill-installer.mjs +12 -0
- package/dist/bin/skill-installer.mjs.map +1 -0
- package/dist/build/buildApp.mjs +4 -3
- package/dist/build/buildApp.mjs.map +1 -1
- package/dist/build/rscProcessor.mjs +9 -3
- package/dist/build/rscProcessor.mjs.map +1 -1
- package/dist/client/entry.d.mts +1 -0
- package/dist/client/entry.mjs +17 -9
- package/dist/client/entry.mjs.map +1 -1
- package/dist/client/globals.mjs.map +1 -1
- package/dist/docs/FAQ.md +5 -0
- package/dist/docs/GettingStarted.md +180 -0
- package/dist/docs/MigratingFromViteSPA.md +321 -0
- package/dist/docs/api/Defer.md +110 -0
- package/dist/docs/api/FunstackStatic.md +184 -0
- package/dist/docs/index.md +22 -0
- package/dist/docs/learn/HowItWorks.md +109 -0
- package/dist/docs/learn/LazyServerComponents.md +120 -0
- package/dist/docs/learn/OptimizingPayloads.md +107 -0
- package/dist/docs/learn/RSC.md +179 -0
- package/dist/docs/learn/SSR.md +104 -0
- package/dist/entries/client.d.mts +1 -1
- package/dist/entries/rsc-client.d.mts +2 -2
- package/dist/entries/rsc-client.mjs +2 -2
- package/dist/entries/server.d.mts +2 -2
- package/dist/plugin/index.d.mts +17 -1
- package/dist/plugin/index.d.mts.map +1 -1
- package/dist/plugin/index.mjs +10 -1
- package/dist/plugin/index.mjs.map +1 -1
- package/dist/rsc/defer.d.mts +18 -3
- package/dist/rsc/defer.d.mts.map +1 -1
- package/dist/rsc/defer.mjs +34 -14
- package/dist/rsc/defer.mjs.map +1 -1
- package/dist/rsc/entry.d.mts.map +1 -1
- package/dist/rsc/entry.mjs +85 -20
- package/dist/rsc/entry.mjs.map +1 -1
- package/dist/rsc-client/clientWrapper.d.mts +3 -3
- package/dist/rsc-client/clientWrapper.d.mts.map +1 -1
- package/dist/rsc-client/clientWrapper.mjs +2 -2
- package/dist/rsc-client/clientWrapper.mjs.map +1 -1
- package/dist/rsc-client/entry.d.mts +1 -1
- package/dist/rsc-client/entry.mjs +1 -1
- package/dist/ssr/entry.d.mts +2 -0
- package/dist/ssr/entry.d.mts.map +1 -1
- package/dist/ssr/entry.mjs +6 -2
- package/dist/ssr/entry.mjs.map +1 -1
- package/package.json +26 -11
- package/skills/funstack-static-knowledge/SKILL.md +44 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# How It Works
|
|
2
|
+
|
|
3
|
+
FUNSTACK Static is a React framework that leverages React Server Components (RSC) to build a fully static Single Page Application. The result is a set of files that can be deployed to **any static file hosting service** - no server required at runtime.
|
|
4
|
+
|
|
5
|
+
FUNSTACK Static is built on top of [@vitejs/plugin-rsc](https://www.npmjs.com/package/@vitejs/plugin-rsc) for its RSC support.
|
|
6
|
+
|
|
7
|
+
## Architecture
|
|
8
|
+
|
|
9
|
+
FUNSTACK Static applications have **a single entrypoint** which is a server component. This component is responsible for rendering the entire application.
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
// src/App.tsx
|
|
13
|
+
export default function App() {
|
|
14
|
+
return (
|
|
15
|
+
<div>
|
|
16
|
+
<h1>Welcome to my FUNSTACK Static app!</h1>
|
|
17
|
+
{/* Your app components go here */}
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Currently, no routing solution is built in. If you need routing, you can use your favorite routing library for SPAs, such as [React Router](https://reactrouter.com/) (SPA mode) or [FUNSTACK Router](https://github.com/uhyo/funstack-router).
|
|
24
|
+
|
|
25
|
+
Server components cannot have any client-side interactivity. To add interactivity, you can use client components. FUNSTACK Static follows [the standard React Server Components conventions](https://react.dev/reference/rsc/server-components#adding-interactivity-to-server-components) (`"use client"`) for defining client components.
|
|
26
|
+
|
|
27
|
+
Behind the scenes, server components are rendered into **RSC payloads** at build time (or inside the development server in development mode). These payloads are then loaded by the client-side React application to reflect the server-rendered content to the DOM.
|
|
28
|
+
|
|
29
|
+
On production mode builds, FUNSTACK Static generates a set of static files that include:
|
|
30
|
+
|
|
31
|
+
- An `index.html` file that bootstraps the client-side React application
|
|
32
|
+
- JavaScript files for the client-side React application and its dependencies
|
|
33
|
+
- Asset files (CSS, images, etc.)
|
|
34
|
+
- RSC payload files generated from server components
|
|
35
|
+
|
|
36
|
+
Typically, the output structure looks like this:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
dist/public
|
|
40
|
+
├── assets
|
|
41
|
+
│ ├── app-91R2BjDJ.css
|
|
42
|
+
│ ├── app-CQU2Svmn.js
|
|
43
|
+
│ ├── app-ovZqc1Hu.css
|
|
44
|
+
│ ├── index-CCEDZan_.js
|
|
45
|
+
│ ├── root-DvE5ENz2.css
|
|
46
|
+
│ └── rsc-D0fjt5Ie.js
|
|
47
|
+
├── funstack__
|
|
48
|
+
│ └── fun:rsc-payload
|
|
49
|
+
│ └── db1923b9b6507ab4.txt
|
|
50
|
+
└── index.html
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The RSC payload files under `funstack__` are loaded by the client-side code to bootstrap the application with server-rendered content.
|
|
54
|
+
|
|
55
|
+
This can been seen as an **optimized version of traditional client-only SPAs**, where the entire application is bundled into JavaScript files. By using RSC, some of the rendering work is offloaded to the build time, resulting in smaller JavaScript bundles combined with RSC payloads that require less client-side processing (parsing is easier, no JavaScript execution needed).
|
|
56
|
+
|
|
57
|
+
## Root Entry Point
|
|
58
|
+
|
|
59
|
+
The root entry point of a FUNSTACK Static application is defined in the `funstack.config.js` file using the `root` option. This is a **special endpoint** that defines the HTML shell of your application. This is a separate endpoint from the main App component.
|
|
60
|
+
|
|
61
|
+
A typical Root component looks like this:
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
// src/Root.tsx
|
|
65
|
+
export default function Root({ children }: { children: React.ReactNode }) {
|
|
66
|
+
return (
|
|
67
|
+
<html lang="en">
|
|
68
|
+
<head>
|
|
69
|
+
<meta charSet="UTF-8" />
|
|
70
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
71
|
+
<title>My FUNSTACK Static App</title>
|
|
72
|
+
</head>
|
|
73
|
+
<body>{children}</body>
|
|
74
|
+
</html>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The `children` prop contains the main App component.
|
|
80
|
+
|
|
81
|
+
The Root component is special in two ways:
|
|
82
|
+
|
|
83
|
+
1. Rendered into a fully static HTML file (`index.html`), not an RSC payload.
|
|
84
|
+
2. Is **not hydrated** on the client side. This means that any client-side interactivity (e.g., event handlers) inside the Root component will not work.
|
|
85
|
+
|
|
86
|
+
The Root entrypoint is a FUNSTACK Static counterpart to the `index.html` file in traditional SPAs. It allows you to still leverage some of the benefits of server components for defining the HTML shell of your application.
|
|
87
|
+
|
|
88
|
+
## Server-Side Rendering
|
|
89
|
+
|
|
90
|
+
By default, FUNSTACK Static only renders the Root shell to HTML. The App component is rendered client-side from its RSC payload. This behavior keeps the initial HTML small and fast to deliver.
|
|
91
|
+
|
|
92
|
+
If you want the App component to also be rendered to HTML (for better SEO or perceived performance), you can enable the `ssr` option:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
funstackStatic({
|
|
96
|
+
root: "./src/root.tsx",
|
|
97
|
+
app: "./src/App.tsx",
|
|
98
|
+
ssr: true,
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
With `ssr: true`, the full page content is visible in the HTML before JavaScript loads. The client then hydrates the existing HTML instead of rendering from scratch.
|
|
103
|
+
|
|
104
|
+
Note that in both modes, React Server Components are still used - the `ssr` option only controls whether the App's HTML is pre-rendered at build time or rendered client-side.
|
|
105
|
+
|
|
106
|
+
## See Also
|
|
107
|
+
|
|
108
|
+
- [React Server Components](/funstack-static/learn/rsc) - Understanding RSC in depth
|
|
109
|
+
- [Getting Started](/funstack-static/getting-started) - Set up your first project
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Using lazy() in Server Components
|
|
2
|
+
|
|
3
|
+
React's `lazy()` API is typically associated with client-side code splitting. However, it can also be used in server environments to reduce the initial response time of the development server by deferring the work needed to compute your application.
|
|
4
|
+
|
|
5
|
+
## The Problem: Development Server Latency
|
|
6
|
+
|
|
7
|
+
When the development server receives a request, it needs to compute `<App />` to generate the response. This computation requires loading all imported modules, even those for contents that are not needed for the current request. For example, consider a routing setup like this:
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
// All these imports are loaded immediately
|
|
11
|
+
import HomePage from "./pages/Home";
|
|
12
|
+
import AboutPage from "./pages/About";
|
|
13
|
+
import DocsPage from "./pages/Docs";
|
|
14
|
+
import SettingsPage from "./pages/Settings";
|
|
15
|
+
// ... more page imports
|
|
16
|
+
|
|
17
|
+
const routes = [
|
|
18
|
+
route({ path: "/", component: defer(<HomePage />) }),
|
|
19
|
+
route({ path: "/about", component: defer(<AboutPage />) }),
|
|
20
|
+
// ... more routes
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export default function App() {
|
|
24
|
+
return <Router routes={routes} />;
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
In a large application with many routes, this upfront loading adds latency to the first request, even though only one route's component will actually render.
|
|
29
|
+
|
|
30
|
+
While use of [defer()](./optimizing-payloads) helps reducing initial load by deferring _rendering_ of route components, the work to _import_ those components still happens immediately.
|
|
31
|
+
|
|
32
|
+
## How lazy() Helps
|
|
33
|
+
|
|
34
|
+
`lazy()` defers the import of a module until the component is actually rendered. In a server environment, this means:
|
|
35
|
+
|
|
36
|
+
1. The initial `<App />` computation only loads the routing structure
|
|
37
|
+
2. Page components are loaded on-demand when their route matches
|
|
38
|
+
3. Unused route components are never loaded for that request
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import { lazy } from "react";
|
|
42
|
+
|
|
43
|
+
// These imports are deferred
|
|
44
|
+
const HomePage = lazy(() => import("./pages/Home"));
|
|
45
|
+
const AboutPage = lazy(() => import("./pages/About"));
|
|
46
|
+
const DocsPage = lazy(() => import("./pages/Docs"));
|
|
47
|
+
const SettingsPage = lazy(() => import("./pages/Settings"));
|
|
48
|
+
|
|
49
|
+
const routes = [
|
|
50
|
+
route({ path: "/", component: defer(<HomePage />) }),
|
|
51
|
+
route({ path: "/about", component: defer(<AboutPage />) }),
|
|
52
|
+
// ... more routes
|
|
53
|
+
];
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
When a user visits `/about`, only `AboutPage` is actually imported. The other page modules remain unloaded, reducing the work the server needs to do.
|
|
57
|
+
|
|
58
|
+
## Combining with `defer()`
|
|
59
|
+
|
|
60
|
+
To use `lazy()` effectively in server components, you should combine it with `defer()`.
|
|
61
|
+
|
|
62
|
+
Without `defer()`, the contents of the lazy-loaded components would be part of the initial RSC payload. This means the server would still need to fully render them before sending the response, negating the benefits of lazy loading.
|
|
63
|
+
|
|
64
|
+
## Example
|
|
65
|
+
|
|
66
|
+
Here's a complete example of using `lazy()` with FUNSTACK Router as a router library:
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
import { lazy, Suspense } from "react";
|
|
70
|
+
import { Outlet } from "@funstack/router";
|
|
71
|
+
import { route } from "@funstack/router/server";
|
|
72
|
+
import { defer } from "@funstack/static/server";
|
|
73
|
+
import { Layout } from "./components/Layout";
|
|
74
|
+
|
|
75
|
+
// Lazy load page components
|
|
76
|
+
const HomePage = lazy(() => import("./pages/Home"));
|
|
77
|
+
const AboutPage = lazy(() => import("./pages/About"));
|
|
78
|
+
const DocsPage = lazy(() => import("./pages/Docs"));
|
|
79
|
+
|
|
80
|
+
const routes = [
|
|
81
|
+
route({
|
|
82
|
+
path: "/",
|
|
83
|
+
component: (
|
|
84
|
+
<Layout>
|
|
85
|
+
<Outlet />
|
|
86
|
+
</Layout>
|
|
87
|
+
),
|
|
88
|
+
children: [
|
|
89
|
+
route({
|
|
90
|
+
path: "/",
|
|
91
|
+
component: defer(<HomePage />),
|
|
92
|
+
}),
|
|
93
|
+
route({
|
|
94
|
+
path: "/about",
|
|
95
|
+
component: defer(<AboutPage />),
|
|
96
|
+
}),
|
|
97
|
+
route({
|
|
98
|
+
path: "/docs",
|
|
99
|
+
component: defer(<DocsPage />),
|
|
100
|
+
}),
|
|
101
|
+
],
|
|
102
|
+
}),
|
|
103
|
+
];
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## When to Use This Pattern
|
|
107
|
+
|
|
108
|
+
This optimization is most beneficial when:
|
|
109
|
+
|
|
110
|
+
- **You have many routes** - The more unused routes you can skip loading, the bigger the win
|
|
111
|
+
- **Page components have heavy dependencies** - If a page imports large libraries, deferring that import saves significant work
|
|
112
|
+
- **Development server responsiveness matters** - This pattern primarily improves development experience; production builds pre-render everything anyway
|
|
113
|
+
|
|
114
|
+
For small applications with few routes, the overhead of `lazy()` may not be worth it. But as your application grows, lazy loading routes becomes increasingly valuable.
|
|
115
|
+
|
|
116
|
+
## See Also
|
|
117
|
+
|
|
118
|
+
- [Optimizing RSC Payloads](/funstack-static/learn/optimizing-payloads) - Using `defer()` to split RSC payloads
|
|
119
|
+
- [How It Works](/funstack-static/learn/how-it-works) - Overall FUNSTACK Static architecture
|
|
120
|
+
- [defer()](/funstack-static/api/defer) - API reference for the defer function
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Optimizing RSC Payloads
|
|
2
|
+
|
|
3
|
+
FUNSTACK Static uses React Server Components (RSC) to pre-render your application at build time. By default, all content is bundled into a single RSC payload. This page explains how to split that payload into smaller chunks for better loading performance.
|
|
4
|
+
|
|
5
|
+
## The Default Behavior
|
|
6
|
+
|
|
7
|
+
Without any optimization, your entire application is rendered into one RSC payload file:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
dist/public/funstack__/
|
|
11
|
+
└── fun:rsc-payload/
|
|
12
|
+
└── b62ec6668fd49300.txt ← Contains everything
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This means users must download the entire payload before seeing any content - even if they only need one page of a multi-page application.
|
|
16
|
+
|
|
17
|
+
**Note:** RSC payloads contain the rendering results of server components only; client components and their JavaScript bundles are handled separately. However, it is still important to optimize RSC payload sizes because the recommended best practice is to keep as much of your UI as server components as possible.
|
|
18
|
+
|
|
19
|
+
## Chunking with defer()
|
|
20
|
+
|
|
21
|
+
The `defer()` function lets you split your application into multiple RSC payloads. Each `defer()` call creates a separate payload file that loads on-demand when that component renders.
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { defer } from "@funstack/static/server";
|
|
25
|
+
|
|
26
|
+
// Instead of this:
|
|
27
|
+
<HeavyContent />
|
|
28
|
+
|
|
29
|
+
// Do this:
|
|
30
|
+
<Suspense fallback={<p>Loading...</p>}>
|
|
31
|
+
{defer(<HeavyContent />)}
|
|
32
|
+
</Suspense>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The content inside `defer()` is still rendered at build time, but it's stored in a separate file and fetched only when needed.
|
|
36
|
+
|
|
37
|
+
**Note:** use of `defer()` requires a `<Suspense>` boundary to handle the loading state while the payload is being fetched.
|
|
38
|
+
|
|
39
|
+
## Route-Level Optimization
|
|
40
|
+
|
|
41
|
+
The most impactful use of `defer()` is wrapping your route components. This ensures users only download the payload for the page they're viewing:
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { defer } from "@funstack/static/server";
|
|
45
|
+
import { route } from "@funstack/router/server";
|
|
46
|
+
import HomePage from "./pages/Home";
|
|
47
|
+
import AboutPage from "./pages/About";
|
|
48
|
+
import DocsPage from "./pages/Docs";
|
|
49
|
+
|
|
50
|
+
const routes = [
|
|
51
|
+
route({
|
|
52
|
+
path: "/",
|
|
53
|
+
component: defer(<HomePage />),
|
|
54
|
+
}),
|
|
55
|
+
route({
|
|
56
|
+
path: "/about",
|
|
57
|
+
component: defer(<AboutPage />),
|
|
58
|
+
}),
|
|
59
|
+
route({
|
|
60
|
+
path: "/docs",
|
|
61
|
+
component: defer(<DocsPage />),
|
|
62
|
+
}),
|
|
63
|
+
];
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
With this setup:
|
|
67
|
+
|
|
68
|
+
- Visiting `/` only downloads the Home page payload
|
|
69
|
+
- Navigating to `/about` fetches the About page payload on-demand
|
|
70
|
+
- Users see content faster because they're not waiting for pages they haven't visited
|
|
71
|
+
|
|
72
|
+
## Output Structure
|
|
73
|
+
|
|
74
|
+
After building with route-level `defer()`, your output looks like this:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
dist/public/funstack__/
|
|
78
|
+
└── fun:rsc-payload/
|
|
79
|
+
├── a3f2b1c9d8e7f6a5.txt ← Home page
|
|
80
|
+
├── b5698be72eea3c37.txt ← About page
|
|
81
|
+
├── b62ec6668fd49300.txt ← Main app shell
|
|
82
|
+
└── c7d8e9f0a1b2c3d4.txt ← Docs page
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Each payload file has a **content-based hash** in its filename. This enables aggressive browser caching - the file only changes when its content changes.
|
|
86
|
+
|
|
87
|
+
**Note:** during development, UUIDs are used instead of content hashes for faster rebuilds. Content hashes are applied during production builds.
|
|
88
|
+
|
|
89
|
+
## Best Practices
|
|
90
|
+
|
|
91
|
+
**Always wrap route components** - This is the single most important optimization. It prevents users from downloading content for pages they may never visit.
|
|
92
|
+
|
|
93
|
+
**Use Suspense boundaries** - Every `defer()` call must be inside a `<Suspense>` boundary. The fallback is shown while the payload is being fetched.
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
<Suspense fallback={<PageSkeleton />}>{defer(<PageContent />)}</Suspense>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Consider below-the-fold content** - Content hidden in collapsed sections, tabs, or modals is a good candidate for `defer()` since users may never need it.
|
|
100
|
+
|
|
101
|
+
> **Tip:** You can combine `defer()` with React 19's `<Activity>` component to prefetch content in the background. When a `defer()`ed node is rendered under `<Activity mode="hidden">`, it won't be shown in the UI, but the fetch of the RSC payload will start immediately. This is useful when hidden content (like inactive tabs or collapsed sections) should be fetched ahead of time so it's ready when the user needs it.
|
|
102
|
+
|
|
103
|
+
## See Also
|
|
104
|
+
|
|
105
|
+
- [defer()](/funstack-static/api/defer) - API reference with full signature and technical details
|
|
106
|
+
- [How It Works](/funstack-static/learn/how-it-works) - Overall FUNSTACK Static architecture
|
|
107
|
+
- [React Server Components](/funstack-static/learn/rsc) - Understanding RSC fundamentals
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# React Server Components
|
|
2
|
+
|
|
3
|
+
[React Server Components (RSC)](https://react.dev/reference/rsc/server-components) are a new paradigm for building React applications where components can run on the server (or at build time) rather than in the browser.
|
|
4
|
+
|
|
5
|
+
## What Are Server Components?
|
|
6
|
+
|
|
7
|
+
Server Components are React components that:
|
|
8
|
+
|
|
9
|
+
- **Run on the server/build time** - Not in the browser
|
|
10
|
+
- **Can be async** - Use `async/await` directly in components
|
|
11
|
+
- **Have zero client bundle impact** - Their code never ships to the browser
|
|
12
|
+
- **Can access server-side resources** - Files, databases, environment variables
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
// This component runs at build time, not in the browser
|
|
16
|
+
async function UserList() {
|
|
17
|
+
// Direct file system access
|
|
18
|
+
const data = await fs.readFile("./data/users.json", "utf-8");
|
|
19
|
+
const users = JSON.parse(data);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<ul>
|
|
23
|
+
{users.map((user) => (
|
|
24
|
+
<li key={user.id}>{user.name}</li>
|
|
25
|
+
))}
|
|
26
|
+
</ul>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## FUNSTACK Static and RSC
|
|
32
|
+
|
|
33
|
+
FUNSTACK Static is a React framework **without a runtime server** that still leverages React Server Components to **improve performance of traditional SPAs**.
|
|
34
|
+
|
|
35
|
+
All server components are rendered at **build time**, producing RSC Payloads that are fetched by the client (browser) at runtime.
|
|
36
|
+
|
|
37
|
+
While dynamic server-side logic isn't possible with FUNSTACK Static, you can still benefit from RSC features like:
|
|
38
|
+
|
|
39
|
+
- Reduced bundle size
|
|
40
|
+
- Build-time data fetching
|
|
41
|
+
- Async components
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
// Build-time data fetching with an async Server Component
|
|
45
|
+
async function BlogPost({ slug }: { slug: string }) {
|
|
46
|
+
// Runs during build, not at runtime
|
|
47
|
+
const content = await fetchMarkdownFile(`./posts/${slug}.md`);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<article>
|
|
51
|
+
<Markdown content={content} />
|
|
52
|
+
</article>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Note:** in FUNSTACK Static, async data is fetched at build time. For truly dynamic data, you'll need client-side JavaScript and fetch data as you would do in pre-RSC SPAs.
|
|
58
|
+
|
|
59
|
+
## Composing Server and Client Components
|
|
60
|
+
|
|
61
|
+
While Server Components can't use hooks or browser APIs, they can render Client Components that do:
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
// Server Component (runs at build time)
|
|
65
|
+
async function ProductPage({ id }: { id: string }) {
|
|
66
|
+
const product = await fetchProduct(id);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div>
|
|
70
|
+
<h1>{product.name}</h1>
|
|
71
|
+
<p>{product.description}</p>
|
|
72
|
+
|
|
73
|
+
{/* Client Component for interactivity */}
|
|
74
|
+
<AddToCartButton productId={id} />
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
// Client Component (runs in browser)
|
|
82
|
+
"use client";
|
|
83
|
+
|
|
84
|
+
import { useState } from "react";
|
|
85
|
+
|
|
86
|
+
export function AddToCartButton({ productId }: { productId: string }) {
|
|
87
|
+
const [added, setAdded] = useState(false);
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<button onClick={() => setAdded(true)}>
|
|
91
|
+
{added ? "Added!" : "Add to Cart"}
|
|
92
|
+
</button>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Benefits for SPAs
|
|
98
|
+
|
|
99
|
+
### 1. Reduced Bundle Size
|
|
100
|
+
|
|
101
|
+
Unlike traditional SPAs where all code ships to the client, Server Components never reach the browser. Only static HTML (and glue code for client components) is sent.
|
|
102
|
+
|
|
103
|
+
### 2. Reduced Warm-Up Time
|
|
104
|
+
|
|
105
|
+
The browser does't need to execute Server Component code, leading to less JavaScript to parse and run on initial load. Only ship the code needed for interactivity.
|
|
106
|
+
|
|
107
|
+
### 3. Build-Time Data Fetching
|
|
108
|
+
|
|
109
|
+
Fetch data once at build time, embedded directly into your pre-rendered HTML:
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
async function BlogIndex() {
|
|
113
|
+
// Fetched once during build, not on every page view
|
|
114
|
+
const posts = await fetchAllPosts();
|
|
115
|
+
return <PostList posts={posts} />;
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 4. Full SPA Interactivity
|
|
120
|
+
|
|
121
|
+
On the browser, your app behaves like any SPA - client-side navigation, state management, and all the interactivity you need:
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
// Client Component for interactive features
|
|
125
|
+
"use client";
|
|
126
|
+
|
|
127
|
+
import { useState } from "react";
|
|
128
|
+
import { useNavigate } from "@funstack/router";
|
|
129
|
+
|
|
130
|
+
export function SearchBox() {
|
|
131
|
+
const [query, setQuery] = useState("");
|
|
132
|
+
const navigate = useNavigate();
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<input
|
|
136
|
+
value={query}
|
|
137
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
138
|
+
onKeyDown={(e) => {
|
|
139
|
+
if (e.key === "Enter") {
|
|
140
|
+
navigate(`/search?q=${query}`);
|
|
141
|
+
}
|
|
142
|
+
}}
|
|
143
|
+
/>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 5. Type-Safe Data Flow
|
|
149
|
+
|
|
150
|
+
Data flows from Server Components to Client Components with full TypeScript support:
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
interface Post {
|
|
154
|
+
id: string;
|
|
155
|
+
title: string;
|
|
156
|
+
content: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function BlogPost({ slug }: { slug: string }): Promise<JSX.Element> {
|
|
160
|
+
const post: Post = await fetchPost(slug);
|
|
161
|
+
return <Article post={post} />;
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Considerations
|
|
166
|
+
|
|
167
|
+
When using FUNSTACK Static, keep in mind:
|
|
168
|
+
|
|
169
|
+
1. **Build-time data** - Server Component data is fetched during build. For runtime data, use Client Components with standard fetch patterns.
|
|
170
|
+
2. **No server context** - No access to cookies, headers, or request data in Server Components.
|
|
171
|
+
3. **Only one entrypoint** - FUNSTACK Static apps are single-page applications, without any routing built in. All routing and navigation must be handled client-side.
|
|
172
|
+
|
|
173
|
+
This makes FUNSTACK Static ideal for developers looking to leverage RSC benefits while deploying simple static SPAs without server infrastructure.
|
|
174
|
+
|
|
175
|
+
## See Also
|
|
176
|
+
|
|
177
|
+
- [Getting Started](/funstack-static/getting-started) - Set up your first project
|
|
178
|
+
- [defer()](/funstack-static/api/defer) - Stream content progressively
|
|
179
|
+
- [funstackStatic()](/funstack-static/api/funstack-static) - Plugin configuration
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Server-Side Rendering
|
|
2
|
+
|
|
3
|
+
In FUNSTACK Static, **Server-Side Rendering (SSR)** means a build-time process that pre-renders your React components (including client components) to HTML. This can make the initial paint faster.
|
|
4
|
+
|
|
5
|
+
FUNSTACK Static supports a `ssr` option to enable SSR. This page explains when to enable this option and what to consider.
|
|
6
|
+
|
|
7
|
+
## What is SSR?
|
|
8
|
+
|
|
9
|
+
By default, FUNSTACK Static renders only the Root shell to HTML. The App component is delivered as an RSC payload and rendered client-side. When you enable `ssr: true`, the App component is also rendered to HTML at build time:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
funstackStatic({
|
|
13
|
+
root: "./src/root.tsx",
|
|
14
|
+
app: "./src/App.tsx",
|
|
15
|
+
ssr: true,
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Pros
|
|
20
|
+
|
|
21
|
+
### Faster Paint on Initial Page Load
|
|
22
|
+
|
|
23
|
+
With SSR enabled, the full page content is visible in the HTML before JavaScript loads. Users see meaningful content immediately, rather than waiting for JavaScript to download, parse, and execute.
|
|
24
|
+
|
|
25
|
+
This improves perceived performance, especially on:
|
|
26
|
+
|
|
27
|
+
- Slower network connections
|
|
28
|
+
- Lower-powered devices
|
|
29
|
+
- Pages with significant content above the fold
|
|
30
|
+
|
|
31
|
+
The browser can start painting content as soon as the HTML arrives, while JavaScript loads in the background. Once loaded, React hydrates the existing HTML to make it interactive.
|
|
32
|
+
|
|
33
|
+
## Cons
|
|
34
|
+
|
|
35
|
+
### Client Components Must Be SSR-Capable
|
|
36
|
+
|
|
37
|
+
When SSR is enabled, your client components run during the build process to generate HTML. This means they must work in a server-like environment where browser APIs are not available.
|
|
38
|
+
|
|
39
|
+
Components that directly access browser-only APIs will cause build errors:
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
"use client";
|
|
43
|
+
|
|
44
|
+
// This will fail during SSR build
|
|
45
|
+
export function BrowserOnlyComponent() {
|
|
46
|
+
const width = window.innerWidth; // window is not defined during SSR
|
|
47
|
+
return <div>Width: {width}</div>;
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
To make components SSR-capable, use `useSyncExternalStore` with a server snapshot:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
"use client";
|
|
55
|
+
|
|
56
|
+
import { useSyncExternalStore } from "react";
|
|
57
|
+
|
|
58
|
+
function subscribeToWidth(callback: () => void) {
|
|
59
|
+
window.addEventListener("resize", callback);
|
|
60
|
+
return () => window.removeEventListener("resize", callback);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getWidth() {
|
|
64
|
+
return window.innerWidth;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getServerWidth() {
|
|
68
|
+
return 0; // Fallback value during SSR
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function SSRSafeComponent() {
|
|
72
|
+
const width = useSyncExternalStore(
|
|
73
|
+
subscribeToWidth,
|
|
74
|
+
getWidth,
|
|
75
|
+
getServerWidth,
|
|
76
|
+
);
|
|
77
|
+
return <div>Width: {width}</div>;
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Common browser APIs to watch for:
|
|
82
|
+
|
|
83
|
+
- `window` and `document`
|
|
84
|
+
- `localStorage` and `sessionStorage`
|
|
85
|
+
- `location` (use your router's APIs instead)
|
|
86
|
+
|
|
87
|
+
## When to Use SSR
|
|
88
|
+
|
|
89
|
+
**Enable SSR when:**
|
|
90
|
+
|
|
91
|
+
- You want the fastest possible initial paint
|
|
92
|
+
- Your client components are already SSR-compatible
|
|
93
|
+
- You're building content-heavy pages
|
|
94
|
+
|
|
95
|
+
**Keep SSR disabled when:**
|
|
96
|
+
|
|
97
|
+
- Your app relies heavily on browser-only APIs
|
|
98
|
+
- You prefer simpler client component development
|
|
99
|
+
- Initial paint speed is not a priority
|
|
100
|
+
|
|
101
|
+
## See Also
|
|
102
|
+
|
|
103
|
+
- [How It Works](/funstack-static/learn/how-it-works) - Understanding the build process
|
|
104
|
+
- [funstackStatic()](/funstack-static/api/funstack-static) - Configuration reference
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
import "../client/entry.mjs";
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DeferredComponent, RegistryContext } from "../rsc-client/clientWrapper.mjs";
|
|
2
2
|
import "../rsc-client/entry.mjs";
|
|
3
|
-
export {
|
|
3
|
+
export { DeferredComponent, RegistryContext };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { DeferredComponent, RegistryContext } from "../rsc-client/clientWrapper.mjs";
|
|
4
4
|
import "../rsc-client/entry.mjs";
|
|
5
5
|
|
|
6
|
-
export {
|
|
6
|
+
export { DeferredComponent, RegistryContext };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { defer } from "../rsc/defer.mjs";
|
|
2
|
-
export { defer };
|
|
1
|
+
import { DeferOptions, defer } from "../rsc/defer.mjs";
|
|
2
|
+
export { type DeferOptions, defer };
|
package/dist/plugin/index.d.mts
CHANGED
|
@@ -19,11 +19,27 @@ interface FunstackStaticOptions {
|
|
|
19
19
|
* @default dist/public
|
|
20
20
|
*/
|
|
21
21
|
publicOutDir?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Enable server-side rendering of the App component.
|
|
24
|
+
* When false, only the Root shell is SSR'd and the App renders client-side.
|
|
25
|
+
* When true, both Root and App are SSR'd and the client hydrates.
|
|
26
|
+
*
|
|
27
|
+
* @default false
|
|
28
|
+
*/
|
|
29
|
+
ssr?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Path to a module that runs on the client side before React hydration.
|
|
32
|
+
* Use this for client-side instrumentation like Sentry, analytics, or feature flags.
|
|
33
|
+
* The module is imported for its side effects only (no exports needed).
|
|
34
|
+
*/
|
|
35
|
+
clientInit?: string;
|
|
22
36
|
}
|
|
23
37
|
declare function funstackStatic({
|
|
24
38
|
root,
|
|
25
39
|
app,
|
|
26
|
-
publicOutDir
|
|
40
|
+
publicOutDir,
|
|
41
|
+
ssr,
|
|
42
|
+
clientInit
|
|
27
43
|
}: FunstackStaticOptions): (Plugin | Plugin[])[];
|
|
28
44
|
//#endregion
|
|
29
45
|
export { FunstackStaticOptions, funstackStatic };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/plugin/index.ts"],"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/plugin/index.ts"],"mappings":";;;UAMiB,qBAAA;;AAAjB;;;;EAME,IAAA;EAKA;;;;EAAA,GAAA;EAoBU;AACX;;;;EAfC,YAAA;EAoBA;;;;;;;EAZA,GAAA;EAUA;;;;;EAJA,UAAA;AAAA;AAAA,iBAGsB,cAAA,CAAA;EACtB,IAAA;EACA,GAAA;EACA,YAAA;EACA,GAAA;EACA;AAAA,GACC,qBAAA,IAAyB,MAAA,GAAS,MAAA"}
|