@funstack/static 0.0.3 → 0.0.5-alpha.0
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 +9 -12
- package/dist/docs/MigratingFromViteSPA.md +321 -0
- package/dist/docs/index.md +2 -0
- package/dist/docs/learn/LazyServerComponents.md +120 -0
- package/dist/docs/learn/OptimizingPayloads.md +2 -0
- package/dist/plugin/index.mjs +4 -1
- package/dist/plugin/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,9 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
A maximally minimal React framework. Vite plugin for static sites with RSC support.
|
|
4
4
|
|
|
5
|
-
> [!WARNING]
|
|
6
|
-
> This is work in progress.
|
|
7
|
-
|
|
8
5
|
## Features
|
|
9
6
|
|
|
10
7
|
- :x: **No server runs** - perfect for CSR (Client Side Rendering) app and static deployment.
|
|
@@ -38,22 +35,22 @@ export default defineConfig({
|
|
|
38
35
|
});
|
|
39
36
|
```
|
|
40
37
|
|
|
41
|
-
##
|
|
38
|
+
## Documentation
|
|
42
39
|
|
|
43
|
-
|
|
40
|
+
For detailed API documentation and guides, visit the **[Documentation](https://uhyo.github.io/funstack-static/)**.
|
|
44
41
|
|
|
45
|
-
|
|
42
|
+
### :robot: FUNSTACK Static Skill
|
|
43
|
+
|
|
44
|
+
FUNSTACK Static provides an Agent Skill to feed your AI agents with knowledge about this framework. After installing `@funstack/static`, run the following command to add the skill to the project:
|
|
46
45
|
|
|
47
46
|
```sh
|
|
48
47
|
npx funstack-static-skill-installer
|
|
48
|
+
# or
|
|
49
|
+
yarn funstack-static-skill-installer
|
|
50
|
+
# or
|
|
51
|
+
pnpm funstack-static-skill-installer
|
|
49
52
|
```
|
|
50
53
|
|
|
51
|
-
This command registers the skill that provides AI assistants with knowledge about the FUNSTACK Static framework, including API references, best practices, and architectural guidance. After installation, your AI assistant will be able to better understand and work with your FUNSTACK Static project.
|
|
52
|
-
|
|
53
|
-
## Documentation
|
|
54
|
-
|
|
55
|
-
For detailed API documentation and guides, visit the **[Documentation](https://uhyo.github.io/funstack-static/)**.
|
|
56
|
-
|
|
57
54
|
## License
|
|
58
55
|
|
|
59
56
|
MIT
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# Migrating from Vite SPA
|
|
2
|
+
|
|
3
|
+
Already have a Vite-powered React SPA? This guide walks you through migrating to FUNSTACK Static to unlock React Server Components and improved performance.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Migrating from a standard Vite React SPA to FUNSTACK Static involves:
|
|
8
|
+
|
|
9
|
+
1. Installing FUNSTACK Static
|
|
10
|
+
2. Adding the Vite plugin
|
|
11
|
+
3. Restructuring your entry point into Root and App components
|
|
12
|
+
4. Converting appropriate components to Server Components
|
|
13
|
+
|
|
14
|
+
The good news: your existing client components work as-is. You can migrate incrementally.
|
|
15
|
+
|
|
16
|
+
## Step 1: Install Dependencies
|
|
17
|
+
|
|
18
|
+
Add FUNSTACK Static to your existing project:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @funstack/static
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or with pnpm:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pnpm add @funstack/static
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Hint:** at this point, a skill installer command is available to add FUNSTACK Static knowledge to your AI agents. Run `npx funstack-static-skill-installer` or similar to add the skill. Then you can ask your AI assistant for help with the migration!
|
|
31
|
+
|
|
32
|
+
## Step 2: Update Vite Config
|
|
33
|
+
|
|
34
|
+
Modify your `vite.config.ts` to add the FUNSTACK Static plugin:
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { funstackStatic } from "@funstack/static";
|
|
38
|
+
import react from "@vitejs/plugin-react";
|
|
39
|
+
import { defineConfig } from "vite";
|
|
40
|
+
|
|
41
|
+
export default defineConfig({
|
|
42
|
+
plugins: [
|
|
43
|
+
funstackStatic({
|
|
44
|
+
root: "./src/Root.tsx",
|
|
45
|
+
app: "./src/App.tsx",
|
|
46
|
+
}),
|
|
47
|
+
react(),
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Note that the existing React plugin remains; FUNSTACK Static works alongside it.
|
|
53
|
+
|
|
54
|
+
## Step 3: Create the Root Component
|
|
55
|
+
|
|
56
|
+
The Root component replaces your `index.html` file. Create `src/Root.tsx`:
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
// src/Root.tsx
|
|
60
|
+
import type React from "react";
|
|
61
|
+
|
|
62
|
+
export default function Root({ children }: { children: React.ReactNode }) {
|
|
63
|
+
return (
|
|
64
|
+
<html lang="en">
|
|
65
|
+
<head>
|
|
66
|
+
<meta charSet="UTF-8" />
|
|
67
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
68
|
+
<title>My App</title>
|
|
69
|
+
{/* Add your existing <head> content here */}
|
|
70
|
+
</head>
|
|
71
|
+
<body>
|
|
72
|
+
<div id="root">{children}</div>
|
|
73
|
+
</body>
|
|
74
|
+
</html>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Move any content from your `index.html` `<head>` section into this component.
|
|
80
|
+
|
|
81
|
+
## Step 4: Update Your App Entry Point
|
|
82
|
+
|
|
83
|
+
Your existing `main.tsx` or `index.tsx` likely looks like this:
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
// Before: src/main.tsx
|
|
87
|
+
import React from "react";
|
|
88
|
+
import ReactDOM from "react-dom/client";
|
|
89
|
+
import App from "./App";
|
|
90
|
+
import "./index.css";
|
|
91
|
+
|
|
92
|
+
ReactDOM.createRoot(document.getElementById("root")!).render(
|
|
93
|
+
<React.StrictMode>
|
|
94
|
+
<App />
|
|
95
|
+
</React.StrictMode>,
|
|
96
|
+
);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
With FUNSTACK Static, you no longer need this file. Instead, your `App.tsx` becomes the entry point and is automatically rendered as a Server Component:
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
// After: src/App.tsx
|
|
103
|
+
import "./index.css";
|
|
104
|
+
import { HomePage } from "./pages/HomePage";
|
|
105
|
+
|
|
106
|
+
export default function App() {
|
|
107
|
+
return <HomePage />;
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
You can delete `main.tsx` - FUNSTACK Static handles the rendering.
|
|
112
|
+
|
|
113
|
+
## Step 5: Mark Client Component Boundaries
|
|
114
|
+
|
|
115
|
+
In a standard Vite SPA, all components are client components. With FUNSTACK Static, components are Server Components by default.
|
|
116
|
+
|
|
117
|
+
As a starting point, it is possible to mark the App component as a client component by adding `"use client"` at the top:
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
// src/App.tsx
|
|
121
|
+
"use client";
|
|
122
|
+
// ...rest of the code
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
This makes the entire app a client component, similar to your previous SPA.
|
|
126
|
+
|
|
127
|
+
However, to take advantage of Server Components, you should incrementally convert components to Server Components. This can be done by removing `"use client"` from components that don't need it and instead adding it only to components that require client-side interactivity.
|
|
128
|
+
|
|
129
|
+
**Note:** you only need to add `"use client"` to components that are **directly imported by Server Components**. This marks the boundary between server and client code. Components imported by other client components don't need the directive.
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
// src/components/Counter.tsx
|
|
133
|
+
"use client";
|
|
134
|
+
|
|
135
|
+
import { useState } from "react";
|
|
136
|
+
import { Button } from "./Button"; // No "use client" needed in Button.tsx
|
|
137
|
+
|
|
138
|
+
export function Counter() {
|
|
139
|
+
const [count, setCount] = useState(0);
|
|
140
|
+
return <Button onClick={() => setCount(count + 1)}>Count: {count}</Button>;
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
In this example, `Counter.tsx` needs `"use client"` because it's imported by a Server Component. But `Button.tsx` doesn't need it since it's only imported by `Counter`, which is already a client component.
|
|
145
|
+
|
|
146
|
+
A component is a client component if it:
|
|
147
|
+
|
|
148
|
+
- Use client-only hooks (`useState`, `useEffect`, `useContext`, etc.)
|
|
149
|
+
- Attach event handlers (`onClick`, `onChange`, etc.)
|
|
150
|
+
- Use browser-only APIs (`window`, `document`, `localStorage`, etc.)
|
|
151
|
+
|
|
152
|
+
## Step 6: Update Your Router (If Applicable)
|
|
153
|
+
|
|
154
|
+
If you're using React Router or another client-side router, you have two options:
|
|
155
|
+
|
|
156
|
+
### Option A: Keep Your Existing Router
|
|
157
|
+
|
|
158
|
+
You can continue using your existing router as a client component:
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
// src/App.tsx
|
|
162
|
+
import { ClientRouter } from "./ClientRouter";
|
|
163
|
+
|
|
164
|
+
export default function App() {
|
|
165
|
+
return <ClientRouter />;
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
// src/ClientRouter.tsx
|
|
171
|
+
"use client";
|
|
172
|
+
|
|
173
|
+
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
|
174
|
+
import { Home } from "./pages/Home";
|
|
175
|
+
import { About } from "./pages/About";
|
|
176
|
+
|
|
177
|
+
export function ClientRouter() {
|
|
178
|
+
return (
|
|
179
|
+
<BrowserRouter>
|
|
180
|
+
<Routes>
|
|
181
|
+
<Route path="/" element={<Home />} />
|
|
182
|
+
<Route path="/about" element={<About />} />
|
|
183
|
+
</Routes>
|
|
184
|
+
</BrowserRouter>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Option B: Use a Server-Compatible Router
|
|
190
|
+
|
|
191
|
+
For better performance, consider migrating to [FUNSTACK Router](https://github.com/uhyo/funstack-router) which is a standalone router for SPAs but still integrates well with Server Components.
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
npm install @funstack/router
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
// src/App.tsx
|
|
199
|
+
import { Router } from "@funstack/router";
|
|
200
|
+
import { route } from "@funstack/router/server";
|
|
201
|
+
import { Home } from "./pages/Home";
|
|
202
|
+
import { About } from "./pages/About";
|
|
203
|
+
|
|
204
|
+
const routes = [
|
|
205
|
+
route({ path: "/", component: <Home /> }),
|
|
206
|
+
route({ path: "/about", component: <About /> }),
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
export default function App() {
|
|
210
|
+
return <Router routes={routes} />;
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Step 7: Delete Unnecessary Files
|
|
215
|
+
|
|
216
|
+
After migration, you can remove:
|
|
217
|
+
|
|
218
|
+
- `src/main.tsx` (or `src/index.tsx`) - no longer needed
|
|
219
|
+
- `index.html` - replaced by `Root.tsx`
|
|
220
|
+
|
|
221
|
+
## Common Migration Patterns
|
|
222
|
+
|
|
223
|
+
### Global Styles
|
|
224
|
+
|
|
225
|
+
Import global CSS in your `App.tsx`:
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
228
|
+
// src/App.tsx
|
|
229
|
+
import "./index.css";
|
|
230
|
+
import "./global.css";
|
|
231
|
+
|
|
232
|
+
export default function App() {
|
|
233
|
+
// ...
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Context Providers
|
|
238
|
+
|
|
239
|
+
Wrap client-side providers in a client component:
|
|
240
|
+
|
|
241
|
+
```tsx
|
|
242
|
+
// src/Providers.tsx
|
|
243
|
+
"use client";
|
|
244
|
+
|
|
245
|
+
import { ThemeProvider } from "./ThemeContext";
|
|
246
|
+
import { AuthProvider } from "./AuthContext";
|
|
247
|
+
|
|
248
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
249
|
+
return (
|
|
250
|
+
<ThemeProvider>
|
|
251
|
+
<AuthProvider>{children}</AuthProvider>
|
|
252
|
+
</ThemeProvider>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
```tsx
|
|
258
|
+
// src/App.tsx
|
|
259
|
+
import { Providers } from "./Providers";
|
|
260
|
+
import { HomePage } from "./pages/HomePage";
|
|
261
|
+
|
|
262
|
+
export default function App() {
|
|
263
|
+
return (
|
|
264
|
+
<Providers>
|
|
265
|
+
<HomePage />
|
|
266
|
+
</Providers>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Environment Variables
|
|
272
|
+
|
|
273
|
+
Client-side environment variables still work the same way with the `VITE_` prefix:
|
|
274
|
+
|
|
275
|
+
```tsx
|
|
276
|
+
// In client components
|
|
277
|
+
const apiUrl = import.meta.env.VITE_API_URL;
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Verifying the Migration
|
|
281
|
+
|
|
282
|
+
Run the development server:
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
npm run dev
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Check that:
|
|
289
|
+
|
|
290
|
+
- Your app loads correctly
|
|
291
|
+
- Interactive components work (clicks, form inputs, etc.)
|
|
292
|
+
- Routing works as expected
|
|
293
|
+
- Styles are applied correctly
|
|
294
|
+
|
|
295
|
+
Build for production:
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
npm run build
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Your static files will be generated in `dist/public`, ready for deployment.
|
|
302
|
+
|
|
303
|
+
## Troubleshooting
|
|
304
|
+
|
|
305
|
+
### "Cannot use hooks in Server Component"
|
|
306
|
+
|
|
307
|
+
Add `"use client"` at the top of components that use React hooks.
|
|
308
|
+
|
|
309
|
+
### "window is not defined"
|
|
310
|
+
|
|
311
|
+
Components using browser APIs must be client components. Add `"use client"` directive.
|
|
312
|
+
|
|
313
|
+
### Styles not loading
|
|
314
|
+
|
|
315
|
+
Ensure CSS imports are in `App.tsx` or in client components that are actually rendered.
|
|
316
|
+
|
|
317
|
+
## What's Next?
|
|
318
|
+
|
|
319
|
+
- Learn about [defer()](/funstack-static/api/defer) for code splitting Server Components
|
|
320
|
+
- Explore [Optimizing RSC Payloads](/funstack-static/learn/optimizing-payloads) for better performance
|
|
321
|
+
- Understand [How It Works](/funstack-static/learn/how-it-works) under the hood
|
package/dist/docs/index.md
CHANGED
|
@@ -6,6 +6,7 @@ A Vite plugin for building static sites with React Server Components.
|
|
|
6
6
|
|
|
7
7
|
- [FAQ](./FAQ.md) - This is a bug in React itself. Please wait for [the fix](https://github.com/facebook/react/pull/34760) to be released.
|
|
8
8
|
- [Getting Started](./GettingStarted.md) - Welcome to **FUNSTACK Static**! Build high-performance Single Page Applications powered by React Server Components - no server required at runtime.
|
|
9
|
+
- [Migrating from Vite SPA](./MigratingFromViteSPA.md) - Already have a Vite-powered React SPA? This guide walks you through migrating to FUNSTACK Static to unlock React Server Components and improved performance.
|
|
9
10
|
|
|
10
11
|
### API
|
|
11
12
|
|
|
@@ -15,6 +16,7 @@ A Vite plugin for building static sites with React Server Components.
|
|
|
15
16
|
### Learn
|
|
16
17
|
|
|
17
18
|
- [How It Works](./learn/HowItWorks.md) - 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.
|
|
19
|
+
- [Using lazy() in Server Components](./learn/LazyServerComponents.md) - 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.
|
|
18
20
|
- [Optimizing RSC Payloads](./learn/OptimizingPayloads.md) - 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.
|
|
19
21
|
- [React Server Components](./learn/RSC.md) - [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.
|
|
20
22
|
- [Server-Side Rendering](./learn/SSR.md) - 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.
|
|
@@ -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
|
|
@@ -98,6 +98,8 @@ Each payload file has a **content-based hash** in its filename. This enables agg
|
|
|
98
98
|
|
|
99
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
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
|
+
|
|
101
103
|
## See Also
|
|
102
104
|
|
|
103
105
|
- [defer()](/funstack-static/api/defer) - API reference with full signature and technical details
|
package/dist/plugin/index.mjs
CHANGED
|
@@ -32,10 +32,13 @@ function funstackStatic({ root, app, publicOutDir = "dist/public", ssr = false,
|
|
|
32
32
|
if (clientInit) resolvedClientInitEntry = path.resolve(config.root, clientInit);
|
|
33
33
|
},
|
|
34
34
|
configEnvironment(_name, config) {
|
|
35
|
-
if (config.optimizeDeps
|
|
35
|
+
if (!config.optimizeDeps) config.optimizeDeps = {};
|
|
36
|
+
if (config.optimizeDeps.include) config.optimizeDeps.include = config.optimizeDeps.include.map((entry) => {
|
|
36
37
|
if (entry.startsWith("@vitejs/plugin-rsc")) entry = `@funstack/static > ${entry}`;
|
|
37
38
|
return entry;
|
|
38
39
|
});
|
|
40
|
+
if (!config.optimizeDeps.exclude) config.optimizeDeps.exclude = [];
|
|
41
|
+
config.optimizeDeps.exclude.push("@funstack/static");
|
|
39
42
|
}
|
|
40
43
|
},
|
|
41
44
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/plugin/index.ts"],"sourcesContent":["import path from \"node:path\";\nimport type { Plugin } from \"vite\";\nimport rsc from \"@vitejs/plugin-rsc\";\nimport { buildApp } from \"../build/buildApp\";\nimport { serverPlugin } from \"./server\";\n\nexport interface FunstackStaticOptions {\n /**\n * Root component of the page.\n * The file should `export default` a React component that renders the whole page.\n * (`<html>...</html>`).\n */\n root: string;\n /**\n * Entry point of your application.\n * The file should `export default` a React component that renders the application content.\n */\n app: string;\n /**\n * Output directory for build.\n *\n * @default dist/public\n */\n publicOutDir?: string;\n /**\n * Enable server-side rendering of the App component.\n * When false, only the Root shell is SSR'd and the App renders client-side.\n * When true, both Root and App are SSR'd and the client hydrates.\n *\n * @default false\n */\n ssr?: boolean;\n /**\n * Path to a module that runs on the client side before React hydration.\n * Use this for client-side instrumentation like Sentry, analytics, or feature flags.\n * The module is imported for its side effects only (no exports needed).\n */\n clientInit?: string;\n}\n\nexport default function funstackStatic({\n root,\n app,\n publicOutDir = \"dist/public\",\n ssr = false,\n clientInit,\n}: FunstackStaticOptions): (Plugin | Plugin[])[] {\n let resolvedRootEntry: string = \"__uninitialized__\";\n let resolvedAppEntry: string = \"__uninitialized__\";\n let resolvedClientInitEntry: string | undefined;\n\n return [\n {\n name: \"@funstack/static:config-pre\",\n // Placed early because the rsc plugin sets the outDir to the default value\n config(config) {\n return {\n environments: {\n client: {\n build: {\n outDir:\n config.environments?.client?.build?.outDir ?? publicOutDir,\n },\n },\n },\n };\n },\n },\n serverPlugin(),\n rsc({\n entries: {\n rsc: \"@funstack/static/entries/rsc\",\n ssr: \"@funstack/static/entries/ssr\",\n client: \"@funstack/static/entries/client\",\n },\n serverHandler: false,\n }),\n {\n name: \"@funstack/static:config\",\n configResolved(config) {\n resolvedRootEntry = path.resolve(config.root, root);\n resolvedAppEntry = path.resolve(config.root, app);\n if (clientInit) {\n resolvedClientInitEntry = path.resolve(config.root, clientInit);\n }\n },\n // Needed for properly bundling @vitejs/plugin-rsc for browser.\n
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/plugin/index.ts"],"sourcesContent":["import path from \"node:path\";\nimport type { Plugin } from \"vite\";\nimport rsc from \"@vitejs/plugin-rsc\";\nimport { buildApp } from \"../build/buildApp\";\nimport { serverPlugin } from \"./server\";\n\nexport interface FunstackStaticOptions {\n /**\n * Root component of the page.\n * The file should `export default` a React component that renders the whole page.\n * (`<html>...</html>`).\n */\n root: string;\n /**\n * Entry point of your application.\n * The file should `export default` a React component that renders the application content.\n */\n app: string;\n /**\n * Output directory for build.\n *\n * @default dist/public\n */\n publicOutDir?: string;\n /**\n * Enable server-side rendering of the App component.\n * When false, only the Root shell is SSR'd and the App renders client-side.\n * When true, both Root and App are SSR'd and the client hydrates.\n *\n * @default false\n */\n ssr?: boolean;\n /**\n * Path to a module that runs on the client side before React hydration.\n * Use this for client-side instrumentation like Sentry, analytics, or feature flags.\n * The module is imported for its side effects only (no exports needed).\n */\n clientInit?: string;\n}\n\nexport default function funstackStatic({\n root,\n app,\n publicOutDir = \"dist/public\",\n ssr = false,\n clientInit,\n}: FunstackStaticOptions): (Plugin | Plugin[])[] {\n let resolvedRootEntry: string = \"__uninitialized__\";\n let resolvedAppEntry: string = \"__uninitialized__\";\n let resolvedClientInitEntry: string | undefined;\n\n return [\n {\n name: \"@funstack/static:config-pre\",\n // Placed early because the rsc plugin sets the outDir to the default value\n config(config) {\n return {\n environments: {\n client: {\n build: {\n outDir:\n config.environments?.client?.build?.outDir ?? publicOutDir,\n },\n },\n },\n };\n },\n },\n serverPlugin(),\n rsc({\n entries: {\n rsc: \"@funstack/static/entries/rsc\",\n ssr: \"@funstack/static/entries/ssr\",\n client: \"@funstack/static/entries/client\",\n },\n serverHandler: false,\n }),\n {\n name: \"@funstack/static:config\",\n configResolved(config) {\n resolvedRootEntry = path.resolve(config.root, root);\n resolvedAppEntry = path.resolve(config.root, app);\n if (clientInit) {\n resolvedClientInitEntry = path.resolve(config.root, clientInit);\n }\n },\n configEnvironment(_name, config) {\n if (!config.optimizeDeps) {\n config.optimizeDeps = {};\n }\n // Needed for properly bundling @vitejs/plugin-rsc for browser.\n // See: https://github.com/vitejs/vite-plugin-react/tree/79bf57cc8b9c77e33970ec2e876bd6d2f1568d5d/packages/plugin-rsc#using-vitejsplugin-rsc-as-a-framework-packages-dependencies\n if (config.optimizeDeps.include) {\n config.optimizeDeps.include = config.optimizeDeps.include.map(\n (entry) => {\n if (entry.startsWith(\"@vitejs/plugin-rsc\")) {\n entry = `@funstack/static > ${entry}`;\n }\n return entry;\n },\n );\n }\n if (!config.optimizeDeps.exclude) {\n config.optimizeDeps.exclude = [];\n }\n // Since code includes imports to virtual modules, we need to exclude\n // us from Optimize Deps.\n config.optimizeDeps.exclude.push(\"@funstack/static\");\n },\n },\n {\n name: \"@funstack/static:virtual-entry\",\n resolveId(id) {\n if (id === \"virtual:funstack/root\") {\n return \"\\0virtual:funstack/root\";\n }\n if (id === \"virtual:funstack/app\") {\n return \"\\0virtual:funstack/app\";\n }\n if (id === \"virtual:funstack/config\") {\n return \"\\0virtual:funstack/config\";\n }\n if (id === \"virtual:funstack/client-init\") {\n return \"\\0virtual:funstack/client-init\";\n }\n },\n load(id) {\n if (id === \"\\0virtual:funstack/root\") {\n return `export { default } from \"${resolvedRootEntry}\";`;\n }\n if (id === \"\\0virtual:funstack/app\") {\n return `export { default } from \"${resolvedAppEntry}\";`;\n }\n if (id === \"\\0virtual:funstack/config\") {\n return `export const ssr = ${JSON.stringify(ssr)};`;\n }\n if (id === \"\\0virtual:funstack/client-init\") {\n if (resolvedClientInitEntry) {\n return `import \"${resolvedClientInitEntry}\";`;\n }\n return \"\";\n }\n },\n },\n {\n name: \"@funstack/static:build\",\n async buildApp(builder) {\n await buildApp(builder, this);\n },\n },\n ];\n}\n"],"mappings":";;;;;;AAwCA,SAAwB,eAAe,EACrC,MACA,KACA,eAAe,eACf,MAAM,OACN,cAC+C;CAC/C,IAAI,oBAA4B;CAChC,IAAI,mBAA2B;CAC/B,IAAI;AAEJ,QAAO;EACL;GACE,MAAM;GAEN,OAAO,QAAQ;AACb,WAAO,EACL,cAAc,EACZ,QAAQ,EACN,OAAO,EACL,QACE,OAAO,cAAc,QAAQ,OAAO,UAAU,cACjD,EACF,EACF,EACF;;GAEJ;EACD,cAAc;EACd,IAAI;GACF,SAAS;IACP,KAAK;IACL,KAAK;IACL,QAAQ;IACT;GACD,eAAe;GAChB,CAAC;EACF;GACE,MAAM;GACN,eAAe,QAAQ;AACrB,wBAAoB,KAAK,QAAQ,OAAO,MAAM,KAAK;AACnD,uBAAmB,KAAK,QAAQ,OAAO,MAAM,IAAI;AACjD,QAAI,WACF,2BAA0B,KAAK,QAAQ,OAAO,MAAM,WAAW;;GAGnE,kBAAkB,OAAO,QAAQ;AAC/B,QAAI,CAAC,OAAO,aACV,QAAO,eAAe,EAAE;AAI1B,QAAI,OAAO,aAAa,QACtB,QAAO,aAAa,UAAU,OAAO,aAAa,QAAQ,KACvD,UAAU;AACT,SAAI,MAAM,WAAW,qBAAqB,CACxC,SAAQ,sBAAsB;AAEhC,YAAO;MAEV;AAEH,QAAI,CAAC,OAAO,aAAa,QACvB,QAAO,aAAa,UAAU,EAAE;AAIlC,WAAO,aAAa,QAAQ,KAAK,mBAAmB;;GAEvD;EACD;GACE,MAAM;GACN,UAAU,IAAI;AACZ,QAAI,OAAO,wBACT,QAAO;AAET,QAAI,OAAO,uBACT,QAAO;AAET,QAAI,OAAO,0BACT,QAAO;AAET,QAAI,OAAO,+BACT,QAAO;;GAGX,KAAK,IAAI;AACP,QAAI,OAAO,0BACT,QAAO,4BAA4B,kBAAkB;AAEvD,QAAI,OAAO,yBACT,QAAO,4BAA4B,iBAAiB;AAEtD,QAAI,OAAO,4BACT,QAAO,sBAAsB,KAAK,UAAU,IAAI,CAAC;AAEnD,QAAI,OAAO,kCAAkC;AAC3C,SAAI,wBACF,QAAO,WAAW,wBAAwB;AAE5C,YAAO;;;GAGZ;EACD;GACE,MAAM;GACN,MAAM,SAAS,SAAS;AACtB,UAAM,SAAS,SAAS,KAAK;;GAEhC;EACF"}
|