🚀 Nuxt 混合渲染实践: 前端性能深度优化

340 阅读13分钟

从0到1的项目,超详细的全链路性能优化手段以及可落地的全球化 CDN 部署策略

1. CSR,SSG,SSR的区别

首先得了解前端三种渲染模式的区别,前端渲染模式的选择直接影响用户体验、开发成本和运维压力。

从体验角度考虑

  • 静态内容为主:选择 SSG+CDN,兼顾速度与成本(如企业官网、帮助文档)。

  • 动态内容为主:SSR > CSR > SSG,但更多是采用 混合模式,如果选SSR需权衡服务器以及运维成本。

  • 轻量化交互场景:纯 CSG 即可(如简单工具类页面)

2. 我们怎么做

2.1. 决策逻辑

  • 排除SSR:全球化的考虑,做SSR的对于机器运维的成本太高,跨地域访问成本更高

  • 静态内容「极致优化」:用 SSG+CDN 组合拳解决「速度」与「成本」的矛盾;

  • 动态内容「适度妥协」:仅依赖用户信息的强交互页面保留 CSG,进行传统的框架优化,加载时序优化,避免过度工程化。

2.2. 分析思路

项目是从0到1的过程,起初的性能指标并没有,更多是从输入url到DOM展现,思考哪一些是我们可以去优化的,整个优化过程可以抽象成三个部分,网络层加载优化,框架层渲染优化,逻辑层处理优化

2.3. 体验指标对比

2.3.1. 正常网络环境(无缓存)

结论:正常网络下,SSG 和优化后的 CSG 均能达到优秀体验,用户无明显感知上的差异。

官方的体验指标评定标准里FCP在1.8s以内就是优秀的,对比了业务中的SSG和CSG的站点,如果是在正常的网络环境下都达到了优秀的标准,从使用体感来看,加载过程中也不会感觉到慢或者明显的白屏,因此,在正常网络环境下0.5s,1s,还是1.5s并没有什么区别

2.3.2. 2.3.2 弱网络环境(以低速4G为例)

结论: 弱网络下,SSG 的优势显著 —— 只需加载静态 HTML即可展示页面, 无需等待 JS 加载再渲染,可快速展示核心内容,无白屏等待时间

在「低速4G」下,  CSG性能表现截图如下,就会出现明显的白屏,而SSG不会,是秒出的体验

3. 三阶段优化

3.1. 网络层优化: CDN全球化加速的策略

核心目标:通过 CDN 配置减少网络请求耗时,提升资源加载效率。

3.1.1. 全球加速

前端构建产物,推送到OSS进行资源管理,同时通过CDN进行全球访问加速

在创建CDN时,配置域名并添加全球加速,用户访问时可以从就近的CDN节点获取静态资源,避免了每次都是从源站(OSS)进行静态资源的拉取

3.1.2. 缓存配置

  1. 静态资源强缓存:
  • JS、CSS、图片等静态资源设置 14 天缓存,要客户端跟随CDN缓存策略,这样资源请求后可以进行本地缓存

  • 长期不变资源(如 .wasm 二进制文件)配置 365 天超长缓存,避免版本未变更时的重复下载。

  1. HTML 动态更新:
  • 不缓存 HTML 文件(Cache-Control: no-cache),通过 CDN 规则优先级(低于静态资源)确保每次请求html获取最新页面结构,避免因缓存导致内容滞后。

3.1.3. 性能优化

  • 【必选】: 开启后优化体感明显

    1. HTTP/2 协议开启:利用多路复用特性,减少 TCP 连接数,提升资源并行加载效率。

    2. 智能压缩全开:启用 Brotli 压缩(压缩级别 6),对 HTML、JS、CSS 文件平均压缩 40%-60%,降低传输体积。

  • 【可选】:

    1. 图片预处理:将大图压缩并转换为 WebP 格式(体积减少 30%+),小图标使用 SVG 或 Base64 内嵌,减少 HTTP 请求。

    2. URL 参数精简:移除静态资源 URL 中的版本参数(如 style.css?v=2.0style.css),避免 CDN 因参数差异误判缓存失效

3.1.4. 3.1.4 规则引擎和重定向

在CDN配置提前配置规则引擎,根据规则从CDN层面就重定向到不同页面,避免进入浏览器请求资源后再进行判断,可大幅减少跳转延时

CDN提供了两种方式来配置规则,第一种是规则脚本,第二种是规则引擎,脚本可以自行编写代码,但需要按照CDN规定的语法来写,比较麻烦,适合比较复杂的匹配逻辑,而规则引擎是CLI配置界面,适合规则明确,相对简单的配置

下面以「中英文站点的自动识别」来解释逻辑

第一,添加规则引擎规则:「重定向到中文站」和「重定向到英文站」, 主要是两条规则

  • 如果用户选择过语言环境,优先使用用户选择的语言环境

  • 如果用户未选择过语言环境,根据用户IP所在地理位置确定语言环境

第二,配置重写访问URL,这里要注意的是执行规则一定要选择break,break的意思是匹配到这一条规则后,下一次进入不会再匹配其他规则,可避免反复重定向

3.2. 3.2 框架层优化: 混合渲染实践(CSG+SSG)

核心策略:静态内容 SSG 预渲染,动态内容 CSG 客户端渲染,平衡体验与开发成本。

有登录态依赖的页面使用CSG,无登录态依赖的页面使用SSG,比如登录页面,协议页面

为什么不是所有页面都SSG?

不是不能做,只是前端性能优化的实践中,还是得结合项目的实际情况进行选择。当时我们做完其他性能优化改造后,整体页面性能已经很好了,再去把剩余页面改造成SSG收效比较小,因此就没有投产, 但也做过一些技术验证过可行性。

3.2.1. SSG 项目改造

在 Nuxt 中实现 SSG 改造可分为配置调整与逻辑适配两大步骤, SSG的改造适用于混合渲染模式以及纯SSG渲染模式,三个项目都是用的以下方式

3.2.1.1. 配置改造: nuxt.config.ts

SSG 的核心是在构建阶段生成静态 HTML 文件,Nuxt 提供了灵活的配置方式,可根据路由需求精细化控制预渲染范围。配置的核心逻辑是:通过 ssr 启用服务端渲染能力,通过 prerender 指定需要生成静态文件的路由。

  • ssr: true:允许该路由在构建 / 请求时执行服务端逻辑(预渲染的前提)。

  • prerender: true:指定该路由在构建阶段生成静态 HTML 文件(SSG 核心)。

  • 未配置的路由默认继承全局 ssr 配置,可按需组合实现 “部分页面静态化、部分页面动态化”。

可参考 Nuxt 官方文档 实践,以下是两种常用配置方案:

方案一: 预渲染 + 自动爬取链接(适合内容关联紧密的站点)

export default defineNuxtConfig({
 // 开启服务端渲染能力(预渲染依赖服务端逻辑)
 ssr: true, 
 nitro: {
   prerender: {
     // 从根路由开始爬取
     routes: ['/'], 
     // 自动爬取页面中的  链接并预渲染
     crawLinks: true, 
   }
 }
})

方案二: 路由规则精准控制(适合混合渲染场景)

通过 routeRules 为不同路由单独配置渲染策略(SSG/SSR/CSR),灵活度更高:

export default defineNuxtConfig({
  // 全局开启服务端渲染能力
  ssr: true, 
  // 路由规则:配置混合渲染模式
  routeRules: {
    // 预渲染的页面(SSG)
    '/login': { ssr: true, prerender: true },
    '/agreement': { ssr: true, prerender: true },
    ...

    // 客户端渲染(CSG)
    '/main/**': { ssr: false, prerender: true },
    .... 
  },
})
3.2.1.2. 逻辑改造: 兼容客户端特有依赖

预渲染的本质也是执行的服务端渲染,但部分代码(如依赖 windowdocument 等浏览器 API)只能在客户端运行。若直接执行,会导致构建报错或页面异常,需通过以下方式区分客户端 / 服务端逻辑

客户端逻辑隔离: onMounted 与 import.meta.client

  1. onMounted 钩子:在 Nuxt 中,onMounted 内的代码默认仅在客户端执行,适合初始化依赖浏览器 API 的逻辑(如 DOM 操作、事件监听):

  2. import.meta.client 判断:通过环境变量显式区分客户端逻辑,适合非生命周期内的代码:

    export const useDevice = () => { // 只在客户端执行检测,SSG构建的时候会忽略这一段代码 if (import.meta.client) { isMobile.value = checkIsMobile(); } }

客户端组件隔离: 组件

对于完全依赖客户端环境的组件(如包含 window 操作的第三方组件),可使用 Nuxt 内置的 <ClientOnly> 组件包装,确保其仅在客户端渲染:

混合渲染:指定逻辑使用SSG,CSG,精细化控制页面的预渲染或者组件的预渲染

  1. nuxt.config.ts需要先改造成SSG渲染的配置

  2. 对页面局部内容控制渲染方式,通过指定组件进行CSG渲染,(如顶部和侧边菜单 SSG + 主体内容 CSR)

3.2.2. CSG 资源加载优化

主要的分析手段有两个

  1. 使用chrome的Performance分析资源加载情况与白屏问题分析,使用Lighthouse进行FCP分析

  2. 基于Nuxt的Analyze并结合Cursor进行首屏资源的拆包分析和优化,首屏CSS从200KB -> 40KB, 入口JS从458KB,拆为三个150KB并行加载

3.2.2.1. 非首屏资源动态加载

非首屏资源,使用import实现动态加载,包括G6,Markdown解析依赖

3.2.2.2. 图片优化

将大图压缩并转换为 WebP 格式(体积减少 30%+),并放到CDN上进行管理

小图标使用 SVG 或 Base64 内嵌,减少 HTTP 请求。

3.2.2.3. 关键资源进行拆包优化

在nuxt项目里,本身自带了一些拆包优化以及tree-shaking的策略,比如

  • 从app.vue里的进入的代码都会打到入口文件,entry.js,每个路由文件都会有一个单独的js文件

  • 项目里引用的组件库自带tree-shaking,entry或者路由文件都只会加载用到的组件代码

因此,代码分割的主要策略是

  • 将入口文件entry.js进行拆包优化,将不常用的lib库单独抽出来,目的是并行加载提高速度,同时利用好CDN的缓存优势,避免每次打包都产生新的hash,导致每次都是重新加载

  • 针对阻塞性CSS进行分析,删除冗余样式

第一步,使用cursor进行拆包分析

在执行打包时,利用cursor将每个文件依赖的chunk以及chunk大小都打印出来写到file.analyze.log里,然后进行包分析,确定拆包范围

第二步,nuxt.config.ts添加vite的构建配置,通过manualChunks进行拆包

export default defineNuxtConfig({
  ...,
  vite: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes('node_modules')) {
            // Vue 核心库
            if ([...vueLibs].some(lib => id.includes(lib))) {
              return 'vue-lib'
            }

            // 国际化库
            if ([...i18nLibs].some(lib => id.includes(lib))) {
              return 'i18n-lib'
            }

            // Tailwind 相关
            if ([...tailwindLibs].some(lib => id.includes(lib))) {
              return 'tailwind-lib'
            }
          }
        },
      },
    },
  }
})

第三步,nuxt.config.ts中通过hooks修改html中的chunk文件的加载顺序,让lib相关的文件提前加载放到entry之前

export default defineNuxtConfig({
  ...,
  hooks: {
    'nitro:build:public-assets': (_nitro) => {
      // 修改html文件中JS的加载顺序  
    }
})

3.3. 逻辑层优化: 用户体验的细节打磨

核心目标:消除加载卡顿,提升交互流畅度,强化设备兼容性。

3.3.1. 登录页整出

登录页面是预渲染的,单个html文件是4kb,整体渲染很快,但登录页面有一些图片,一开始使用的是CDN,虽然已经有缓存优化了,但毕竟是网络请求,而且背景图很大,有70KB,访问登录页的时候背景图总是会有个请求过程再出现,体验不是很好。

因此,针对登录页的优化是

  1. 页面SSG整出

  2. 依赖图片全部用base 64加载,与html一起加载

优化后,html虽然增加到了80KB,但首次访问时不会出现图片加载延迟的情况,整体页面呈现更为流畅

3.3.2. 登录态校验前置

登录态校验原先是在Nuxt的中间件里执行,但就需要entry和lib的相关JS加载完后,才能执行,这势必会导致首次访问的速度变慢。

因此,针对登录态校验的逻辑前置到所有逻辑前面,单独提成一个JS文件,放到所有JS前面,并内敛到html里,随html一起下发并执行

  1. 设置async进行异步执行,避免阻塞html渲染

  2. 要求后端提供一个单独的checkToken接口提升整体响应速度,从原先的50ms提升到18ms

  3. 在中间件中判断校验结果,并增加兜底逻辑

在这18ms的响应过程中,并行做了两件事,一方面是执行了登录态校验,另一方面也是执行了渲染逻辑

// 全局JS
(function() {
  // 创建认证完成的Promise
  window.__CHECK_AUTH_READY__ = new Promise((resolve) => {
    window.__RESOLVE_AUTH__ = resolve
  })

  function checkAuthStatus() {
    // 1. 请求后端接口,执行校验逻辑
    ... 
    
    // 2. 请求成功
    window.__RESOLVE_AUTH__(true)

    // 3. 请求失败
     window._RESOLVE_AUTH__(false)
  }
})()


// middleware/auth.ts
export default defineNuxtRouterMiddleware(async () => {
  if (!import.meta.client) {
    return
  }

  const isAuthenticated = useAuth()
  
  isAuthenticated.value = await Promise.race([
    window.__AUTH_READY__,
    new Promise(resolve => {
      cont time = setTimeout(() => {
        // 兜底逻辑,1.5s没有响应进行重新校验
      }, 1500)
    })
  ])
])

3.3.3. SpaLoadingTemplate 设置骨架屏

部分页面采用的是CSG渲染,不管优化到什么程度,客户端渲染势必需要等待JS加载完成再进行渲染,,首次加载或者是弱网环境下,依然会有个卡顿现象,因此利用Nuxt的SpaLoadingTemplate设置骨架屏

4. 总结

按优先级和收效进行排序

  1. 网络优化: 充分利用CDN的能力,收效最大,全球加速 | 缓存配置 | 资源压缩 | 规则引擎+重定向

  2. **渲染模式:**SSG预加载体验,在弱网模式下以及首次加载远远好于CSG

  3. 动态加载: 非关键资源动态加载,大幅减少首屏加载js大小

  4. 代码分割: 不常用的css和js要抽离出来,利用缓存优势,避免每次打包都产生新的hash,重新加载

  5. 逻辑优化: 骨架屏以及一些体验细节的优化

优化的核心原则

  1. 能预渲染的,就预渲染

  2. 利用缓存优势,优先客户端强缓存,其次CDN缓存,最后no-cache进行协商缓存

  3. 利用CDN进行前置处理进行重定向的判断,避免进入客户端再去判断

  4. 不能有白屏,不能有闪烁,不能有超出用户预期的体验

技术方案的选择本质是对「用户体验」「开发成本」「场景适配性」的综合权衡,得结合项目的实际情况进行选择。脱离具体场景的「极致优化」,往往是技术炫技而非真实需求了。就像我们在项目中纠结于将 FCP 从 0.6s 压到 0.5s 时,更该思考:这个差值对用户而言,真的体验的出来吗。

因此,性能优化还是要回归用户体验的本质,当用户打开页面时,不会注意到 SSG 如何预渲染 HTML,也不会关心 CDN 节点离自己多近,他们只会觉得「页面自然而然就出来了」,当他感受不到性能时才是好的体验,就会专注在产品功能本身,这种「无感知的流畅」,才是体验的终极目标。