Server Components
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.
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:
export default defineNuxtConfig({
experimental: {
componentIslands: {
selectiveClient: true, // or 'deep', to enable `nuxt-client`
remoteIsland: false, // allow rendering islands from a remote source
},
},
})
.server.vue Components
Add the .server suffix to a component to make it a standalone server component:
-| app/
---| components/
-----| HighlightedMarkdown.server.vue
Use it like any other component:
<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.
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.islandContextinside 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 thecontextprop on<NuxtIsland>(read inside the island fromnuxtApp.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
Refererheaders
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.
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.
<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.
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
lazyprop (with a#fallbackslot) 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 generateorprerenderroute 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.
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-clientcomponents, 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/providedoes 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 itserrorevent andrefresh()method (#25744). useIdhas known limitations inside islands; see the<NuxtIsland>documentation.- Each nested island adds extra overhead, so be careful when nesting islands within other islands.