Server Components

Render individual components on the server only, keeping their JavaScript out of your client bundle.

Nuxt renders your app on the server by default, but then it ships the JavaScript for every component to the browser and hydrates the whole page. For content-heavy components (markdown rendering, syntax highlighting, CMS output) that never change on the client, this is wasted work: the user downloads, parses and executes code whose only job is to reproduce HTML that is already on the page.

Server components (also called island components) invert this. A server component is rendered on the server, its HTML is embedded in the page, and none of its JavaScript is sent to the client. Its dependencies (a markdown parser, a highlighting library) stay on the server too.

Read Daniel Roe's guide to Nuxt Server Components.

Enabling Server Components

Component islands are controlled by experimental.componentIslands. The default value is 'auto', which enables the feature automatically as soon as your app contains a server component or island, so in most cases you do not need any configuration.

Set the option explicitly if you want remote islands or selective client hydration:

nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    componentIslands: {
      selectiveClient: true, // or 'deep', to enable `nuxt-client`
      remoteIsland: false, // allow rendering islands from a remote source
    },
  },
})
Server components are still marked experimental. You can follow the roadmap on GitHub.

.server.vue Components

Add the .server suffix to a component to make it a standalone server component:

Directory Structure
-| app/
---| components/
-----| HighlightedMarkdown.server.vue

Use it like any other component:

app/pages/example.vue
<template>
  <div>
    <!--
      rendered on the server; the markdown parsing and highlighting
      libraries are not included in your client bundle
     -->
    <HighlightedMarkdown markdown="# Headline" />
  </div>
</template>

Components inside ~/components/islands/ are also registered as islands and can be rendered with <NuxtIsland> directly, for example <NuxtIsland name="MyIsland" /> for ~/components/islands/MyIsland.vue.

You can also pair a .server.vue component with a .client.vue component of the same name for separate server and client implementations. In that case the component is not an island: the client half hydrates normally.

Server components (and islands) must have a single root element. (HTML comments are considered elements as well.)

How Islands Are Rendered

Server components use <NuxtIsland> under the hood. Rendering an island issues a request to a dedicated island endpoint, which:

  • creates a new, isolated Vue app on the server to render just that component
  • creates an 'island context' that you can access via nuxtApp.ssrContext.islandContext inside the island
  • runs your plugins again, unless they set env: { islands: false } (object-syntax plugins)

Because the island is isolated from the rest of your app:

  • you cannot share state (provide/inject, Pinia, useState) between the page and the island; pass data via props instead
  • useRoute() inside an island reflects the island's own request, not the page the user is on. If an island needs route information, pass it in explicitly, either as props or via the context prop on <NuxtIsland> (read inside the island from nuxtApp.ssrContext.islandContext)
  • route middleware does not run when rendering islands

Props are serialized and sent as GET query parameters. This makes island responses cacheable, but it also means:

  • props must be JSON-serializable
  • props are limited by URL length, so avoid passing large amounts of data
  • props may be visible in server access logs, CDN caches and Referer headers
Because props come from the request (URL query or body), treat them as untrusted input. Nuxt rejects the props most likely to leak through unintentionally: a top-level as that the island does not declare (an undeclared prop falls through as an attribute onto the island's root), and, with vue.runtimeCompiler enabled, a template anywhere in the props. Beyond that, avoid feeding props you have not validated into dynamic component resolution (<component :is>, h(), resolveDynamicComponent(), or a polymorphic as / asChild prop), since a string can resolve to any registered component or HTML element.Props a component does not declare fall through as attributes onto its single root element, so an island whose root is a polymorphic component (e.g. from reka-ui / @nuxt/ui) can receive attributes you did not bind. Set defineOptions({ inheritAttrs: false }) on such islands, or declare the props you accept.To switch components based on caller input, map a discriminator through an allowlist of imported components rather than passing the raw prop:
<script setup lang="ts">
import type { Component } from 'vue'
import CardA from './CardA.vue'
import CardB from './CardB.vue'

const props = defineProps<{ variant: string }>()
const allowed: Record<string, Component> = { a: CardA, b: CardB }
const component = allowed[props.variant] ?? CardA
</script>

<template>
  <component :is="component" />
</template>

Changing an island's props triggers a network request that re-renders the component on the server and updates its HTML in place.

Read the full <NuxtIsland> API documentation, including props, slots, events and known limitations.

Selective Hydration with nuxt-client

An island is static by default, but you can hydrate individual components inside it by adding the nuxt-client attribute. This requires experimental.componentIslands.selectiveClient to be enabled.

app/components/ServerWithClient.server.vue
<template>
  <div>
    <HighlightedMarkdown markdown="# Headline" />
    <!-- Counter will be loaded and hydrated client-side -->
    <Counter
      nuxt-client
      :count="5"
    />
  </div>
</template>

The component marked with nuxt-client is server-rendered as part of the island, then hydrated by the main client app. Only its chunk is shipped to the client; the rest of the island remains static.

Setting selectiveClient: 'deep' additionally allows passing slots to nuxt-client components. Those slots are rendered on the server and are not interactive on the client.

Use nuxt-client only on local .vue SFCs. Built-ins like <NuxtLink> skip the islands transform. After client navigation you may see Failed to locate Teleport target, or the link disappears with no error. Wrap the built-in in your own .vue file and put nuxt-client on that wrapper. See #29251 and #26002.

Slots

Slots can be passed to an island component if declared in the island. Slot content is provided by the parent, so it belongs to the main client app and is interactive (it is wrapped in a <div> with display: contents;).

<NuxtIsland> reserves the #fallback slot to specify content rendered before the island loads (when lazy is set) or when fetching the island fails.

The Client Navigation Round Trip

On the initial server-rendered page load, islands are rendered inline and there is no extra request. On client-side navigation, however, each island on the destination page must be fetched from the server (you can see these requests in the network tab). This has real costs:

  • islands block on a network round trip during navigation, unless you pass the lazy prop (with a #fallback slot) to render them non-blockingly
  • an app with many islands per page makes many requests per navigation

Islands work best on pages that are reached by full page loads (content and marketing pages) or when their number per page is small. If a component needs to update frequently on the client, an island is probably the wrong tool.

Prerendering and Caching

Islands play well with static and cached rendering:

  • during prerendering (nuxt generate or prerender route rules), island responses are cached, so identical islands (same name, props and context) are rendered once and reused
  • because props travel as GET query parameters, island responses can also be cached by your server or CDN at the island endpoint level
  • two instances of the same island with the same props share a single server render and payload entry

Note that island responses being keyed only on name, props and context is exactly what keeps them cacheable independently of the page they appear on; this is also why they cannot see the current route (see above).

If you are building a mostly-static site, islands combine well with prerender and noScripts route rules.

See the mostly-static site recipe for combining prerendering, noScripts, islands and lazy hydration.

One interaction to be aware of: island slots and nuxt-client components rely on a small inline script to relocate teleported content into place before hydration. On routes rendered with noScripts, that script is omitted, so fully interactive nuxt-client components will not hydrate there. Plain static islands are unaffected.

Current Limitations

Server components are experimental, and some rough edges are tracked in open issues:

  • Most features for server-only and island components, such as slots and nuxt-client components, are only available for single file components.
  • Global styles of your application are sent with each island response, and island assets can be loaded twice in some setups (#29591).
  • Using islands can significantly increase the number of chunks generated at build time (#34855).
  • :slotted() styles are ignored in server component slots (#31510).
  • Template refs cannot reference elements inside a server component from the parent (#31512).
  • inject/provide does not cross the island boundary, so injecting from the page into a standalone server component does not work (#22751).
  • Server components rendered via the auto-generated wrapper do not expose load and error events; use <NuxtIsland> directly if you need its error event and refresh() method (#25744).
  • useId has known limitations inside islands; see the <NuxtIsland> documentation.
  • Each nested island adds extra overhead, so be careful when nesting islands within other islands.
Read more about server component file conventions in the components directory documentation.