Shipping a React app and shipping a Vue app look almost identical until the moment you wire up continuous integration. That’s when the two ecosystems diverge: different build outputs, different SSR models, different failure modes in production. This tutorial walks through building a real deployment pipeline for both stacks side by side, using the versions that are actually current as of August 26, 2026: React 19.2.8 with Next.js 16.2.11, and Vue 3.5.41 with Nuxt 4.5.2. By the end you’ll have two working GitHub Actions pipelines, a rollback plan, and a monitoring setup for each framework.
This is a hands-on, step-by-step build. It assumes you already know the basics of React or Vue components — the goal here is the part most tutorials skip: getting code from a commit to a production URL safely, repeatedly, and with a rollback path when something breaks.
Don't miss new tech stories on Google
Add Tech Insider once in the Google app and our stories appear in your news suggestions.
What You’ll Build: React + Next.js vs Vue + Nuxt Pipelines
By the end of this guide you’ll have two parallel projects, each with its own CI/CD pipeline: a React 19.2 app running on Next.js 16.2.11 (the current Active LTS line), and a Vue 3.5 app running on Nuxt 4.5.2. Both pipelines will run linting, type checks, unit tests, a production build, a preview deployment on every pull request, and a gated production deployment on merge to main. Both will include a documented rollback procedure and basic uptime monitoring.
The reason to build both side by side instead of picking one is practical: most engineering teams today aren’t choosing React or Vue in a vacuum — they’re maintaining one and evaluating the other, or inheriting a Vue codebase after years of React experience (or vice versa). Seeing the pipeline differences explicitly makes the tradeoffs concrete instead of theoretical.
Prerequisites and Version Requirements
Before starting, confirm you have the following installed. Version mismatches are the single most common cause of “it worked on my machine” pipeline failures, so don’t skip the verification commands.
| Tool | Required Version | Verify With |
| Node.js | 20.x LTS or newer | node -v |
| npm | 10.x or newer | npm -v |
| React | 19.2.8 (patched July 21, 2026) | npm ls react |
| Next.js | 16.2.11 (Active LTS, security patch) | npx next --version |
| Vue | 3.5.41 (released August 5, 2026) | npm ls vue |
| Nuxt | 4.5.2 (released August 5, 2026) | npx nuxt --version |
| Git | 2.40 or newer | git --version |
| GitHub CLI (optional) | 2.60 or newer | gh --version |
| Vercel CLI | latest | vercel --version |
A quick note on why the versions matter here specifically. Nuxt 3 officially reached end-of-life on July 31, 2026, so any pipeline you build today targeting Nuxt should be on the Nuxt 4.x line — this tutorial uses 4.5.2, which shipped as a performance patch on top of the Nuxt 4.5.0 release from July 18, 2026 that upgraded the build toolchain to Vite 8 and added Rspack 2 as an alternative bundler. On the React side, Next.js shipped a security release on July 20-21, 2026 patching nine CVEs in the App Router across both the Active LTS (16.2.11) and Maintenance LTS (15.5.21) lines — if your existing pipeline is pinned to an older Next.js version, this is a good moment to force an upgrade as part of building the new pipeline.
Step 1: Scaffold the React + Next.js 16.2 Application
Start with a clean Next.js project targeting the App Router, which is the default and recommended structure as of Next.js 16.2.11.
npx create-next-app@latest react-pipeline-demo \
--typescript \
--eslint \
--app \
--src-dir \
--import-alias "@/*"
cd react-pipeline-demo
npm ls next react react-dom
# [email protected]
# [email protected]
# [email protected]
Add a health-check route now — you’ll need it later for both the CI pipeline and production monitoring. Create src/app/api/health/route.ts:
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({
status: "ok",
framework: "next.js",
version: "16.2.11",
timestamp: new Date().toISOString(),
});
}
Commit this as your baseline before touching CI configuration. It’s much easier to debug a broken pipeline against a known-good app than to debug both at once.
Step 2: Scaffold the Vue 3.5 + Nuxt 4.5 Application
Now build the equivalent Vue project on Nuxt 4.5.2. If you have an existing project still targeting Nuxt 3, treat this as a forced migration point since Nuxt 3 is past end-of-life.
npx nuxi@latest init vue-pipeline-demo
cd vue-pipeline-demo
npm install
npx nuxt --version
# Nuxt 4.5.2
npm ls vue
# [email protected]
Add a matching health-check endpoint in server/api/health.get.ts:
export default defineEventHandler(() => {
return {
status: "ok",
framework: "nuxt",
version: "4.5.2",
timestamp: new Date().toISOString(),
};
});
Enable Nuxt 4.5’s experimental SSR streaming in nuxt.config.ts — this became available with the 4.5.0 release and improves time-to-first-byte on data-heavy pages:
export default defineNuxtConfig({
compatibilityDate: "2026-08-26",
experimental: {
streamAsyncData: true,
},
nitro: {
preset: "vercel",
},
});
Step 3: Structure Environment Variables and Secrets
Both frameworks handle environment variables differently enough to cause real bugs if you copy-paste blindly between them. Next.js requires a NEXT_PUBLIC_ prefix for anything exposed to the browser bundle; Nuxt uses a runtimeConfig object with an explicit public key. Get this wrong and you’ll either leak a secret into client JavaScript or have a variable silently come back undefined in the browser.
# .env.local — Next.js
DATABASE_URL=postgres://user:pass@host:5432/db
NEXT_PUBLIC_API_BASE_URL=https://api.example.com
NEXT_PUBLIC_ANALYTICS_ID=G-XXXXXXX
// nuxt.config.ts — Vue/Nuxt equivalent
export default defineNuxtConfig({
runtimeConfig: {
databaseUrl: process.env.DATABASE_URL,
public: {
apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL,
analyticsId: process.env.NUXT_PUBLIC_ANALYTICS_ID,
},
},
});
Store the actual secret values in your CI provider’s encrypted secrets store (GitHub Actions secrets, in this tutorial) rather than committing an .env file. Never rely on .gitignore alone — a rebase or a force-push can resurrect a committed secret in history even after you delete the file.
Step 4: Build the GitHub Actions Workflow for Next.js
Create .github/workflows/react-pipeline.yml in the Next.js repository. This workflow lints, type-checks, tests, and builds on every push, then deploys only on merges to main.
name: React Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npx tsc --noEmit
- run: npm test -- --ci
- run: npm run build
deploy-production:
needs: build-and-test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install -g vercel@latest
- run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
- run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
- run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
Note the order of steps in build-and-test: lint before type-check, type-check before tests, tests before build. This ordering matters for pipeline speed — a lint failure should fail in seconds, not after a five-minute build has already run.
Step 5: Build the GitHub Actions Workflow for Nuxt
The Vue/Nuxt pipeline follows the same shape, with one addition: running the Vue Language Tools type checker (v3.3.11, updated August 21, 2026) alongside standard TypeScript checks, since Nuxt’s single-file components need their own type-checking pass.
name: Vue Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npx vue-tsc --noEmit
- run: npm test -- --run
- run: npm run build
deploy-production:
needs: build-and-test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install -g vercel@latest
- run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
- run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
- run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
If you’re deploying Nuxt somewhere other than Vercel — say, a Node server or an edge platform — swap the deploy step for your target’s CLI, but keep the needs: build-and-test gate. Skipping that gate to “save time” is how broken builds end up in production.
Step 6: Wire In Automated Testing Before Deploy
Both pipelines above assume a working npm test script. For React, Vitest with React Testing Library is the current standard pairing; for Vue, Vitest with Vue Test Utils covers component-level tests. Add a minimal smoke test to each project so the pipeline has something real to run against.
// React: src/app/page.test.tsx
import { render, screen } from "@testing-library/react";
import Page from "./page";
test("renders the home page", () => {
render( );
expect(screen.getByRole("main")).toBeInTheDocument();
});
// Vue: app/app.test.ts
import { mount } from "@vue/test-utils";
import { describe, it, expect } from "vitest";
import App from "./app.vue";
describe("App", () => {
it("mounts without errors", () => {
const wrapper = mount(App);
expect(wrapper.exists()).toBe(true);
});
});
These are intentionally trivial. The point isn’t test coverage depth right now — it’s proving the pipeline actually fails when a test fails. Break one on purpose, push it, and confirm the deploy job never runs.
Step 7: Configure Preview Deployments for Pull Requests
Preview deployments — a unique URL per pull request — are where most day-to-day review happens on both stacks. Vercel generates these automatically for both Next.js and Nuxt projects once the GitHub integration is connected, without extra workflow steps. To surface the preview URL as a PR comment explicitly (useful if you’re not using Vercel’s native GitHub app), add this step to the build-and-test job on pull requests only:
deploy-preview:
needs: build-and-test
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install -g vercel@latest
- run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
- run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
- id: deploy
run: echo "url=$(vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }})" >> "$GITHUB_OUTPUT"
- uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `Preview deployed: ${{ steps.deploy.outputs.url }}`
})
This block is identical for both the React and Vue pipelines — one of the few places where the two stacks genuinely don’t differ in setup.
Step 8: Set Up Production Builds and SSR Streaming
Here’s where the two stacks meaningfully diverge. Next.js 16.2.11’s App Router uses React Server Components by default, streaming HTML as data resolves; React 19.2.8’s patch specifically improved the RSC decoding path, so builds on 19.2.8 handle streamed payloads more efficiently than on 19.2.7. Nuxt 4.5’s SSR streaming (via experimental.streamAsyncData, enabled in Step 2) is a newer, opt-in feature built on Nitro rather than a default behavior — you have to explicitly turn it on and test it, because not every Nuxt module is compatible with streamed responses yet.
Verify both builds produce the output you expect before wiring them into CI permanently:
# Next.js — check the build output for RSC payload sizes
npm run build
# Route (app) Size First Load JS
# ┌ ○ / 142 B 108 kB
# └ ○ /api/health 0 B 0 B
# Nuxt — check Nitro's build summary
npm run build
# Nuxt Nitro server built with preset: vercel
# .output/server/index.mjs 1.2 MB
If the Nuxt build throws errors related to streaming and a specific module (common with older auth or CMS modules not yet updated for 4.5), disable streamAsyncData for that route with defineRouteRules rather than turning it off globally.
Step 9: Add Dependency Audits and Security Scanning
The July 2026 Next.js security release (patching nine CVEs in the App Router across versions 16.2.11 and 15.5.21) is the clearest recent argument for running automated dependency audits on every build rather than only when someone remembers. Add an audit step to both pipelines’ build-and-test job, right after npm ci:
- run: npm audit --audit-level=high
- run: npx next info # React pipeline only — dumps resolved versions
For a stricter gate, add Dependabot or Renovate configuration so version bumps like the 16.2.11 security patch arrive as an automatic pull request instead of depending on someone checking release notes manually. A minimal .github/dependabot.yml works identically for both repos:
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
Step 10: Configure a Rollback Strategy
Every pipeline needs a rollback path that doesn’t depend on a fresh git revert and a full rebuild under pressure. Vercel keeps every previous production deployment addressable by its own immutable URL, so the fastest rollback for both frameworks is promoting a prior deployment rather than rebuilding:
# List recent production deployments
vercel ls react-pipeline-demo --prod
# Promote a specific prior deployment back to production
vercel promote --token=$VERCEL_TOKEN
This command is identical for the Nuxt project — just swap the project name. Document the exact rollback command in your team’s runbook before you need it, not while a page is down. A rollback that requires remembering CLI flags at 2 a.m. is a rollback that won’t get used correctly.
Step 11: Add Monitoring, Logging, and Alerting
The health-check routes from Step 1 and Step 2 exist for exactly this step. Point an external uptime monitor at both /api/health (Next.js) and /api/health (Nuxt) so you get paged before users notice, not after. For error tracking inside the app itself, both frameworks support the same instrumentation pattern:
# Next.js instrumentation file: src/instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./monitoring/init-server');
}
}
For Nuxt, use a server plugin instead, registered in server/plugins/monitoring.ts, which Nitro runs automatically at server startup. Whichever error-tracking service you choose, confirm it captures both client-side errors and server-side (SSR) errors separately — SSR errors on both frameworks tend to surface in deployment logs rather than the browser console, and teams that only wire up client-side tracking miss them entirely.
Step 12: Tune Caching and CDN Rules
Static assets on both stacks should be cached aggressively at the CDN edge; HTML for dynamic routes needs more care. Next.js’s App Router defaults to caching statically renderable segments and revalidating dynamic ones based on your fetch cache options. Nuxt uses route rules in nuxt.config.ts for the same purpose, and they’re worth setting explicitly rather than trusting framework defaults for a production launch:
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true },
'/blog/**': { swr: 3600 },
'/dashboard/**': { ssr: false },
'/api/**': { cors: true },
},
});
Test cache headers directly after every deploy rather than assuming the config took effect — CDN configuration is one of the more common places where a setting looks correct in code but doesn’t propagate as expected:
curl -sI https://your-app.vercel.app/blog/example | grep -i cache-control
# cache-control: public, s-maxage=3600, stale-while-revalidate
Step 13: Final Go-Live Checklist and Verification
Before pointing a real domain at either pipeline, run through this checklist for both stacks:
- CI fails correctly when lint, type-check, or a test fails (verify by breaking one intentionally)
- Production deploy only triggers from
main, never from a feature branch - Secrets are stored in CI provider secrets, not in committed
.envfiles - Health-check endpoints respond with
200and are wired to an uptime monitor - Rollback command is tested at least once against a real prior deployment
- Cache headers on both static and dynamic routes match expectations
- Dependency audit step is active and Dependabot/Renovate is configured
- Error tracking captures both client-side and SSR errors separately
Once every item is checked for both the React/Next.js and Vue/Nuxt pipelines, you have two production-grade deployment setups you can compare directly rather than by reputation.
Choosing a Deployment Target for Each Stack
This tutorial uses Vercel throughout because it has first-class, near-zero-config support for both Next.js and Nuxt, which keeps the comparison fair. That said, it’s not the only option, and the right target often depends on constraints outside the framework itself — data residency requirements, existing infrastructure, or cost at scale. Netlify supports both frameworks through its own adapters and has comparable preview-deployment behavior. Cloudflare Pages works well for both too, though Nuxt’s Nitro server needs the Cloudflare Workers preset (nitro: { preset: 'cloudflare-pages' }) rather than the Vercel preset used earlier in this guide. A self-hosted option — a plain Node server behind a reverse proxy — works for both frameworks as well, but you lose automatic preview deployments and have to build that mechanism yourself, typically with a separate staging environment per pull request or a shared staging server with path-based routing.
| Deployment Target | Next.js Support | Nuxt Support | Automatic Previews |
| Vercel | Native, first-party | Native, via Nitro preset | Yes, out of the box |
| Netlify | Adapter-based, well maintained | Adapter-based, via Nitro preset | Yes, out of the box |
| Cloudflare Pages | Supported via OpenNext adapter | Native, via cloudflare-pages Nitro preset | Yes, out of the box |
| Self-hosted Node | Supported (standalone output mode) | Supported (Nitro Node preset) | No, must be built manually |
If you’re choosing a target for the first time, weigh the automatic-preview-deployment feature seriously. It’s easy to underestimate how much day-to-day review friction it removes — reviewers click a live URL instead of pulling a branch locally, which materially speeds up how quickly pull requests get merged in a team that reviews UI-heavy changes often.
Handling Database Migrations Inside the Pipeline
Neither React nor Vue dictates a database layer, but most production apps built on either stack eventually need schema migrations to run as part of deployment, not as a manual step someone remembers to do separately. The safest pattern for both stacks is to run migrations as a distinct CI step, gated before the deploy job, so a failed migration blocks the deployment instead of shipping application code that expects a schema that doesn’t exist yet.
run-migrations:
needs: build-and-test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npx prisma migrate deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
deploy-production:
needs: [build-and-test, run-migrations]
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install -g vercel@latest
- run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
- run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
- run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
This ordering — migrations before deploy, both gated behind the test suite — is identical whether your API layer sits inside Next.js route handlers or Nuxt server routes. The database doesn’t care which frontend framework is calling it, so this is one more place where the two pipelines end up nearly identical once you get past the framework-specific build steps.
React vs Vue Deployment Pipeline: Feature Comparison
| Aspect | React + Next.js 16.2.11 | Vue + Nuxt 4.5.2 |
| Current stable core | React 19.2.8 (patched July 21, 2026) | Vue 3.5.41 (released Aug 5, 2026) |
| Meta-framework LTS | Next.js 16.2.11 Active LTS / 15.5.21 Maintenance LTS | Nuxt 4.5.2 (Nuxt 3 reached EOL July 31, 2026) |
| Default build tool | Turbopack / Webpack via Next.js | Vite 8 (default) or Rspack 2 (alternative, since Nuxt 4.5.0) |
| SSR streaming | Built into App Router / RSC by default | Opt-in via experimental.streamAsyncData |
| Type-checking in CI | tsc --noEmit | vue-tsc --noEmit (Vue Language Tools 3.3.11) |
| Preview deployments | Native via Vercel GitHub integration | Native via Vercel GitHub integration |
| Rollback method | vercel promote to prior deployment | vercel promote to prior deployment |
| Next major version in RC | N/A (19.2 is current stable) | Vue 3.6.0-rc.2 (Vapor Mode, July 22, 2026) |
The practical takeaway from this table: the two pipelines are more alike than different at the CI/CD layer. The real divergence is in SSR behavior (default vs. opt-in streaming) and in how aggressively each ecosystem is moving — Vue’s 3.6 release candidate signals a bigger architectural shift (Vapor Mode’s compiler that skips the virtual DOM entirely) than anything currently on React’s roadmap for the 19.x line.
Common Pitfalls When Shipping React or Vue Apps to Production
1. Deploying with a stale lockfile. Running npm install instead of npm ci in a pipeline step lets a lockfile drift silently, meaning your CI build and your local build can resolve different dependency versions. Always use npm ci in CI, never npm install.
2. Forgetting the public-variable prefix. A variable missing NEXT_PUBLIC_ or the Nuxt runtimeConfig.public key will work fine on the server and silently return undefined in the browser — a bug that only appears in production, since local dev often has broader env access.
3. Enabling Nuxt’s SSR streaming globally without testing every route. Not every third-party Nuxt module handles a streamed response correctly. Enable it per-route with routeRules rather than flipping the global flag on a large existing app.
4. Skipping the type-check step to save CI minutes. Both tsc --noEmit and vue-tsc --noEmit feel slow, so teams under deadline pressure sometimes comment them out “temporarily.” That’s precisely when a type error reaches production.
5. No gate between preview and production deploys. If the same workflow trigger can deploy to both preview and production depending on a typo in a branch name condition, you will eventually deploy an unreviewed branch straight to production. Test the if: github.ref == 'refs/heads/main' condition explicitly.
6. Ignoring security patch releases like the July 2026 Next.js CVE fixes. Pinning a Next.js version and never revisiting it means a project can sit on a version with known, published CVEs for months. Dependabot or Renovate catches this automatically if configured.
Troubleshooting Common Deployment Errors
Build fails with “Module not found” only in CI, not locally. Usually a case-sensitivity mismatch — CI runners are Linux and case-sensitive, while local macOS/Windows filesystems often aren’t. Check import paths match file names exactly, including capitalization.
Next.js build succeeds but the deployed site shows a 500 error. Check that all required environment variables are set in the Vercel project settings for the Production environment specifically, not just Preview. A variable present in Preview but missing in Production is a common gap.
Nuxt build throws “Cannot find module” for an auto-imported composable. Confirm the composable lives in a directory Nuxt auto-scans (composables/, utils/) and that you haven’t overridden the default imports config in nuxt.config.ts.
vue-tsc fails with prop type errors that don’t appear in the editor. This usually means your editor is using a cached or different version of Vue Language Tools than what’s pinned in package.json. Update to 3.3.11 or newer and restart the TS server.
GitHub Actions workflow never triggers the deploy job. Double check the needs: key references the exact job name from the build-and-test block, and that the triggering job didn’t get skipped due to a path filter you forgot you added.
Preview deployment comment never posts to the pull request. The default GITHUB_TOKEN needs pull-requests: write permission explicitly declared in the workflow file under newer GitHub Actions permission defaults — add a permissions: block at the top of the workflow if comments silently fail.
Rollback via vercel promote returns “deployment not found.” The deployment URL expires from the quick-list after a retention window on some plans. Use vercel ls --prod immediately before promoting rather than reusing an old URL from a chat log or ticket.
Cache headers show no-store when you expected s-maxage. A dynamic data fetch inside the route (cookies, headers, or an uncached database call) forces both frameworks to opt out of static caching for that route automatically. Isolate the dynamic part into a smaller component instead of making the whole page dynamic.
npm audit blocks the pipeline on a transitive dependency you can’t upgrade directly. Use npm audit fix first; if that doesn’t resolve it, check whether the vulnerable package has a patched version available via an override in package.json before considering --audit-level adjustments, and never silently ignore a high-severity finding.
Advanced Tips for Scaling Your Pipeline
Once the basic pipeline is stable, a few refinements pay off as the team and codebase grow. First, split the CI job into parallel matrix jobs for lint, type-check, and test rather than running them sequentially in one job — this cuts wall-clock time meaningfully once a test suite grows past a couple hundred tests. Second, cache the node_modules and framework build cache (.next/cache for Next.js, .nuxt and node_modules/.cache for Nuxt) between runs using actions/cache, keyed on the lockfile hash — this alone often cuts build time by more than half on repeat runs.
Second, if you’re anticipating Vue’s Vapor Mode (currently in release candidate as 3.6.0-rc.2), keep an eye on bundle-size metrics in your pipeline now, before adopting it. Vapor Mode compiles away the virtual DOM runtime for eligible components, and having a baseline bundle-size report in CI makes the eventual before/after comparison trivial instead of guesswork. A simple size-check step:
- name: Report bundle size
run: |
du -sh .output/public/_nuxt/*.js | sort -rh | head -10
Finally, treat the July 2026 Next.js security release as a template for how you respond to future patch releases on both stacks: pin exact versions in package.json (not ranges) for production apps, let Dependabot propose the bump as a reviewable pull request, and require the full pipeline (including the audit step from Step 9) to pass before merging any dependency update — security patches are exactly the kind of change that shouldn’t skip CI, even under time pressure.
Adding Performance Budgets to the Pipeline
A pipeline that only checks correctness (lint, types, tests) will happily ship a page that got 40% heavier without anyone noticing until a user complains. Both stacks benefit from a Lighthouse CI step that runs against the preview deployment and fails the build if key metrics regress past a threshold. Add this as a job that runs after the preview deployment succeeds, since Lighthouse needs a live URL to test against rather than a local build:
lighthouse-check:
needs: deploy-preview
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install -g @lhci/cli@latest
- run: lhci autorun --collect.url=${{ needs.deploy-preview.outputs.url }}
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
This step is framework-agnostic — Lighthouse tests the rendered page, not the source code — so the same job definition works for both the React/Next.js and Vue/Nuxt repositories without modification. Set the performance budget thresholds in a lighthouserc.json file at the repo root so the numbers are visible in code review rather than buried in a CI job’s configuration. A reasonable starting budget for a content-heavy marketing page is a performance score above 90, largest contentful paint under 2.5 seconds, and cumulative layout shift under 0.1; dashboard-style apps behind authentication can usually tolerate a slightly lower performance score since they aren’t optimizing for first-time visitor conversion.
Worth calling out: Nuxt 4.5’s SSR streaming and Next.js’s RSC-based streaming both tend to improve perceived load time (time to first meaningful paint) more than they improve raw Lighthouse performance scores, since Lighthouse’s default metrics weigh total load completion heavily. Don’t be surprised if enabling streaming on either stack doesn’t move the Lighthouse number much even though real users experience the page as faster — that’s a known limitation of lab-based testing versus field data, and it’s worth pairing Lighthouse CI with real-user monitoring (RUM) data from production rather than trusting lab scores alone for this specific tradeoff.
Building a Complete Working Project
Putting every step together, your final repository structure for either stack should look like this by the end of the tutorial:
react-pipeline-demo/
├── .github/
│ ├── workflows/
│ │ └── react-pipeline.yml
│ └── dependabot.yml
├── src/
│ └── app/
│ ├── api/health/route.ts
│ ├── page.tsx
│ └── page.test.tsx
├── src/instrumentation.ts
├── .env.local (gitignored)
├── package.json
└── next.config.ts
vue-pipeline-demo/
├── .github/
│ ├── workflows/
│ │ └── vue-pipeline.yml
│ └── dependabot.yml
├── server/
│ ├── api/health.get.ts
│ └── plugins/monitoring.ts
├── app/
│ ├── app.vue
│ └── app.test.ts
├── .env (gitignored)
├── package.json
└── nuxt.config.ts
Push both repositories, open a pull request against each, and confirm you see: a passing CI check, a posted preview URL, and — after merging — a production deployment plus a working rollback command you’ve tested at least once. That’s the complete loop this tutorial set out to build.
Frequently Asked Questions
Do I need separate CI/CD pipelines for React and Vue, or can one workflow handle both?
Keep them separate. The build commands, type-checkers (tsc vs vue-tsc), and framework-specific steps differ enough that a single shared workflow adds conditional complexity without saving meaningful setup time.
Is Next.js 16.2.11 the version I should use for a new production project in August 2026?
Yes — it’s the current Active LTS release as of this writing, and it includes the July 2026 security patches. If you need long-term maintenance-only support instead, 15.5.21 is the patched Maintenance LTS line.
Should I migrate an existing Nuxt 3 project before setting up this pipeline?
Yes. Nuxt 3 reached end-of-life on July 31, 2026, meaning it no longer receives security patches. Any new CI/CD investment should target Nuxt 4.5.x.
Is Vue’s Vapor Mode ready for production pipelines yet?
Not yet. As of late August 2026 it’s at release candidate stage (3.6.0-rc.2). It’s worth testing in a branch, but treat it as pre-production until a stable 3.6.0 release ships.
Can I use this same GitHub Actions structure with a deployment target other than Vercel?
Yes. The lint/type-check/test/build steps in build-and-test are platform-agnostic. Only the deploy-production and deploy-preview jobs need to change to match your target’s CLI (AWS, Cloudflare Pages, a self-hosted Node server, and so on).
Why run npm audit in CI if Dependabot already flags vulnerabilities?
Dependabot flags known vulnerabilities on a schedule, but a fresh transitive dependency introduced in a same-day pull request won’t have been scanned yet. Running the audit at build time closes that gap.
How long should preview deployments stay accessible after a pull request merges?
There’s no universal rule, but most teams clean these up automatically after 7-30 days via their hosting platform’s retention settings, since old preview URLs can otherwise accumulate and complicate access audits.
What’s the fastest way to verify a rollback actually works before an incident?
Deploy a small, deliberately labeled change (like a version banner in the footer) to production, then run the rollback command from Step 10 against it in a scheduled maintenance window. If the banner reverts, your rollback path is confirmed working end-to-end.
Does adding a performance budget step slow down every pull request meaningfully?
A Lighthouse CI run against a live preview URL typically adds one to two minutes to the pipeline, which runs in parallel with other checks rather than blocking them sequentially if you configure it as a separate job. Most teams find that acceptable given how often it catches an unintentional bundle-size regression before it reaches production.
Related Coverage
- React vs Vue TypeScript Setup: 12 Steps, 90 Min [2026]
- React Router v8 vs Vue Router: 14-Step Setup Guide [2026]
- React Server Components vs Nuxt SSR: 13 Steps [2026]
- Vercel vs Netlify vs Cloudflare Pages: 4x TTFB Gap [2026]
- Turborepo vs Nx vs Lerna: 7x CI Speed Gap [2026]
For more on this cluster, see the AI coding tools guide.
Sources and further reading: React blog, Next.js blog, Vue.js official site, Nuxt blog, GitHub Actions documentation.


