Exploring Server Components in Nuxt

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.

Michael Thiessen
Nuxt 3

Mastering Nuxt 3 course is here!

Get notified when we release new tutorials, lessons, and other expert Nuxt content.

Click here to view the Nuxt 3 course

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.

The NuxtIsland Component

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.

Client-only and Server-only Pages

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>

Paired Server Components

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!

Wrapping Up

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!

Michael Thiessen
Michael is a passionate full time Vue.js and Nuxt.js educator. His weekly newsletter is sent to over 11,000 Vue developers, and he has written over a hundred articles for his blog and VueSchool.

Follow MasteringNuxt on