Skip to content

Next.js 14 还是 Vue 3 + Vite?同一游戏站 MVP 我做了两遍,对比了三天 ​

译注:本文翻译自 dev.to 用户 no_momo 的技术实践记录,原文发布于 2026 年 9 月 22 日。作者用三天时间把同一个游戏站 MVP 分别用 Next.js 14 和 Vue 3 + Vite 实现,记录了两套方案在 SEO、首屏性能和构建流程上的差异,最终选择了混合架构。原文链接见文末。

结论先行 ​

作者最终选择了混合方案:静态内容页用 Next.js 14,游戏运行页则使用独立懒加载的客户端外壳。

但这个决定并非一拍脑袋。三天里他把同一个 MVP 写了两遍,踩过 Next.js 静态导出下动态路由 404 的坑,也处理了 Vue 的 SEO 插件问题。对于面临类似选型的开发者来说,这个推演过程可能比结论本身更有参考价值。

测试环境与约束 ​

环境配置:

  • Node 20.11 / npm 10.5
  • Next.js 14.2.3(App Router + output: 'export')
  • Vue 3.4 + Vite 5.2 + vue-router 4
  • 测试设备:MacBook Pro M1(构建测试)、iPhone 12(Lighthouse 移动端测试)
  • 部署目标:Vercel 静态托管(零服务端)

动手之前,作者列出了三条硬性约束:

  1. SEO 必须可用:游戏站大部分流量来自搜索引擎,游戏详情页必须可被爬取。
  2. 首屏必须快:移动端 4G 下 LCP 低于 2.5 秒。
  3. 游戏 iframe 不能阻塞主线程:浏览列表页时,不应加载任何游戏运行时代码。

这三条约束最终主导了整个决策。

第一天:Next.js 版本——静态导出上了一课 ​

路由结构为 / 首页(SSG)、/games/[category] 分类页(SSG)、/play/[id] 游戏运行页(客户端渲染),使用 output: 'export' 做纯静态导出并部署到 Vercel。

坑一:动态路由 404。 next build 之后首页和分类页正常生成 HTML,但 /play/[id] 全部返回 404。原因是 App Router 配合 output: 'export' 默认不支持动态路由,除非显式定义 generateStaticParams。更麻烦的是,即便定义了它,所有游戏 ID 都必须在构建时预生成;如果游戏数量增长到几百个,每新增一个游戏都要触发全量重建,CI 时间会指数级增长。

作者尝试了 @falsefoundation/next-dynamic-exports,它通过生成 fallback 页面支持动态路由,但需要 Web 服务器重写规则(Nginx try_files),增加了部署复杂度。另一个思路是把游戏运行页从动态路由改为客户端渲染的静态外壳——/play 页面加载后通过 useSearchParams 读取游戏 ID。这绕开了 generateStaticParams 的限制,但运行页不再预渲染,SEO 受损。作者的取舍是:游戏运行页本身 SEO 价值低(用户从详情页跳转而来),可以接受。

Next.js 版本数据:首页 LCP 1.2 秒(移动端 4G 限速),首屏 JS(gzip)约 85KB,构建时间 12 秒(含 6 个预生成游戏页)。

第二天:Vue 3 + Vite 版本——确实轻量 ​

Vue 版本路由结构与 Next.js 版本一致。优势很明显:Vite 冷启动不到 1 秒,热更新几乎无感,构建时间仅 6 秒,是 Next.js 的一半;首屏 JS(gzip)约 50KB,比 Next.js 少近 40%。Vue 3 运行时本身轻量,Vite 的 tree-shaking 也没有留下多余代码。

但 SEO 成了问题。 Vue + Vite 默认是 SPA,首页和分类页的 HTML 里只有 <div id="app"></div>,爬虫看不到内容。解决方案是安装 vite-plugin-seo-prerender,在构建时把 SPA 预渲染成静态 HTML:

javascript
// vite.config.ts
import seoPrerender from 'vite-plugin-seo-prerender'

export default defineConfig({
  plugins: [
    seoPrerender({
      routes: ['/', '/games/puzzle', '/games/sports']
    })
  ]
})

但该插件有局限:只适合为少量页面生成静态 HTML。如果游戏数量增长到几百个,每个详情页都要预渲染,构建时间将不可控。另一个选项是 Nuxt 3,但引入另一个框架与"轻量"目标相矛盾。

Vue 版本数据:首页 LCP 1.5 秒(移动端 4G 限速,预渲染后),首屏 JS(gzip)约 50KB,构建时间 6 秒。

第三天:关键发现——瓶颈不是框架,是 iframe 初始化时机 ​

两个版本都跑起来后,作者用 Chrome DevTools Performance 面板分别录制了从首页到点击"开始游戏"的完整时间线。

一个反直觉的结果出现了:两个版本的性能差异远小于预期。 Next.js 版本首屏 JS 多出 35KB,但在 4G 下大约只是 200ms 的下载时间。浏览列表页时真正的时间消耗不是框架 JS 体积,而是游戏 iframe 的初始化。

在默认实现中,iframe 在游戏详情页挂载时就被初始化。即便 iframe 的 src 指向空占位页,创建 contentWindow 和初始化沙箱环境仍会消耗主线程时间——在 Moto G Power 上,这一操作约需 400ms。

正确做法是门面模式(facade pattern):iframe 挂载前只渲染轻量静态占位(缩略图 + 播放按钮),用户点击"开始"时才注入真正的 iframe。这与 Lighthouse 对延迟加载第三方资源的建议一致。DEV 社区有一个 Next.js 静态 wiki 案例,用门面模式把 YouTube iframe 从 hydration 时挂载改为点击时加载,使移动端 TBT 从破坏性水平降到可接受范围。

这意味着:框架选择对首屏性能的影响,远小于 iframe 初始化策略的影响。 作者特意记下这一点,因为它直接改变了选型标准。

最终选择:混合架构 ​

基于三天的对比,作者做出如下决定:

静态内容页(首页、分类页、详情页)使用 Next.js SSG。 理由:SEO 开箱即用,无需额外插件;动态路由虽有局限,但游戏详情页 ID 数量可控(初期仅 6 个游戏);如果游戏数量增长到 200+,可引入 Headless CMS 配合 ISR,但那需要服务端,会打破零后端约束。

游戏运行页使用独立客户端渲染外壳。 理由:运行页 SEO 价值低,无需预渲染;用 next/dynamic 配合 ssr: false 懒加载游戏 iframe 组件;采用门面模式,用户点击"开始"后才初始化 iframe。

Vue 版本未继续维护。 它构建更快、包更小,但 SEO 需要额外插件,且游戏数量增长后预渲染方案的可扩展性不如 Next.js 的 SSG 体系。

具体实现如下:

javascript
// components/GameLauncher.tsx
'use client'
import { useState } from 'react'
import dynamic from 'next/dynamic'

const GameIframe = dynamic(() => import('./GameIframe'), {
  ssr: false,
  loading: () => <div className="h-full bg-zinc-900 animate-pulse" />
})

export default function GameLauncher({ game }) {
  const [started, setStarted] = useState(false)

  if (!started) {
    return (
      <button
        onClick={() => setStarted(true)}
        className="relative w-full aspect-video rounded-lg overflow-hidden"
      >
        <img src={game.thumbnail} alt={game.title} className="w-full h-full object-cover" />
        <div className="absolute inset-0 flex items-center justify-center">
          <svg width="64" height="64" viewBox="0 0 64 64">
            <circle cx="32" cy="32" r="30" fill="rgba(0,0,0,0.6)" />
            <polygon points="26,20 26,44 46,32" fill="white" />
          </svg>
        </div>
      </button>
    )
  }

  return <GameIframe src={game.entry} gameId={game.id} />
}

ssr: false 确保 iframe 组件在服务端渲染时不会被打包进 HTML,只在用户点击后从客户端加载。这与 Next.js 官方懒加载建议一致:对于不参与 SSR 的重型组件,使用 next/dynamic 配合 ssr: false 来减小初始包体积。

数据对比 ​

指标Next.js 14Vue 3 + Vite
首屏 JS(gzip)~85KB~50KB
首页 LCP(4G)1.2s1.5s(预渲染后)
构建时间12s6s
动态路由支持需要 generateStaticParams内置
SEO开箱即用需要额外插件
游戏运行时懒加载next/dynamic 原生支持需手动实现

测试条件:MacBook Pro M1 构建,iPhone 12 + Chrome DevTools 4G 限速,各运行 10 次取中位数。

值得注意的是,Vue 预渲染后的 LCP 反而比 Next.js 慢。原因是 vite-plugin-seo-prerender 生成的 HTML 中仍包含完整的 Vue 运行时,而 Next.js SSG 输出的 HTML 更干净,hydration 成本更低。

取舍与未解问题 ​

取舍方面:Next.js 构建时间更长(12s vs 6s),每新增一个游戏都要重建,若游戏数达到 50 个,构建时间可能超过 30 秒;Vue 版本被放弃,它开发体验更好、包更小,但 SEO 和可扩展性需要额外投入——如果游戏站不需要 SEO(内部工具或纯娱乐站),Vue + Vite 是更好的选择;混合架构增加了复杂度,静态页和运行页使用不同渲染策略,需要理解两套逻辑。

未解决的问题:如果游戏数量超过 100 个,generateStaticParams 全量预生成会成为瓶颈,需要 ISR 或 Headless CMS,但那就不再是零后端了;作者没有深入研究 Vue 的预渲染方案,vite-plugin-prerender-static 支持多路由生成和 SEO meta 标签,在游戏数量较少时可能够用;Astro 是另一个值得关注的方向,有开发者用 Astro + 原生 JS 构建了 50+ 游戏站点,JS 包 gzip 后控制在 100KB 以内,强调"每个游戏只加载它真正需要的 JavaScript"。

原文链接 ​

https://dev.to/no_momo/nextjs-14-or-vue-3-vite-i-built-the-same-game-site-mvp-both-ways-and-compared-them-for-3-days-e7c