Skip to content

Commit 6ef6289

Browse files
committed
ssr
1 parent c589ce7 commit 6ef6289

File tree

4 files changed

+255
-20
lines changed

4 files changed

+255
-20
lines changed

src/api/application.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ Calling `createApp` returns an application instance. This instance provides an a
1919

2020
In addition, since the `createApp` method returns the application instance itself, you can chain other methods after it which can be found in the following sections.
2121

22+
## createSSRApp()
23+
24+
<!-- TODO -->
25+
2226
## app.mount()
2327

2428
- **Arguments:**

src/api/ssr.md

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
# Server-Side Rendering API
22

3-
:::tip
4-
APIs listed on this page are exported by the `@vue/server-renderer` package.
5-
:::
6-
73
## renderToString()
84

5+
> Exported from `vue/server-renderer`
6+
97
**Signature**
108

119
```ts
@@ -18,8 +16,8 @@ function renderToString(
1816
**Usage**
1917

2018
```js
21-
const { createSSRApp } = require('vue')
22-
const { renderToString } = require('@vue/server-renderer')
19+
import { createSSRApp } from 'vue'
20+
import { renderToString } from 'vue/server-renderer'
2321
2422
const app = createSSRApp({
2523
data: () => ({ msg: 'hello' }),
@@ -60,7 +58,7 @@ function renderToNodeStream(input: App | VNode, context?: SSRContext): Readable
6058
renderToNodeStream(app).pipe(res)
6159
```
6260

63-
**Note:** This method is not supported in the ESM build of `@vue/server-renderer`, which is decoupled from Node.js environments. Use `pipeToNodeWritable` instead.
61+
**Note:** This method is not supported in the ESM build of `vue/server-renderer`, which is decoupled from Node.js environments. Use `pipeToNodeWritable` instead.
6462

6563
## pipeToNodeWritable()
6664

@@ -174,3 +172,30 @@ renderToSimpleStream(
174172
}
175173
)
176174
```
175+
176+
## useSSRContext()
177+
178+
**Signature**
179+
180+
```ts
181+
function useSSRContext<T = Record<string, any>>(): T | undefined
182+
```
183+
184+
**Usage**
185+
186+
`useSSRContext()` is used to retrieve the context object passed to `renderToString()` or other server render APIs:
187+
188+
```vue
189+
<script setup>
190+
import { useSSRContext } from 'vue'
191+
192+
// make sure to only call it during SSR
193+
// https://vitejs.dev/guide/ssr.html#conditional-logic
194+
if (import.meta.env.SSR) {
195+
const ctx = useSSRContext()
196+
// ...attach properties to the context
197+
}
198+
</script>
199+
```
200+
201+
This can be used to attach information that is needed for rendering the final HTML (e.g. head metadata) to the render context.

src/guide/scaling-up/ssr.md

Lines changed: 218 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,236 @@
1-
# Server-Side Rendering <Badge text="WIP" />
1+
---
2+
aside: deep
3+
---
24

3-
## Cross-Request State Pollution
5+
# Server-Side Rendering (SSR)
6+
7+
## Overview
8+
9+
### What is SSR?
10+
11+
Vue.js is a framework for building client-side applications. By default, Vue components produce and manipulate DOM in the browser as output. However, it is also possible to render the same components into HTML strings on the server, send them directly to the browser, and finally "hydrate" the static markup into a fully interactive app on the client.
12+
13+
A server-rendered Vue.js app can also be considered "isomorphic" or "universal", in the sense that the majority of your app's code runs on both the server **and** the client.
14+
15+
### Why SSR?
16+
17+
Compared to a client-side Single-Page Application (SPA), the advantage of SSR primarily lies in:
18+
19+
- **Better SEO**: the search engine crawlers will directly see the fully rendered page.
20+
21+
:::tip
22+
Note that as of now, Google and Bing can index synchronous JavaScript applications just fine. Synchronous being the key word there. If your app starts with a loading spinner, then fetches content via Ajax, the crawler will not wait for you to finish. This means if you have content fetched asynchronously on pages where SEO is important, SSR might be necessary.
23+
:::
24+
25+
- **Faster time-to-content**: this is more prominent on slow internet or slow devices. Server-rendered markup doesn't need to wait until all JavaScript has been downloaded and executed to be displayed, so your user will see a fully-rendered page sooner. This generally results in improved [Core Web Vitals](https://web.dev/vitals/) metrics, better user experience, and can be critical for applications where time-to-content is directly associated with conversion rate.
26+
27+
- **Unified mental model**: you get to use the same language and the same declarative, component-oriented mental model for developing your entire app, instead of jumping back and forth between a backend templating system and a frontend framework.
28+
29+
There are also some trade-offs to consider when using SSR:
30+
31+
- Development constraints. Browser-specific code can only be used inside certain lifecycle hooks; some external libraries may need special treatment to be able to run in a server-rendered app.
32+
33+
- More involved build setup and deployment requirements. Unlike a fully static SPA that can be deployed on any static file server, a server-rendered app requires an environment where a Node.js server can run.
34+
35+
- More server-side load. Rendering a full app in Node.js is going to be more CPU-intensive than just serving static files, so if you expect high traffic, be prepared for corresponding server load and wisely employ caching strategies.
36+
37+
Before using SSR for your app, the first question you should ask is whether you actually need it. It mostly depends on how important time-to-content is for your app. For example, if you are building an internal dashboard where an extra few hundred milliseconds on initial load doesn't matter that much, SSR would be an overkill. However, in cases where time-to-content is absolutely critical, SSR can help you achieve the best possible initial load performance.
38+
39+
### SSR vs. SSG
40+
41+
**Static Site Generation (SSG)**, also referred to as pre-rendering, is another popular technique for building fast websites. If the data needed to server-render a page is the same for every user, then instead of rendering the page every time a request comes in, we can render it only once, ahead of time, during the build process. Pre-rendered pages are generated and served as static HTML files.
42+
43+
SSG retains the same performance characteristics of SSR apps: it provides great time-to-content performance. At the same time, it is cheaper and easier to deploy than SSR apps because the output is static HTML and assets. The keyword here is **static**: SSG can only be applied to pages consuming static data, i.e. data that is known at build time and does not change between deploys. Every time the data changes, a new deployment is needed.
44+
45+
If you're only investigating SSR to improve the SEO of a handful of marketing pages (e.g. `/`, `/about`, `/contact`, etc), then you probably want SSG instead of SSR. SSG is also great for content-based websites such as documentation sites or blogs. In fact, this website you are reading right now is statically generated using [VitePress](https://vitepress.vuejs.org/), a Vue-powered static site generator.
46+
47+
## Basic Usage
48+
49+
### Rendering an App
50+
51+
Vue's server-rendering API is exposed under `vue/server-renderer`.
52+
53+
Let's take a look at the most bare-bone example of Vue SSR in action. First, create a new directory and run `npm install vue` in it. Then, create an `example.mjs` file:
54+
55+
```js
56+
// example.mjs
57+
// this runs in Node.js on the server.
58+
import { createSSRApp } from 'vue'
59+
import { renderToString } from 'vue/server-renderer'
60+
61+
const app = createSSRApp({
62+
data: () => ({ msg: 'hello' }),
63+
template: `<div>{{ msg }}</div>`
64+
})
65+
66+
;(async () => {
67+
const html = await renderToString(app)
68+
console.log(html)
69+
})()
70+
```
71+
72+
Then run:
73+
74+
```sh
75+
> node example.mjs
76+
```
77+
78+
...which should print the following:
79+
80+
```html
81+
<div>hello</div>
82+
```
83+
84+
[`renderToString()`](/api/ssr.html#rendertostring) takes a Vue app instance and returns a Promise that resolves to the rendered HTML of the app. It is also possible to perform streaming render using [Node.js Stream API](https://nodejs.org/api/stream.html) or [Web Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API). Check out the [SSR API Reference](/api/ssr.html) for full details.
85+
86+
### Client Hydration
87+
88+
In actual SSR applications, the server-rendered markup is typically embedded in an HTML page like this:
89+
90+
```html{6}
91+
<!DOCTYPE html>
92+
<html>
93+
<head>...</head>
94+
<body>
95+
<div id="app">
96+
<div>hello</div> <!-- server-rendered content -->
97+
</div>
98+
</body>
99+
</html>
100+
```
101+
102+
On the client side, Vue needs to perform the **hydration** step. It creates the same Vue application that was run on the server, matches each component to the DOM nodes it should control, and attaches event listeners so the app becomes interactive.
103+
104+
The only thing different from a client-only app is that we need to use [`createSSRApp()`](/api/application.html#createssrapp) instead of `createApp()`:
105+
106+
```js{2}
107+
// this runs in the browser.
108+
import { createSSRApp } from 'vue'
109+
110+
const app = createSSRApp({
111+
// ...same app as on server
112+
})
113+
114+
// mounting an SSR app on the client assumes
115+
// the HTML was pre-rendered and will perform
116+
// hydration instead of mounting new DOM nodes.
117+
app.mount('#app')
118+
```
119+
120+
## Higher Level Solutions
121+
122+
While the examples so far are relatively simple, production-ready SSR apps are fullstack projects that involve a lot more than just Vue APIs. We will need to:
123+
124+
- Build the app twice: once for the client, and once for the server.
125+
126+
:::tip
127+
Vue components are compiled differently when used for SSR - templates are compiled into string concatenations instead of Virtual DOM render functions for more efficient rendering performance.
128+
:::
129+
130+
- In the server request handler, render the HTML page with the correct outer shell and app markup, including client-side asset links and resource hints. We may also need to switch between SSR and SSG mode, or even mix both in the same app.
131+
132+
- Manage routing, data fetching, and state management stores in a universal manner.
133+
134+
This is quite advanced and highly dependent on the built toolchain you have chosen to work with. Therefore, we highly recommend going with a higher-level, opinionated solution that abstracts away the complexity for you. Below we will introduce a few recommended SSR solutions in the Vue ecosystem.
135+
136+
### Nuxt
137+
138+
[Nuxt](https://v3.nuxtjs.org/) is a higher-level framework built on top of the Vue ecosystem which provides a streamlined development experience for writing universal Vue applications. Better yet, you can also use it as a static site generator! We highly recommend giving it a try.
139+
140+
### Quasar
141+
142+
[Quasar](https://quasar.dev) is a complete Vue-based solution that allows you to target SPA, SSR, PWA, mobile app, desktop app, and browser extension all using one codebase. It not only handles the build setup, but also provides a full collection of Material Design compliant UI components.
143+
144+
### Vite SSR
145+
146+
Vite provides built-in [support for Vue server-side rendering](https://vitejs.dev/guide/ssr.html), but it is intentionally low-level. If you wish to go directly with Vite, check out [vite-plugin-ssr](https://vite-plugin-ssr.com/), a community plugin that abstracts away many challenging details for you.
147+
148+
You can also find an example Vue + Vite SSR project using manual setup [here](https://github.com/vitejs/vite/tree/main/packages/playground/ssr-vue), which can serve as a base to build upon. Note this is only recommended if you are experienced with SSR / build tools and really want to have complete control over the higher-level architecture.
149+
150+
## Writing SSR-friendly Code
151+
152+
Regardless of your build setup or higher-level framework choice, there are some principles that apply in all Vue SSR applications.
153+
154+
### Reactivity on the Server
155+
156+
During SSR, each request URL maps to a desired state of our application. There is no user interaction and no DOM updates, so reactivity is unnecessary on the server. By default, reactivity is disabled during SSR for better performance.
157+
158+
### Component Lifecycle Hooks
159+
160+
Since there are no dynamic updates, lifecycle hooks such as <span class="options-api">`mounted`</span><span class="composition-api">`onMounted`</span> or <span class="options-api">`updated`</span><span class="composition-api">`onUpdated`</span> will **NOT** be called during SSR and will only be executed on the client.<span class="options-api"> The only hooks that are called during SSR are `beforeCreate` and `created`</span>
161+
162+
You should avoid code that produces side effects that need cleanup in <span class="options-api">`beforeCreate` and `created`</span><span class="composition-api">`setup()` or the root scope of `<script setup>`</span>. An example of such side effects is setting up timers with `setInterval`. In client-side only code we may setup a timer and then tear it down in <span class="options-api">`beforeUnmount`</span><span class="composition-api">`onBeforeUnmount`</span> or <span class="options-api">`unmounted`</span><span class="composition-api">`onUnmounted`</span>. However, because the unmount hooks will never be called during SSR, the timers will stay around forever. To avoid this, move your side-effect code into <span class="options-api">`mounted`</span><span class="composition-api">`onMounted`</span> instead.
163+
164+
### Access to Platform-Specific APIs
165+
166+
Universal code cannot assume access to platform-specific APIs, so if your code directly uses browser-only globals like `window` or `document`, they will throw errors when executed in Node.js, and vice-versa.
167+
168+
For tasks shared between server and client but use different platform APIs, it's recommended to wrap the platform-specific implementations inside a universal API, or use libraries that do this for you. For example, you can use [`node-fetch`](https://github.com/node-fetch/node-fetch) to use the same fetch API on both server and client.
169+
170+
For browser-only APIs, the common approach is to lazily access them inside client-only lifecycle hooks such as <span class="options-api">`mounted`</span><span class="composition-api">`onMounted`</span>.
171+
172+
Note that if a 3rd party library is not written with universal usage in mind, it could be tricky to integrate it into an server-rendered app. You _might_ be able to get it working by mocking some of the globals, but it would be hacky and may interfere with the environment detection code of other libraries.
173+
174+
### Cross-Request State Pollution
4175

5176
In the State Management chapter, we introduced a [simple state management pattern using Reactivity APIs](state-management.html#simple-state-management-with-reactivity-api). In an SSR context, this pattern requires some additional adjustments.
6177

7178
The pattern declares shared state as **singletons**. This means there is only once instance of the reactive object throughout the entire lifecycle of our application. This works as expected in a pure client-side Vue application, since the our application code is initialized fresh for each browser page visit.
8179

9180
However, in an SSR context, the application code is typically initialized only once on the server, when the server boots up. In such case, singletons in our application will be shared across multiple requests handled by the server! If we mutate the shared singleton store with data specific to one user, it can be accidentally leaked to a request from another user. We call this **cross-request state pollution.**
10181

11-
// TODO finish
182+
To workaround this, we need to create a fresh instance of the application and the shared object on each request. Then, instead of directly importing it in our components, we provide the shared state using [app-level provide](/guide/components/provide-inject.html#app-level-provide) and inject it in components that need it.
12183

13-
## Higher Level Solutions
184+
State Management libraries like Pinia are designed with this in mind. Consult [Pinia's SSR guide](https://pinia.vuejs.org/ssr/) for more details.
14185

15-
### Nuxt.js
186+
### Hydration Mismatch
16187

17-
Properly configuring all the discussed aspects of a production-ready server-rendered app can be a daunting task. Luckily, there is an excellent community project that aims to make all of this easier: [Nuxt.js](https://v3.nuxtjs.org/). Nuxt.js is a higher-level framework built on top of the Vue ecosystem which provides an extremely streamlined development experience for writing universal Vue applications. Better yet, you can even use it as a static site generator (with pages authored as single-file Vue components)! We highly recommend giving it a try.
188+
If the DOM structure of the pre-rendered HTML does not match the expected output of the client-side app, there will be a hydration mismatch error. In most cases, this is caused by browser's native HTML parsing behavior trying to correct invalid structures in the HTML string. For example, a common gotcha is that [`<div>` cannot be placed inside `<p>`](https://stackoverflow.com/questions/8397852/why-cant-the-p-tag-contain-a-div-tag-inside-it):
18189

19-
### Quasar Framework SSR + PWA
190+
```html
191+
<p><div>hi</div></p>
192+
```
20193

21-
[Quasar Framework](https://quasar.dev) will generate an SSR app (with optional PWA handoff) that leverages its best-in-class build system, sensible configuration and developer extensibility to make designing and building your idea a breeze. With over one hundred specific "Material Design 2.0"-compliant components, you can decide which ones to execute on the server, which are available in the browser, and even manage the `<meta>` tags of your site. Quasar is a node.js and webpack based development environment that supercharges and streamlines rapid development of SPA, PWA, SSR, Electron, Capacitor and Cordova apps—all from one codebase.
194+
If we produce this in our server-rendered HTML, the browser will terminate the first `<p>` when `<div>` is encountered and parse it into the following DOM structure:
22195

23-
### Vite SSR
196+
```html
197+
<p></p>
198+
<div>hi</div>
199+
<p></p>
200+
```
201+
202+
When Vue encounters a hydration mismatch, it will attempt to automatically recover and adjust the pre-rendered DOM to match the client side state. This will lead to some rendering performance loss due to incorrect nodes being discarded and new nodes being mounted, but in most cases, the app should continue to work as expected. That said, it is still best to eliminate hydration mismatches during development.
203+
204+
### Custom Directives
205+
206+
Since most custom directives involve direct DOM manipulation, they are ignored during SSR.
207+
208+
You can provide a transform function to implement the server-side rendering logic for a custom directive. The function should be passed under the `directiveTransforms` option to `@vue/compiler-dom`.
209+
210+
Example Vite config that provides an empty stub for a custom `v-focus` directive:
24211

25-
[Vite](https://vitejs.dev/) is a new breed of frontend build tool that significantly improves the frontend development experience. It consists of two major parts:
212+
```js
213+
import vue from '@vitejs/plugin-vue'
26214

27-
- A dev server that serves your source files over native ES modules, with rich built-in features and astonishingly fast Hot Module Replacement (HMR).
215+
export default {
216+
plugins: [
217+
vue({
218+
template: {
219+
compilerOptions: {
220+
directiveTransforms: {
221+
// an empty array indicates that v-focus
222+
// does not add any rendered attributes to the element.
223+
focus: () => ({ props: [] })
224+
}
225+
}
226+
}
227+
})
228+
]
229+
}
230+
```
28231

29-
- A build command that bundles your code with [Rollup](https://rollupjs.org/), pre-configured to output highly optimized static assets for production.
232+
Writing a proper directive transform requires TypeScript proficiency and knowledge of Vue's compiler API. We plan to add documentation for the compiler API in the future, but for now, you will need to consult the [source code](https://github.com/vuejs/vue-next/blob/master/packages/compiler-core/src/transform.ts#L53-L63) to learn more about it.
30233

31-
Vite also provides built-in [support for server-side rendering](https://vitejs.d](/guide/scaling-up/ssr.html). You can find an example project with Vue [here](https://github.com/vitejs/vite/tree/main/packages/playground/ssr-vue)
234+
:::tip
235+
Currently, the SSR compiler will throw an error if a server-side transform is not found for a custom directive. This behavior will be adjusted to a warning in the future.
236+
:::

test.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<p><div>hi</div></p>

0 commit comments

Comments
 (0)