The New Default. Your hub for building smart, fast, and sustainable AI software
Table of Contents
and 6 more
Editor’s note: Jakub Andrzejewski wrote this guide in March 2025. In October 2026, Monterail’s editorial team updated it for Nuxt 4.5. We corrected the code samples to match how Nuxt islands behave today, and added sections on how islands work under the hood, security, trade-offs, and when not to use them.
Nuxt server components are Vue components that render only on the server. Their HTML goes into the page, but their JavaScript, and the libraries they depend on, never reach the browser.
You create one by adding a .server.vue suffix to a component file. Nuxt then renders it as an “island”: a static piece of server-rendered HTML inside an otherwise interactive Nuxt app.
Executive Summary
Most Vue apps make users download, parse, and execute JavaScript whose only job is to recreate HTML that’s already on the screen. Nuxt server components remove that cost one component at a time.
Markdown renderers, syntax highlighters, CMS blocks, and footers stay on the server, along with their dependencies, and the browser gets plain HTML. The payoff is smaller bundles, faster interactivity, and simpler handling of secrets. The cost is an extra network round trip on client-side navigation, a feature that’s still marked experimental, and a strict isolation model your team has to understand.
Used selectively on heavy, non-interactive content, islands are one of the cheapest performance wins in the Vue ecosystem. Used everywhere, they slow your app down.
What Are Server Components in Nuxt?
A Nuxt server component (also called a server-only component or island component) renders on the server and sends only HTML to the client, with zero component JavaScript. Nuxt.js, the powerful Vue framework, has always been at the forefront of modern web development. It offers server-side rendering (SSR), static site generation (SSG), and hybrid rendering. Server components add a finer-grained option on top of those modes: instead of deciding how a whole page renders, you decide for a single component.
That distinction matters because of how standard SSR works. Nuxt’s own documentation puts it plainly. Nuxt renders your app on the server by default, but then ships the JavaScript for every component to the browser and hydrates the whole page. For content that never changes 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 invert that. Unlike traditional Vue components, which render on the server and then rehydrate with JavaScript on the client, server components are processed entirely on the server. They don’t need to be sent as JavaScript to the browser, which minimizes client-side execution. A heavy markdown parser or syntax highlighter used inside a server component never enters your client bundle.
Server components let Nuxt developers keep the interactive feel of a single-page app while paying the JavaScript cost only for the parts that are actually interactive. As Daniel Roe, who leads the Nuxt core team, notes in his guide to Nuxt server components, they aren’t a magic bullet. They’re “a useful option when there is [a] disproportionate amount of code needed to render a component on the client.”
Why Do Nuxt Server Components Matter for Your Product?
Server components matter because JavaScript is the most expensive asset you ship. Every kilobyte you keep on the server is one users on mid-range phones don’t have to download, parse, and execute. As web applications with Nuxt grow in complexity, performance stops being a nice-to-have and starts showing up in conversion rates, search rankings, and support tickets.
Here’s what that means in business terms:
Smaller bundles, faster interaction. The component’s code and its dependencies stay on the server. That directly reduces the main-thread work that delays interactivity on content-heavy pages.
Secrets stay server-side. A server component can call a database or a private API with credentials the browser never sees. Daniel Roe calls this one way of “separating your concerns.” A Nitro server route is the other common option.
SEO-friendly by construction. Search engines receive complete HTML without waiting for client-side rendering. That’s true of any Nuxt SSR, but islands let you keep that benefit while shrinking the JavaScript cost of each page.
No server required for static sites. By default, Nuxt prerenders server components during
nuxt generate, so they work on static hosting too, as long as their props don’t change at runtime (roe.dev).
The link between rendering strategy and revenue isn’t theoretical. When Monterail rebuilt Easyship’s website on Nuxt after an AngularJS version the team described as “way too slow and an SEO nightmare,” loading times and overall performance improved by 37% and organic traffic grew by 14%. That project relied on Nuxt’s server-side rendering, not islands specifically. Islands are the next, more surgical step in the same direction: render on the server whatever doesn’t need the browser.
How Do Nuxt Server Components Work Under the Hood?
Each server component is rendered by a dedicated “island” endpoint that spins up a new, isolated Vue app on the server, renders just that component, and returns its HTML. Server components use the <NuxtIsland> component under the hood. Knowing what that implies saves teams from most of the mistakes covered later in this guide.
What happens on first load vs. client-side navigation?
On the first server-rendered page load, islands render inline with no extra request. On client-side navigation, the browser fetches each island on the destination page from the server. According to the Nuxt docs, this has real costs:
Islands block navigation on a network round trip unless you pass the
lazyprop together with a#fallbackslot.A page with many islands makes many requests per navigation.
Islands work best on pages that people reach through full page loads, such as content and marketing pages, or where there are only a few islands per page. When a <NuxtLink> points to a server-only page, Nuxt prefetches the island response too, so the round trip happens before the user clicks.
Why can’t an island share state with the rest of the page?
Because every island is rendered in its own isolated Vue app, so state doesn’t cross the island boundary. In practice, the Nuxt docs say:
You can’t share state (provide/inject, Pinia,
useState) between the page and the island. Pass data through props instead.useRoute()inside an island reflects the island’s own request, not the page the user is on. Pass route information explicitly.Route middleware doesn’t run when islands render.
Your plugins run again for each island, unless an object-syntax plugin sets
env: { islands: false }.
How are props sent to an island?
Props are serialized as JSON and sent as GET query parameters. That makes island responses cacheable by your server or CDN, but it also means:
Props must be JSON-serializable.
Props are limited by URL length, so don’t pass large amounts of data.
Props may show up in server access logs, CDN caches, and
Refererheaders.Props are untrusted user input (more on this in the trade-offs section).
Changing an island’s props triggers a new request that re-renders the component on the server and swaps its HTML in place.
How do server components compare with other Nuxt rendering options?
Use the following table to choose the right rendering option for each component, not for the whole app.
Option | Where it renders | Component JS shipped to the browser? | Interactive? | Cost on client-side navigation | Best for |
|---|---|---|---|---|---|
Regular component | Server (SSR), then hydrated on the client | Yes | Yes | None beyond the bundle | Most UI |
| Browser only | Yes | Yes | None beyond the bundle | Browser-only APIs, widgets that can’t SSR |
| Server only | No | No (except | One request per island | Heavy, static content |
Lazy hydration ( | Server, hydrated later or never | Yes (loaded lazily) | Yes, once hydrated | None beyond the bundle | Below-the-fold interactive components |
Lazy hydration deserves a mention here because it solves a neighboring problem. If a component does need to be interactive, just not immediately, hydrate-on-visible or hydrate-on-interaction is usually a better choice than an island. Both are part of a wider toolkit of Vue performance techniques worth knowing.
How Are Nuxt Islands Different from React Server Components and Astro Islands?
Nuxt server components share a name with React Server Components (RSCs) but work differently, and they’re the mirror image of Astro-style islands. Daniel Roe describes RSCs as “an entirely different approach to rendering server components which is often linked to streaming responses from server to client.” Nuxt islands are rendered through a dedicated endpoint and embedded as HTML.
The comparison with Astro is even more useful. The “islands architecture” popularized by Astro and îles embeds dynamic islands inside a static page. In Roe’s words, “The Nuxt approach is the opposite – we embed static ‘islands’ within a dynamic Nuxt app.”
Nuxt server components | React Server Components (Next.js) | Astro islands | |
|---|---|---|---|
Default mode of the app | Interactive SPA with SSR | Server-first component tree | Static HTML |
What the “island” is | A static, server-rendered component | Not an island model | An interactive component |
How it’s delivered | Inline on first load; island endpoint on navigation | Streamed RSC payload | Hydrated JS per island |
Status | Experimental | Stable in Next.js App Router | Stable |
If your team is weighing Nuxt against Next.js, this difference matters more than any feature checklist. React makes server components the default and asks you to opt into client code. Nuxt makes interactive components the default and lets you opt individual components out of the client.
How Do You Create and Use a Server-Only Component in Nuxt 4?
Add the .server.vue suffix to a component file and use it like any other component. In current Nuxt versions, no configuration is needed.
Step 1: Check your configuration (you probably don’t need any)
Older guides, including the first version of this one, told you to set experimental.componentIslands: true. That’s no longer necessary. According to the Nuxt 4 docs, the option defaults to 'auto', which turns the feature on as soon as your app contains a server component or island.
You only need explicit configuration for selective client hydration or remote islands:
// 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 progress on the server components roadmap issue on GitHub.
Step 2: Create a
Here’s a server component that fetches a post and renders it. The parent passes a small identifier, not the whole post, and the component fetches its own data on the server:
<!-- app/components/BlogPost.server.vue -->
<script setup lang="ts">
const props = defineProps<{ slug: string }>()
const post = await $fetch<{ id: number; title: string; content: string }>(
`https://api.example.com/posts/${props.slug}`,
)
</script>
<template>
<article>
<h1>{{ post.title }}</h1>
<p>{{ post.content }}</p>
</article>
</template>
Use it anywhere, exactly like a regular component:
<!-- app/pages/blog/[slug].vue -->
<script setup lang="ts">
const route = useRoute()
</script>
<template>
<div>
<BlogPost :slug="String(route.params.slug)" />
</div>
</template>
Two rules to remember:
A server component must have a single root element. HTML comments count as elements, so a leading comment breaks this rule.
Await your async calls. Without
await, rendering can finish before the data arrives.
Step 3: Add interactivity where you need it
An island is static by default, so a button inside it won’t respond to clicks. You have two supported ways to add interactive pieces.
Option A: hydrate individual children with nuxt-client. With selectiveClient enabled (see Step 1), add the nuxt-client attribute to a component inside the island. Nuxt server-renders it as part of the island, then hydrates only that component in the browser:
<!-- app/components/BlogPost.server.vue -->
<template>
<article>
<h1>{{ post.title }}</h1>
<p>{{ post.content }}</p>
<!-- Only LikeButton's chunk is shipped to the client -->
<LikeButton nuxt-client :post-id="post.id" />
</article>
</template>
Use nuxt-client only on your own local .vue single-file components. Built-ins like <NuxtLink> skip the islands transform, so wrap them in your own .vue file first (nuxt/nuxt#29251).
Option B: pass interactive content through a slot. Slot content comes from the parent, so it belongs to the main client app and is interactive:
<!-- app/components/BlogPost.server.vue -->
<template>
<article>
<h1>{{ post.title }}</h1>
<p>{{ post.content }}</p>
<slot />
</article>
</template>
<!-- app/pages/blog/[slug].vue -->
<script setup lang="ts">
const slug = String(useRoute().params.slug)
</script>
<template>
<BlogPost :slug="slug">
<LikeButton :post-id="slug" />
</BlogPost>
</template>
<ClientOnly> isn’t a replacement for either option. Inside an island, nothing hydrates unless it’s marked nuxt-client or provided by the parent through a slot.
Step 4: Avoid blocking navigation with
On client-side navigation, each island is a network request. Pass the lazy prop and a #fallback slot so the page can render while the island loads. The fallback also shows if fetching the island fails:
<template>
<BlogPost :slug="slug" lazy>
<template #fallback>
<p>Loading article…</p>
</template>
</BlogPost>
</template>
Step 5 (optional): Render islands directly with
Components inside ~/components/islands/ are registered as islands, and you can render them with <NuxtIsland> directly. That gives you access to its error event and refresh() method, which the auto-generated .server.vue wrapper doesn’t expose (nuxt/nuxt#25744):
<template>
<NuxtIsland name="PostStats" :props="{ slug }" lazy>
<template #fallback>
<p>Loading stats…</p>
</template>
</NuxtIsland>
</template>
You can also pair Comments.server.vue with Comments.client.vue. Nuxt renders the server half first and swaps in the client half once it mounts. In that case the component isn’t an island: the client half hydrates normally.
When Should You Use Nuxt Server Components? Real-World Use Cases
Use server components for content that’s expensive to render on the client, doesn’t change after it loads, and appears a limited number of times per page. The use cases below show where they shine, and where to be careful.
Content-heavy websites (blogs, news, documentation). A documentation site like Vue’s official docs could use server components to render markdown files or headless CMS content on the server, keeping the parser and syntax highlighter out of the client bundle. The Nuxt docs use exactly this example: a
HighlightedMarkdown.server.vuecomponent whose “markdown parsing and highlighting libraries are not included in your client bundle.” This is the canonical use case. Pair it with prerendering, and identical islands are rendered once and reused.Site-wide static chrome (footers, mega-menus, legal blocks). Daniel Roe converted his site footer simply by adding
.server.vue, because “there’s no need for any of that to be dynamic.” Since Nuxt 4.5.2, links rendered inside server components navigate client-side out of the box, which removes a common workaround (roe.dev).Personalized content and server-only secrets. A user dashboard with personalized recommendations can fetch user-specific data without exposing API keys to the front end. Be careful, though: island responses are cacheable and keyed on name, props, and context. Make sure personalized islands are never cached at the CDN layer, and remember that route middleware doesn’t run for islands. Enforce authorization inside the data layer itself.
Server-side API aggregation. Travel booking platforms aggregate data from multiple services (flights, hotels) on the server before sending a single response, which minimizes client-side API calls. An island can render the aggregated result as HTML. If the client needs the raw data, for filtering or sorting for instance, a Nitro server route running on your Node.js backend is the better tool.
AI and machine learning output. AI-driven recommendations or generated text can be produced on the server, with only the final result sent to the client, which protects proprietary prompts and model logic. Slow inference inside an island blocks navigation, though. Use
lazywith a#fallback, or stream the result from a server route if responses take more than a moment.SEO-critical landing pages. On a community platform for NFT fans, Monterail picked Nuxt specifically to increase the app’s visibility in Google search results. Islands extend that logic. Marketing pages reached by full page loads get complete HTML with a fraction of the JavaScript, and that’s precisely where islands have no navigation penalty.
What Are the Most Common Mistakes with Nuxt Server Components?
Nearly every mistake comes from forgetting one fact: an island is a separate, static Vue app rendered on the server. Nuxt server components bring a lot of value, but don’t overuse them, and watch out for these pitfalls.
Mistake 1: Expecting an island to share state with the page
useState, useFetch, and useAsyncData do run inside a server component, on the server. The problem is that the island is an isolated app, so its state is never shared with the page around it:
<!-- app/components/CartSummary.server.vue -->
<script setup lang="ts">
// ❌ This is a separate 'cart' from the one the page uses
const cart = useState('cart', () => [])
</script>
Fix: pass the data the island needs in as props. If the component needs to read and update shared client state, it shouldn’t be an island. Make it a regular component, or a nuxt-client child that uses your Pinia store.
Mistake 2: Passing large data sets through props
Fetching in the page and passing results down is a fine pattern for regular components. For islands, it backfires, because props travel as GET query parameters:
<!-- app/pages/blog/index.vue -->
<script setup lang="ts">
const { data: posts } = await useAsyncData('posts', () => $fetch('/api/posts'))
</script>
<template>
<!-- ❌ If PostList is a .server.vue component, the whole array is serialized into a URL -->
<PostList :posts="posts" />
</template>
Fix: pass a small identifier and let the island fetch its own data on the server:
<!-- app/components/PostList.server.vue -->
<script setup lang="ts">
const props = defineProps<{ category: string }>()
const posts = await $fetch<{ id: number; title: string }[]>('/api/posts', {
query: { category: props.category },
})
</script>
<template>
<ul>
<li v-for="post in posts" :key="post.id">{{ post.title }}</li>
</ul>
</template>
Mistake 3: Putting interactive logic inside an island
Event handlers and local state in a server component never run in the browser, because none of its JavaScript is shipped:
<!-- app/components/Counter.server.vue -->
<script setup lang="ts">
let count = 0
const increment = () => count++ // ❌ Never runs in the browser
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
Fix: move the interactive part into a regular component and render it with nuxt-client or through a slot (see Step 3).
Mistake 4: Reading the route inside an island
<script setup lang="ts">
const route = useRoute() // ❌ Reflects the island's own request, not the user's page
</script>
Fix: read the route in the page and pass what the island needs as props, or use the context prop on <NuxtIsland>.
Mistake 5: Forgetting to await async calls
<script setup lang="ts">
const posts = $fetch('/api/posts') // ❌ Missing await
</script>
Fix: always await async calls so the data is there before rendering:
<script setup lang="ts">
const posts = await $fetch('/api/posts')
</script>
Mistake 6: Trusting island props
Island props come from the request URL, so anyone can craft them. Never feed unvalidated props into <component :is>, h(), or resolveDynamicComponent(). Map them through an allowlist instead, as the Nuxt docs recommend:
<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>
Mistake 7: Making every component a server component
Don’t make every component a server component. Each island adds a network request on client-side navigation, islands can significantly increase the number of chunks generated at build time (nuxt/nuxt#34855), and each nested island adds overhead. Use server components for heavy, static, non-interactive content, and regular or lazily hydrated components for everything else.
What Are the Trade-Offs and Limitations of Nuxt Server Components?
The main trade-offs are experimental status, a network round trip per island on client-side navigation, an isolation model that blocks shared state, and props that must be treated as untrusted input. There’s no free lunch. Here’s what your team signs up for.
Is it production-ready?
Server components are still officially experimental in Nuxt 4.5, but they’ve been available since Nuxt 3 and are used in production, including on the Nuxt core team lead’s own site. The practical risk isn’t sudden removal. It’s rough edges and behavior changes between minor releases. Pin your Nuxt version, read release notes, and cover islands with end-to-end tests.
What security issues should you know about?
Keep Nuxt patched. Nuxt v4.5.1, released in July 2026, was a critical security release that, among other fixes, addressed “server-side RCE and unauthorized component instantiation via server island props” and a “server component DoS” (release notes). The Nuxt team recommended upgrading immediately with npx nuxt upgrade --dedupe, and purging CDN caches if you use cache, swr, or isr route rules. Treat island props the way a web application security assessment would: as untrusted input.
The upgrade path matters too. Nuxt 3 reached end of life on July 31, 2026, and no longer receives security patches, and Nuxt 5 is estimated for Q4 2026 (Nuxt roadmap). If you’re still on Nuxt 3, upgrade to Nuxt 4 before investing heavily in islands.
What are the known technical limitations?
The Nuxt docs list these current rough edges:
Most island features, such as slots and
nuxt-client, only work with single-file components.Template refs can’t reference elements inside a server component from the parent (#31512).
inject/providedoesn’t cross the island boundary (#22751).useIdcan produce colliding IDs inside islands, and doesn’t work in interactive components inside islands (NuxtIsland docs).With webpack and Rspack, scoped
:slotted()styles in server component slots can fail (#31510).On routes rendered with
noScripts,nuxt-clientcomponents won’t hydrate. Plain static islands are unaffected.
Should this component be a server component? A quick checklist
Question | If yes | If no |
|---|---|---|
Does it need event handlers, local state, or browser APIs? | Regular or client component (or a |
|
Does it pull in a heavy library (markdown, highlighting, date/i18n formatting)? |
| Benefit is smaller |
Does it need data from Pinia, | Pass it as small props, or don’t use an island |
|
Will its input exceed what fits comfortably in a URL? | Pass an ID and fetch inside the island |
|
Does it appear many times per page, or on pages reached mostly through client-side navigation? | Use with care, and consider |
|
Does it need to update frequently on the client? | Not an island |
|
Are Nuxt Server Components Worth It for Your Team in 2026?
Yes, for the right components. If your Vue app ships heavy rendering libraries for content that never changes, islands are one of the most cost-effective performance improvements available. They’re worth less if your UI is mostly interactive, or if most navigation happens client-side across many small components.
The best results come from treating rendering as a per-component decision. Keep interactive UI as regular components, defer below-the-fold interactivity with lazy hydration, and move heavy, static content into server components. That kind of hybrid architecture is one more reason teams choose Nuxt over all-or-nothing rendering models.
Getting there takes judgment: knowing which components to convert, how to measure the impact, and how to avoid the isolation and security pitfalls. It’s the kind of work Monterail’s Vue.js and Nuxt developers do on custom web applications every day. As Monterail’s conversation with Daniel Roe about working on Nuxt shows, the framework rewards teams that understand how it works under the hood.
FAQ
)





