|
1 |
| -# Nuxt 3 Rendering modes |
2 |
| - |
3 |
| -## Story behind |
4 |
| - |
5 |
| -We were developing a listing site using Nuxt 3 and aimed to optimize page load times while maintaining SEO benefits by choosing the right rendering mode for each page. Upon investigating this, we found that documentation was limited, particularly for newer and more complex rendering modes like [ISR](#isr). This scarcity was evident in the lack of specific technical details for functionality and testing. Moreover, various rendering modes are referred to differently across knowledge resources, and there are notable differences in implementation among providers such as Vercel, Netlify, etc. This led us to compile the information into the following article, which offers a clear, conceptual explanation and technical insights on setting up different rendering modes in Nuxt 3. |
6 |
| - |
7 |
| -Knowledge prerequisites: base knowledge of Nuxt |
8 |
| - |
9 |
| -## Rendering modes |
10 |
| -### Project setup |
11 |
| - |
12 |
| -The project consists of 7 pages, each displaying the current time and an HTML response from the same route. Specifically, the route /api/hello returns a JSON response with the current time, and each page features a different available rendering mode enabled. |
13 |
| - |
14 |
| -The pages differ only in their title that refers to the rendering mode used for generating them. [In the case of the simple SPA for example:](pages/spa.vue) |
15 |
| - |
16 |
| -```vue |
17 |
| -<template> |
18 |
| - <div> |
19 |
| - <p>{{ pageType }} page</p> |
20 |
| - <pre>Time after hydration: {{ new Date().toUTCString() }} </pre> |
21 |
| - <pre>Time in server rendered HTML: {{ data }}</pre> |
22 |
| - <NuxtLink to="/">Home</NuxtLink> |
23 |
| - </div> |
24 |
| -</template> |
25 |
| -<script setup lang="ts"> |
26 |
| -const pageType = "SPA"; // value differs for each route |
27 |
| -const { data } = await useFetch('/api/hello') |
28 |
| -</script> |
29 |
| -``` |
30 |
| - |
31 |
| -To make it visible when the page was rendered, we showcase 2 timestamps on the site: |
32 |
| - |
33 |
| -1. One we get from the API response, to see when was the page rendered by the server:: |
34 |
| - |
35 |
| -```vue |
36 |
| -<template> |
37 |
| -[...] |
38 |
| - <pre>Time in server rendered HTML: {{ data }}</pre> |
39 |
| -[...] |
40 |
| -</template> |
41 |
| -
|
42 |
| -<script setup lang="ts"> |
43 |
| -const { data } = await useFetch('/api/hello') |
44 |
| -</script> |
45 |
| -``` |
46 |
| - |
47 |
| -2. And one in the browser: |
48 |
| - |
49 |
| -```vue |
50 |
| -<pre>Time after hydration: {{ new Date().toUTCString() }} </pre> |
51 |
| -``` |
52 |
| - |
53 |
| -We utilize these two timestamps to demonstrate the functionality of each rendering mode, focusing on the hydration process. In case you're new to SSR frameworks: First, we send the browser a full-fledged HTML version of the initial state of our site. Then it get's hydrated, meaning Vue takes over, builds whatever it needs, runs client-side JavaScript if necessary and attaches itself to the existing DOM elements. From here on, everything works the same as with any other SPA. In our scenario, this implies that the current first `<pre>` element will always display the timestamp of the time the page got rendered by the browser, while the second `<pre>` element showcases the time Vue got the response from the API, thus when the HTML got rendered on the server. |
54 |
| - |
55 |
| -Our [API route](server/api/hello.ts) is as simple as this: |
56 |
| - |
57 |
| -```javascript |
58 |
| -export default defineEventHandler((event) => { |
59 |
| - return new Date().toUTCString(); |
60 |
| -}); |
61 |
| -``` |
62 |
| - |
63 |
| -Rendering modes are set up in [nuxt.config](nuxt.config.ts): |
64 |
| - |
65 |
| -```javascript |
66 |
| -export default defineNuxtConfig({ |
67 |
| - devtools: { enabled: true }, |
68 |
| - ssr: true, |
69 |
| - routeRules: { |
70 |
| - "/isr_ttl": { isr: 60 }, |
71 |
| - "/isr_no_ttl": { isr: true }, |
72 |
| - "/swr_ttl": { swr: 60 }, |
73 |
| - "/swr_no_ttl": { swr: true }, |
74 |
| - "/ssg": { prerender: true }, |
75 |
| - "/spa": { ssr: false }, |
76 |
| - }, |
77 |
| -}); |
78 |
| -``` |
79 |
| - |
80 |
| -#### Startup |
81 |
| -Start the example project with: |
82 |
| -```bash |
83 |
| -git clone [email protected]:RisingStack/nuxt3-rendering-modes.git |
84 |
| -cd nuxt3-rendering-modes |
85 |
| -pnpm install |
86 |
| -pnpm dev |
87 |
| -``` |
88 |
| - |
89 |
| -### Technical details and showcase |
90 |
| -#### SPA |
91 |
| - |
92 |
| -**Single Page Application** (also called **Client Side Rendering**). |
93 |
| - |
94 |
| -HTML elements are generated after the browser downloads and parses all the JavaScript code containing the instructions to create the current interface. |
95 |
| - |
96 |
| -We use the route `/spa` to illustrate how this rendering mode works: |
97 |
| - |
98 |
| -| Data | Value | |
99 |
| -| ------------------------------- | ----------------------------- | |
100 |
| -| Time in server rendered HTML | HTML response is blank | |
101 |
| -| Time in API response | Tue, 16 Jan 2024 09:47:10 GMT | |
102 |
| -| Time after hydration | Tue, 16 Jan 2024 09:47:10 GMT | |
103 |
| - |
104 |
| -As we can see in the table, the HTML response is blank, and the "Time after hydration" matches the "Time in API response". This occurs because the API request is made client-side. On subsequent requests or page reloads, the HTML response will always be blank, and the time will change with each request. However, the browser-rendered value and the API response value will consistently be the same. |
105 |
| - |
106 |
| -<img src="readme_assets/spa.gif" width="1200"/> |
107 |
| - |
108 |
| -To enable this mode, set up a route rule in nuxt.config as follows: |
109 |
| - |
110 |
| -```javascript |
111 |
| -export default defineNuxtConfig({ |
112 |
| - routeRules: { |
113 |
| - "/spa": { ssr: false }, |
114 |
| - }, |
115 |
| -}); |
116 |
| -``` |
117 |
| - |
118 |
| -#### SSR |
119 |
| -**Server Side Rendering** (also called **Universal Rendering**). |
120 |
| - |
121 |
| -The Nuxt server generates HTML on demand and delivers a fully rendered HTML page to the browser. |
122 |
| - |
123 |
| -We use the route `/ssr` to illustrate the behaviour of this rendering mode: |
124 |
| - |
125 |
| -| Data | Value | |
126 |
| -| ------------------------------- | ----------------------------- | |
127 |
| -| Time in server rendered HTML | Tue, 16 Jan 2024 09:47:45 GMT | |
128 |
| -| Time in API response | Tue, 16 Jan 2024 09:47:45 GMT | |
129 |
| -| Time after hydration | Tue, 16 Jan 2024 09:47:48 GMT | |
130 |
| - |
131 |
| -In this case, the "Time after hydration" might slightly differ from the "Time in API response" since the API response is generated beforehand. However, the timestamps will be very close to each other because the HTML generation occurs on demand and is not cached. This behavior will remain consistent across subsequent requests or page reloads. |
132 |
| - |
133 |
| -<img src="readme_assets/ssr.gif" width="1200"/> |
134 |
| - |
135 |
| -To enable this mode, enable SSR in `nuxt.config` as follows: |
136 |
| - |
137 |
| -```javascript |
138 |
| -export default defineNuxtConfig({ |
139 |
| - ssr: true |
140 |
| -}); |
141 |
| -``` |
142 |
| - |
143 |
| -#### SSG |
144 |
| -**Static Site Generation** |
145 |
| - |
146 |
| -The page is generated at build time, served to the browser, and will not be regenerated again until the next build. |
147 |
| - |
148 |
| -The route `/ssg` demonstrates SSG behavior: |
149 |
| - |
150 |
| -| Data | Value | |
151 |
| -| ------------------------------- | ----------------------------- | |
152 |
| -| Time in server rendered HTML | Tue, 16 Jan 2024 10:00:41 GMT | |
153 |
| -| Time in API response | Tue, 16 Jan 2024 10:00:41 GMT | |
154 |
| -| Time after hydration | Tue, 16 Jan 2024 10:09:09 GMT | |
155 |
| - |
156 |
| -In the table mentioned above, there is a noticeable time difference between the time after hydration and other timestamps. This is because, in SSG mode, HTML is generated during build time and remains unchanged afterward. This behavior will persist across subsequent requests or page reloads. |
157 |
| - |
158 |
| -<img src="readme_assets/ssg.gif" width="1200"/> |
159 |
| - |
160 |
| -To enable this mode, set up a route rule in nuxt.config as follows: |
161 |
| - |
162 |
| -```javascript |
163 |
| -export default defineNuxtConfig({ |
164 |
| - routeRules: { |
165 |
| - "/ssg": { prerender: true }, |
166 |
| - }, |
167 |
| -}); |
168 |
| -``` |
169 |
| - |
170 |
| -#### SWR |
171 |
| -**Stale While Revalidate** |
172 |
| - |
173 |
| -This mode employs a technique called stale-while-revalidate, which enables the server to provide stale data while simultaneously revalidating it in the background. The server generates an HTML response on demand, which is then cached. When deployed, the caching specifics can vary depending on the provider (eg. Vercel, Netlify, etc.), and information about where the cache is stored is usually not disclosed. There are two primary settings for caching: |
174 |
| - |
175 |
| -1. No TTL (Time To Live): This means the response is cached until there is a change in the content. |
176 |
| -2. TTL Set: This implies that the response is cached until the set TTL expires. |
177 |
| - |
178 |
| -Nuxt saves the API response that was used for generating the first version of the page. Then upon all subsequent requests, only the API gets called, until the response changes. When a change is detected during a request – with no TTL set – or when the TTL expires, the server returns the stale response and generates new HTML in the background, which will be served for the next request. |
179 |
| - |
180 |
| -##### SWR without TTL |
181 |
| - |
182 |
| -To observe the behavior of the SWR mode without a TTL set, you can take a look at the `/swr_no_ttl` route: |
183 |
| - |
184 |
| -| Data | Value - first request | Value - second request | Value - third request | |
185 |
| -| ------------------------------- | ----------------------------- | ----------------------------- | ----------------------------- | |
186 |
| -| Time in server rendered HTML | Tue, 16 Jan 2024 09:48:55 GMT | Tue, 16 Jan 2024 09:48:55 GMT | Tue, 16 Jan 2024 09:49:02 GMT | |
187 |
| -| Time in API response | Tue, 16 Jan 2024 09:48:55 GMT | Tue, 16 Jan 2024 09:48:55 GMT | Tue, 16 Jan 2024 09:49:02 GMT | |
188 |
| -| Time after hydration | Tue, 16 Jan 2024 09:48:58 GMT | Tue, 16 Jan 2024 09:49:03 GMT | Tue, 16 Jan 2024 09:49:10 GMT | |
189 |
| - |
190 |
| -Let's dissect the above table a bit. |
191 |
| - |
192 |
| -In the first column, the behavior is similar to that observed with [SSR](#ssr), as the "Time after hydration" slightly differs from the "Time provided in API response". In the second column, it appears the user waited around 5 seconds before reloading the page. The content is served from the cache, with only the time after hydration changing. However, this action triggers the regeneration of the page in the background due to the change in the API response since the first page load. As a result, a new version of the page is obtained upon the third request. To understand this, compare the "Time after hydration" in the second column with the "Time in server rendered HTML" in the third column. The difference is only about 1 second, indicating that the server's rendering of the third request occurred almost concurrently with the serving of the second request. |
193 |
| - |
194 |
| -<img src="readme_assets/swr_no_ttl.gif" width="1200"/> |
195 |
| - |
196 |
| -To enable this mode, set up a route rule in `nuxt.config` as follows: |
197 |
| - |
198 |
| -```javascript |
199 |
| -export default defineNuxtConfig({ |
200 |
| - routeRules: { |
201 |
| - "/swr_no_ttl": { swr: true }, |
202 |
| - }, |
203 |
| -}) |
204 |
| -``` |
205 |
| - |
206 |
| -##### SWR with TTL |
207 |
| -This rendering mode is set up on `/swr_ttl` route: |
208 |
| - |
209 |
| -| Data | Value - first request | Value - second request | Value - first request after TTL of 60 seconds passed | Value - second request after TTL of 60 seconds passed | |
210 |
| -| ------------------------------- | ----------------------------- | ----------------------------- | ---------------------------------------------------- | ---------------------------------------------------- | |
211 |
| -| Time in server rendered HTML | Tue, 16 Jan 2024 09:49:52 GMT | Tue, 16 Jan 2024 09:49:52 GMT | Tue, 16 Jan 2024 09:49:52 GMT | Tue, 16 Jan 2024 09:50:58 GMT | |
212 |
| -| Time in API response | Tue, 16 Jan 2024 09:49:52 GMT | Tue, 16 Jan 2024 09:49:52 GMT | Tue, 16 Jan 2024 09:49:52 GMT | Tue, 16 Jan 2024 09:50:58 GMT | |
213 |
| -| Time after hydration | Tue, 16 Jan 2024 09:49:55 GMT | Tue, 16 Jan 2024 09:50:00 GMT | Tue, 16 Jan 2024 09:51:00 GMT | Tue, 16 Jan 2024 09:51:06 GMT | |
214 |
| - |
215 |
| -In this scenario, the values of the first request in the `/swr_ttl` route are again similar to those observed in [SSR mode](#ssr), with only the time after hydration differing slightly from the other values. For the second and subsequent requests, until the TTL of 60 seconds expires, the "Time in API response" row retains the same timestamp as the first request. After the TTL expires (as shown in the third column), the time in the API response is still stale. However, in the fourth column, a new timestamp appears in the "Time in API response" row, indicating that the content has been updated post-TTL expiry. |
216 |
| - |
217 |
| -<img src="readme_assets/swr_ttl.gif" width="1200"/> |
218 |
| - |
219 |
| -To enable this mode, set up a route rule in nuxt.config as following: |
220 |
| - |
221 |
| -```javascript |
222 |
| -export default defineNuxtConfig({ |
223 |
| - routeRules: { |
224 |
| - "/swr_ttl": { swr: 60 }, |
225 |
| - }, |
226 |
| -}) |
227 |
| -``` |
228 |
| - |
229 |
| -#### ISR |
230 |
| -**Incremental Static Regeneration** (also called **Hybrid Mode**) |
231 |
| - |
232 |
| -This rendering mode operates similarly to SWR (Stale-While-Revalidate), with the primary distinction being that the response is cached on a CDN (Content Delivery Network). There are two potential settings for caching: |
233 |
| - |
234 |
| -1. No TTL (Time To Live): This implies that the response is cached permanently. |
235 |
| -2. TTL Set: In this case, the response is cached until the TTL expires. |
236 |
| - |
237 |
| -***Note***: ISR in Nuxt 3 differs significantly from ISR in Next.js in terms of HTML generation. In Nuxt 3, ISR generates HTML on demand, while in Next.js, ISR typically generates HTML during the build time by default. |
238 |
| - |
239 |
| -##### ISR without TTL |
240 |
| - |
241 |
| -This mode is available on the `/isr_no_ttl` route: |
242 |
| - |
243 |
| -| Data | Value - first request | Value - second request | Value - third request | |
244 |
| -| ------------------------------- | ----------------------------- | ----------------------------- | ----------------------------- | |
245 |
| -| Time in server rendered HTML | Tue, 16 Jan 2024 09:52:54 GMT | Tue, 16 Jan 2024 09:52:54 GMT | Tue, 16 Jan 2024 09:52:54 GMT | |
246 |
| -| Time in API response | Tue, 16 Jan 2024 09:52:54 GMT | Tue, 16 Jan 2024 09:52:54 GMT | Tue, 16 Jan 2024 09:52:54 GMT | |
247 |
| -| Time after hydration | Tue, 16 Jan 2024 09:52:56 GMT | Tue, 16 Jan 2024 09:53:03 GMT | Tue, 16 Jan 2024 09:53:11 GMT | |
248 |
| - |
249 |
| -In the table and screencast provided, it's evident that the value in the "Time in API response" row remains unchanged, even after 60 seconds have elapsed, which is typically the default TTL for Vercel. This observation aligns with the behavior of ISR without TTL in Nuxt 3, where the content is cached permanently. |
250 |
| - |
251 |
| -<img src="readme_assets/isr_no_ttl.gif" width="1200"/> |
252 |
| - |
253 |
| -To enable this mode, set up a route rule in `nuxt.config` as follows: |
254 |
| - |
255 |
| -```javascript |
256 |
| -export default defineNuxtConfig({ |
257 |
| - routeRules: { |
258 |
| - "/isr_no_ttl": { isr: true }, |
259 |
| - }, |
260 |
| -}) |
261 |
| -``` |
262 |
| -##### ISR with TTL |
263 |
| - |
264 |
| -The route `/isr_ttl` demonstrates ISR behaviour without TTL: |
265 |
| - |
266 |
| -| Data | Value - first request | Value - second request | Value - first request after TTL of 60 seconds passed | Value - second request after TTL of 60 seconds passed | |
267 |
| -| ------------------------------- | ----------------------------- | ----------------------------- | ---------------------------------------------------- | ---------------------------------------------------- | |
268 |
| -| Time in server rendered HTML | Tue, 16 Jan 2024 10:01:21 GMT | Tue, 16 Jan 2024 10:01:21 GMT | Tue, 16 Jan 2024 10:01:21 GMT | Tue, 16 Jan 2024 10:02:24 GMT | |
269 |
| -| Time in API response | Tue, 16 Jan 2024 10:01:21 GMT | Tue, 16 Jan 2024 10:01:21 GMT | Tue, 16 Jan 2024 10:01:21 GMT | Tue, 16 Jan 2024 10:02:24 GMT | |
270 |
| -| Time after hydration | Tue, 16 Jan 2024 10:01:24 GMT | Tue, 16 Jan 2024 10:01:28 GMT | Tue, 16 Jan 2024 10:02:25 GMT | Tue, 16 Jan 2024 10:02:32 GMT | |
271 |
| - |
272 |
| -For the first request on the `/isr_ttl` route, the observed values are similar to the [SSR behavior](#ssr) behavior, with only the time after hydration showing a slight difference. During the second and subsequent requests, until the TTL of 60 seconds passes, the "Time in API response" row retains the same timestamp as in the first request. After the TTL expires (as shown in the third column), the time in the API response remains stale. It's only in the fourth column that a new timestamp appears in the "Time in API response" row, indicating an update post-TTL expiry. |
273 |
| - |
274 |
| -<img src="readme_assets/isr_ttl.gif" width="1200"/> |
275 |
| - |
276 |
| -To enable this mode, set up a route rule in `nuxt.config` as follows: |
277 |
| - |
278 |
| -```javascript |
279 |
| -export default defineNuxtConfig({ |
280 |
| - routeRules: { |
281 |
| - "/isr_ttl": { isr: 60 }, |
282 |
| - }, |
283 |
| -}) |
284 |
| -``` |
285 |
| -Note that all the above mentioned rendering modes, except for ISR, can be easily tested in a local environment by building and previewing the app. ISR, however, relies on a CDN network for its functionality, which means it requires a CDN for proper testing. For example, deploying to Vercel would be necessary to test ISR effectively. |
| 1 | +This is a showcase project illustrating the technical details of the rendering modes available in Nuxt 3. For more information, please refer to the corresponding [article](https://blog.risingstack.com/nuxt-3-rendering-modes/) |
0 commit comments