大部分为静态的网站

为内容型网站和营销型网站提供近乎零 JavaScript,同时保留交互功能模块。

默认情况下,Nuxt 会在服务器端渲染页面,然后对整个页面进行水合。对于一个包含 95% 静态文本和图片的页面来说,这些用于水合的 JavaScript 大多是额外开销:它会延迟交互,并与内容争夺带宽和 CPU,从而可能影响移动端 PageSpeed 分数等指标。

Nuxt 提供了避免在不需要的地方承担这项开销的各项功能。本方案将它们结合起来:

  1. 在构建时预渲染内容路由。
  2. 使用 noScripts 路由规则,移除这些路由上的脚本
  3. 对需要服务器端渲染但不需要交互的组件使用服务器组件
  4. 对保留脚本的路由使用延迟水合,只有在需要时才为交互功能付出开销。

预渲染并移除脚本

对于完全没有客户端交互的路由,可以在 routeRules 中结合使用 prerendernoScripts

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

这些路由会在构建时渲染为纯 HTML,渲染后的页面会省略 Nuxt 入口脚本、导入映射、负载脚本和 JS 资源提示。CSS 仍会被包含。最终页面实际上几乎没有 JavaScript 成本。

详细了解 noScripts 确切省略的内容,以及应用范围的 features.noScripts 选项。
noScripts 页面不会进行水合。页面上的任何内容都不可交互:没有 @click 处理程序,没有 <NuxtLink> 预取(链接仍会作为普通的 <a> 标签正常工作),也无法从该页面进行客户端导航。仅在确实需要这种行为的路由上使用它。

导航到和离开无脚本路由

由于 noScripts 页面没有客户端路由器,因此每次导航到此类页面或从此类页面离开,都会进行整页加载。Nuxt 会为你处理这两个方向:

  • 从脚本路由导航 noScripts 路由(使用 navigateTo<NuxtLink>router.push)会触发文档加载,因此目标页面会由服务器以无脚本方式提供,而不是由客户端路由器渲染。
  • 由于这些路由永远不会在客户端渲染,当覆盖它们的路由规则可以在构建时解析时,它们的页面组件会从客户端 bundle 中移除。
  • 脚本页面和无脚本页面都会生成声明式的 speculation rules 标签,因此支持此功能的浏览器会在用户点击链接之前预取并预渲染目标页面。这不会在页面本身运行任何 JavaScript,并且这些规则的作用域限定为你的页面路由,因此指向 /logout 等服务器路由的链接永远不会被推测性获取。

启用实验性的 view transitions后,这些页面还会选择加入跨文档 view transitions,因此整页导航会以动画形式呈现,而不是出现闪烁:

nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    viewTransition: true,
  },
})

用于服务器渲染组件的小岛

静态页面的某些部分仍然适合使用背后具有真实逻辑的组件:渲染后的 Markdown、语法高亮代码、CMS 内容。对于这些内容,请使用服务器组件。它们在服务器上渲染,其依赖项永远不会进入客户端构建包:

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

这样的静态小岛在 noScripts 路由上也能正常工作,因为它们的 HTML 会在渲染时嵌入。

小岛中的完全交互式组件(使用 nuxt-client 标记的组件,或交互式小岛插槽)在 noScripts 路由上无法工作。小岛传送内容会在水合之前通过一段小型内联脚本重新定位到正确位置;禁用脚本后,该重新定位无法执行,也不会进行任何水合。如果页面哪怕只需要一个交互式组件,也请在该路由上保留脚本,并改用延迟水合。 :::

其余部分的延迟 Hydration

对于需要一定交互性的路由,保持脚本启用,但使用延迟 Hydration控制组件何时进行 Hydration:
app/pages/index.vue
<template>
  <div>
    <HeroSection />
    <!-- 永远不需要变为可交互的静态内容 -->
    <LazyTestimonials hydrate-never />
    <!-- 仅在滚动到视口内时进行 Hydration -->
    <LazyNewsletterSignup hydrate-on-visible />
    <!-- 浏览器空闲时进行 Hydration -->
    <LazyCookieBanner hydrate-on-idle />
  </div>
</template>
hydrate-never 对内容网站尤其有用:组件的 HTML 会在服务器端渲染,但永远不会进行 Hydration,因此其 JavaScript 永远不会执行(不过 prop 变化仍会触发 Hydration)。

将各部分结合起来

一个典型的基本静态网站最终会是这样:
nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    // 纯内容:预渲染,不包含 JavaScript
    '/': { prerender: true, noScripts: true },
    '/blog/**': { prerender: true, noScripts: true },
    // 包含交互式组件的页面:预渲染,保留脚本
    '/contact': { prerender: true },
  },
})
  • 内容路由提供纯 HTML 和 CSS,导航到这些路由时会进行推测性预取的文档加载
  • islands(.server.vue 组件)负责处理各处的服务端渲染小部件,使大型依赖不会进入客户端包
  • 少量交互式路由会保留脚本,并通过 hydrate-on-visible / hydrate-on-idle / hydrate-never 限制浏览器在初始阶段执行的工作量
详细了解预渲染,包括选择性预渲染和载荷提取。
详细了解 Nuxt 应用的通用性能最佳实践。