Mostly-Static Sites

Ship near-zero JavaScript for content and marketing sites while keeping islands of interactivity.

By default, Nuxt server-renders a page and then hydrates all of it. For a page that is 95% static text and images, that hydration JavaScript is mostly overhead: it delays interactivity and competes with your content for bandwidth and CPU, which can hurt metrics like mobile PageSpeed scores.

Nuxt has the pieces to avoid paying that cost where you don't need it. This recipe combines them:

  1. Prerender your content routes at build time.
  2. Drop the scripts on those routes with the noScripts route rule.
  3. Use server components for widgets that need server rendering but no interactivity.
  4. Use lazy hydration on routes that keep their scripts, so interactivity is paid for only when needed.

Prerender and Drop Scripts

For routes that have no client-side interactivity at all, combine prerender and noScripts in routeRules:

nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    '/blog/**': { prerender: true, noScripts: true },
    '/about': { prerender: true, noScripts: true },
  },
})

These routes are rendered to plain HTML at build time, and the rendered pages omit the Nuxt entry scripts, import map, payload script and JS resource hints. CSS is still included. The result is a page with effectively zero JavaScript cost.

Read more about exactly what noScripts omits, and the app-wide features.noScripts option.
A noScripts page does not hydrate. Nothing on it is interactive: no @click handlers, no <NuxtLink> prefetching (links still work as plain <a> tags), no client-side navigation away from the page. Only use it on routes where that is what you want.

Islands for Server-Rendered Widgets

Some parts of a static page still benefit from being components with real logic behind them: rendered markdown, syntax-highlighted code, CMS content. Use server components for these. They render on the server, and their dependencies never reach the client bundle:

Directory Structure
-| app/
---| components/
-----| HighlightedMarkdown.server.vue
app/pages/blog/[slug].vue
<template>
  <article>
    <h1>{{ post.title }}</h1>
    <HighlightedMarkdown :markdown="post.body" />
  </article>
</template>

Static islands like this work fine on noScripts routes, since their HTML is embedded at render time.

Fully interactive widgets inside islands (components marked with nuxt-client, or interactive island slots) do not work on a noScripts route. Island teleports are relocated into place by a small inline script before hydration; with scripts disabled, that relocation cannot run and nothing hydrates. If a page needs even one interactive widget, keep scripts on that route and use lazy hydration instead.

Lazy Hydration for the Rest

For routes that need some interactivity, keep scripts enabled but control when components hydrate using lazy hydration:

app/pages/index.vue
<template>
  <div>
    <HeroSection />
    <!-- static content that never needs to become interactive -->
    <LazyTestimonials hydrate-never />
    <!-- hydrate only when scrolled into view -->
    <LazyNewsletterSignup hydrate-on-visible />
    <!-- hydrate when the browser is idle -->
    <LazyCookieBanner hydrate-on-idle />
  </div>
</template>

hydrate-never is particularly useful on content sites: the component's HTML is server-rendered, but it is never hydrated, so its JavaScript is never executed (though prop changes will still trigger hydration).

Putting It Together

A typical mostly-static site ends up with:

nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    // pure content: prerendered, zero JS
    '/': { prerender: true, noScripts: true },
    '/blog/**': { prerender: true, noScripts: true },
    // pages with an interactive widget: prerendered, scripts kept
    '/contact': { prerender: true },
  },
})
  • content routes ship plain HTML and CSS
  • islands (.server.vue components) handle server-rendered widgets everywhere, keeping heavy dependencies out of the client bundle
  • the few interactive routes keep scripts, with hydrate-on-visible / hydrate-on-idle / hydrate-never limiting how much work the browser does up front
Read more about prerendering, including selective prerendering and payload extraction.
Read more general performance best practices for Nuxt apps.