Learn how to enhance your Nuxt apps by using server components for selective server-side rendering, without sacrificing client-side interactivity. Discover practical examples and techniques to optimize performance and maintain flexibility in your application's architecture.
Get notified when we release new tutorials, lessons, and other expert Nuxt content.
Server components in Nuxt allow us to embed islands of server-only rendering inside of our apps.
This way, we don’t have to give up client-side interactivity, but can still gain the benefits of SSR where we need it.
Using a server component is easy. Once enabled, we just use the *.server.vue
suffix and Nuxt does the rest.
But server components can be more interesting than that! Let’s look at a few examples.
Nuxt uses the NuxtIsland
component internally for server components and pages, but you can also use it directly if you need to:
<NuxtIsland
name="Counter"
:props="{
startingCount,
}"
/>
The name
is the same name you’d use in a template to use an auto-imported component (the component needs to be global
). This means a component at ~/components/server/Counter.server.vue
would be used like this with the filename prepended:
<NuxtIsland
name="ServerCounter"
:props="{
startingCount,
}"
/>
This component must be a server component though! The easiest way to do this is by putting the *.server.vue
suffix on it.
Whenever the props of this component change on the client, a new request will be made to the server to re-render the component. You can also force this behaviour by using the refresh
method on the ref
:
<template>
<NuxtIsland
name="Counter"
ref="serverCounter"
:props="{
startingCount,
}"
/>
</template>
<script setup>
const serverCounter = ref(null);
async function forceRefresh() {
if (!serverCounter.value) return;
await serverCounter.value.refresh();
}
</script>
However, keep in mind that each NuxtIsland
component is rendered as a full Nuxt app on the server. So having multiple of these on a single page can lead to performance issues.
You can read more about this component in the docs.
We can easily set a page in our /pages
folder to be either client-side or server-side rendered only, just by changing the suffix.
The client-page.client.vue
will only render on the client-side. The exception is for any server components that you have in there. Those will be rendered on the server:
<template>
<div>Client page: {{ count }}</div>
<!-- Rendered like a normal server component would -->
<JustServer />
</template>
<script setup lang="ts">
const count = ref(0);
onMounted(() => {
setInterval(() => {
count.value++;
}, 1000);
});
</script>
But server-page.server.vue
will be rendered only on the server, disabling any interactivity:
<template>
<div>Server page: {{ count }}</div>
</template>
<script setup lang="ts">
const count = ref(6);
onMounted(() => {
setInterval(() => {
count.value++;
}, 1000);
});
</script>
If you navigate to /server-page
you’ll get “Server page: 6”, and that’s it. No counting, since the Javascript is never shipped.
Remember, server components are required to have a single root node, and a server page is really just a server component:
<template>
<div>
<div>Server page.</div>
<div>With multiple root nodes.</div>
</div>
</template>
But unlike a regular server component, you cannot nest interactive components within it, unless you have set experimental.componentIsland.selectiveClient
in your config to deep
:
<template>
<div>
<div>Server page: {{ count }}</div>
<!-- Renders "17" but doesn't become interactive by default -->
<Counter nuxt-client :starting-count="17" />
</div>
</template>
You can write more complex components that have different logic on the server and the client by splitting them into paired server components. All you need to do is keep the same name, changing only the suffix to *.client.vue
and *.server.vue
.
For example, in our Counter.server.vue
we set up everything we want to run during SSR:
<template>
<div>This is paired: {{ startingCount }}</div>
</template>
<script setup lang="ts">
withDefaults(defineProps<{ startingCount: number }>(), {
startingCount: 0,
});
</script>
We grab our startingCount
prop and render it to the page — no need to do anything else because we’re not interactive at this point.
Then, Nuxt will find Counter.client.vue
and ship that to the client in order to hydrate and make the component interactive:
<template>
<div>This is paired: {{ count }}</div>
</template>
<script setup lang="ts">
const props = withDefaults(
defineProps<{ startingCount: number }>(),
{
startingCount: 0,
}
);
const offset = ref(0);
const count = computed(
() => props.startingCount + offset.value
);
onMounted(() => {
setInterval(() => {
offset.value++;
}, 1000);
});
</script>
We’re careful to make sure we avoid hydration mismatches, and we bootstrap our interactivity.
A nice feature to improve separation of concerns where you need it!
In this article we saw a few different things we can do with server components.
We can use the NuxtIsland
component directly, if we need more control over how things are being rendered. Instead of just server components, we can also have server pages, which are only ever server rendered.
And if our components get too complex, we can create paired server components, by splitting up the server-side and client-side implementations and keeping them in two separate files.
There’s a lot more we can do with server components, but even just these few things can get us pretty far!