React: Deployment and CI/CD

Last updated: 2026-08-26

Tom’s blog was running fine locally, but after deploying it to production, he ran into all sorts of problems: images wouldn’t load, API URLs were hardcoded to localhost, and manually uploading code via SSH was slow and erro-prone. He needed an automated deployment pipeline to turn the process from code push to production into a fully automated workflow.

This lesson will guide you through the entire process, from "local development" to "production deployment." You'll learn how to deploy with a single click using Vercel, set up an automated pipeline with GitHub Actions, manage multi-environment variables, and optimize build artifacts to improve page load times. These deployment skills are a crucial step for front-end engineers in transitioning from "writing code" to "delivering products."


1. What You'll Learn



2. Conceptual Diagrams

Tom designed a complete CI/CD pipeline: When a developer pushes code to GitHub, it automatically triggers the build and testing processes; once these are successful, the code is automatically deployed to the staging environment for review; after the review is approved and the code is merged into the main branch, it is automatically deployed to the production environment.

The core of the pipeline is automation and standardization. All inspection steps (lint, type-check, test) are executed automatically in the CI environment without requiring manual intervention. This ensures that any code quality issues are detected before merging, guaranteeing that only validated code is deployed to the production environment. Vercel’s Preview Deployment automatically generates a separate preview URL for each branch, allowing product managers and testers to view the results with a single click.

100%
flowchart LR
    A[Developer Push Code] --> B[GitHub Receive]
    B --> C[GitHub Actions Trigger]
    C --> D{Run CI Process}
    D --> E[Install Dependencies<br/>npm ci]
    E --> F[Code Review<br/>lint / type-check]
    F --> G[Run Test<br/>jest / playwright]
    G --> H[Build<br/>next build]
    H --> I{Deployment Objectives}
    I -->|Feature Branch| J[Vercel Preview<br/>Preview Environment]
    I -->|Main Branch| K[Vercel Production<br/>Production Environment]
    J --> L[Preview URL Automatically Generated]
    K --> M[CDN Distribution<br/>Global Acceleration]


3. A Real-Life Scenario

Deployment Options Deployment Methods ISR Support Operating Costs Use Cases
Vercel Automatic (Git push) ✅ Native Very low Next.js projects, individuals/small teams
Netlify Automatic (Git push) ⚠️ Requires configuration Low Static sites, Gatsby
Docker + Nginx Manual/CI ❌ Must be set up manually Medium Private deployment, requires full control
AWS Amplify Automated ⚠️ Limited Medium AWS Ecosystem Projects
Traditional Server PM2 Manual High Complex customization requirements

Tom’s list of blog deployment issues keeps getting longer: every time he updates a post, he has to log into the server via SSH and manually run git pull, npm run build, and pm2 restart—a process that takes at least 10 minutes. If he forgets to back up the database, one mistake could wipe everything out. What’s even more frustrating is that changes made by other team members often overwrite his code.

He decided to adopt a modern deployment solution using Vercel and GitHub Actions. The first step was to migrate the code from FTP to a GitHub repository; the second step was to connect to Vercel to enable automated deployment; and the third step was to configure GitHub Actions to add code checks and testing workflows. After the migration was complete, every time code was pushed to the main branch, Vercel automatically built and deployed it, with the entire process taking less than 2 minutes.

Tom also compared the pros and cons of several deployment options: Vercel is best suited for Next.js projects and offers the simplest configuration; Netlify also supports Next.js, but some advanced features (ISR, middleware) require additional configuration; Docker deployment is suitable for scenarios requiring full control over the server environment; and traditional server deployment (Nginx + PM2) offers the highest flexibility but involves the highest operational costs. For personal blogs and small projects, Vercel is undoubtedly the best choice.

(1) Vercel Automatic Deployment

Vercel is a serverless deployment platform provided by Vercel, the company behind Next.js. It is deeply integrated with Next.js and supports all Next.js features, including automatic framework detection, Serverless Functions, Edge Functions, ISR, and middleware. Vercel’s automated deployment mechanism is based on Git integration—once you connect your GitHub, GitLab, or Bitbucket repository, each push automatically triggers a build and deployment.

Vercel offers three environments: Production (production environment, bound to a custom domain), Preview (preview environment, with a separate URL automatically generated for each branch), and Development (local development environment). The Preview environment is particularly well-suited for team collaboration—a preview URL is automatically generated for each pull request, making it easy for reviewers to see how the changes look in a live environment.

Vercel Deployment Steps (GUI Method)

  1. Click Add New -> Project on the Vercel Dashboard
  2. Select a GitHub repository and authorize Vercel to access it
  3. Vercel automatically detects the Next.js framework and uses the default configuration
  4. Add the necessary environment variables in "Environment Variables"
  5. Click "Deploy" and wait about 1–2 minutes for the deployment to complete.
  6. Once deployment is complete, Vercel automatically generates the .vercel.app domain name
  7. Add a custom domain in Settings -> Domains

Vercel Deployment Steps (CLI Method)

BASH
# 1. Global Installation Vercel CLI
npm install -g vercel

# 2. Log In Vercel Account
vercel login

# 3. Run the deployment from the project's root directory
# The first time you run it, you'll be guided through the project setup.
vercel

# 4. Deploy to the production environment
vercel --prod

▶ Example 1: Vercel Deployment Configuration and CLI

Output:

TEXT 📖 Display only
added <n> packages in <time>
Command executed
Command executed
Command executed
<directory listing>
Command executed
BASH
# 1. Installation Vercel CLI
npm install -g vercel

# 2. Log in to the project root directory
vercel login

# 3. Deploy the project's root directory to the preview environment
vercel

# 4. Deploy to the production environment
vercel --prod

# 5. View the current deployment status
vercel ls

# 6. View the deployment log
vercel logs --all

Output:

TEXT 📖 Display only
added <n> packages in <time>
TS
// next.config.ts - Vercel Automatically read this configuration
import type { NextConfig } from 'next'

const config: NextConfig = {
  // Image Optimization Settings - Allow remote image domains
  images: {
    remotePatterns: [
      { protocol: 'https', hostname: 'images.example.com' },
      { protocol: 'https', hostname: '**.cloudfront.net' },
    ],
  },

  // HTTP Compression
  compress: true,

  // Remove X-Powered-By header (security)
  poweredByHeader: false,

  // Custom Build Directory(Optional)
  distDir: '.next',

  // Enable Strict Mode
  reactStrictMode: true,
}

export default config
JSON
// vercel.json - Vercel Project Configuration(Optional,Most projects do not require)
{
  "framework": "nextjs",
  "buildCommand": "npm run build",
  "outputDirectory": ".next",
  "regions": ["hnd1", "iad1"],
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "X-Frame-Options", "value": "DENY" }
      ]
    }
  ]
}

(2) GitHub Actions CI/CD

GitHub Actions is a CI/CD service provided by GitHub that uses YAML configuration files to define workflows. A specified sequence of jobs is automatically triggered with every push or pull request. Tom has configured three jobs: the first runs linting and type checking whenever a push is made to any branch; the second runs full testing and building when a push is made to the main branch; and the third automatically deploys to the preview environment when a new version is released.

Workflow files are stored in the .github/workflows/ directory, and you can name them as you like. Each workflow can contain multiple jobs, and you can set dependencies between jobs. GitHub offers a wide variety of marketplace actions, allowing you to directly reuse steps created by the community.

The core concepts of GitHub Actions include: on defining trigger events (push, pull_request, schedule, etc.), jobs defining the tasks to be executed, steps defining the execution steps for each task, and actions/ community-contributed reusable modules. Tom’s workflow uses three community actions: actions/checkout@v4 (check out code), actions/setup-node@v4 (configure the Node.js environment), and actions/upload-artifact@v4 (upload build artifacts).

▶ Example 2: A Complete GitHub Actions Workflow

Output:

TEXT 📖 Display only
Save the above YAML configuration to the specified file path. The settings will take effect on the next server restart.
YAML
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  # Job 1:Code Quality and Type Checking
  quality:
    name: Code Quality Check
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: TypeScript type check
        run: npx tsc --noEmit

      - name: Lint check
        run: npm run lint

  # Job 2:Run Test + Build
  test-and-build:
    name: Test & Build
    needs: quality  # Dependency quality Mission Successful
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run unit tests
        run: npm test
        env:
          CI: true

      - name: Build project
        run: npm run build
        env:
          NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}

      - name: Upload build artifacts
        uses: actions/upload-artifact@v4
        with:
          name: next-build
          path: .next/

  # Job 3:Automatically deploy to Vercel
  deploy:
    name: Deploy to Vercel
    needs: test-and-build
    runs-on: ubuntu-latest
    # Only at main Branch Deployment
    if: github.ref == 'refs/heads/main'

    steps:
      - uses: actions/checkout@v4

      - name: Deploy to Vercel
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
          vercel-args: '--prod'

Output:

TEXT 📖 Display only
Missing deps → stale closures. Extra deps → unnecessary runs. ESLint exhaustive-deps rule catches both. Always include all referenced values.

(3) Developing Optimization Strategies

Deployment is more than just uploading code to a server. A well-optimized build configuration can significantly improve page load times and reduce bandwidth costs. Tom has devoted a great deal of effort to deployment optimization, focusing primarily on three areas: package size analysis, image optimization, and caching strategies.

The package size analysis uses the @next/bundle-analyzer plugin, which allows you to visually inspect the size of each module and identify dependencies with abnormal sizes. Tom discovered that moment.js accounted for 85KB in his project. After switching to dayjs (6KB), the JavaScript load on the first screen was reduced by 28%. He also found a component that had never been used but was globally imported, so he removed it using Tree Shaking.

Image optimization is implemented using Next.js’s built-in next/image component, which automatically generates WebP/AVIF formats, responsive sizes, and lazy loading. Tom’s blog post contains a large number of images; after using next/image, the average image load time dropped from 1.2 seconds to 0.3 seconds.

The caching strategy is implemented by configuring the CDN’s Cache-Control header. Static resources (JS, CSS, images) are set to a one-year cache (max-age=31536000), while HTML pages are set to a shorter cache (max-age=60) in conjunction with ISR automatic updates. This way, after a user’s first visit, subsequent static resources are loaded directly from the browser cache without the need for a new request.

Quick Reference for Other Deployment Options

Solution Configuration Complexity Next.js Compatibility Use Cases
Vercel Very Low Perfect Next.js's Preferred Deployment Platform
Netlify Low Good (some advanced features are limited) Small static sites
Docker Intermediate Good Team projects requiring consistent environments
Traditional Nginx High Requires manual SSR configuration Enterprise-hosted server
AWS Amplify Chinese Good AWS Ecosystem Projects

▶ Example 3: Building an Optimized Configuration

Output:

TEXT 📖 Display only
Missing deps → stale closures. Extra deps → unnecessary runs. ESLint exhaustive-deps rule catches both. Always include all referenced values.
TS
// next.config.ts - Complete Optimization Configuration
import type { NextConfig } from 'next'

// Package Volume Analysis(Enable on Demand)
const withBundleAnalyzer = process.env.ANALYZE === 'true'
  ? require('@next/bundle-analyzer')({ enabled: true })
  : (config: any) => config

const config: NextConfig = {
  // === Image Optimization ===
  images: {
    // Allowed Remote Image Domains
    remotePatterns: [
      { protocol: 'https', hostname: 'images.example.com' },
      { protocol: 'https', hostname: 'cdn.example.com' },
    ],
    // Image Format(Supported by default WebP)
    formats: ['image/avif', 'image/webp'],
    // Equipment Breakpoint(Configure according to the design draft)
    deviceSizes: [640, 768, 1024, 1280, 1536],
  },

  // === Safety and Performance ===
  compress: true,
  poweredByHeader: false,
  reactStrictMode: true,

  // === CDN Layout ===
  // If you use a custom CDN,Settings assetPrefix
  // assetPrefix: 'https://cdn.example.com',

  // === Experimental Features ===
  experimental: {
    // Optimization CSS Volume
    optimizePackageImports: ['antd', '@ant-design/icons', 'lodash-es'],
  },
}

export default withBundleAnalyzer(config)

// vercel.json - Caching and Security Header Configuration
// {
//   "headers": [
//     {
//       "source": "/static/(.*)",
//       "headers": [
//         { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
//       ]
//     },
//     {
//       "source": "/_next/image(.*)",
//       "headers": [
//         { "key": "Cache-Control", "value": "public, max-age=86400, stale-while-revalidate=2592000" }
//       ]
//     }
//   ]
// }

Output:

TEXT 📖 Display only
Stale closure bug: useEffect reads old value from closure. Fix: add to deps array or use functional update setState(prev => ...)
BASH
# Commands for Package Volume Analysis
ANALYZE=true npm run build

# This command generates two HTML Report:
# - .next/analyze/client.html (Client-Side Code Analysis)
# - .next/analyze/server.html (Server-Side Code Analysis)

# After opening your browser to view the report,,You can identify and optimize dependencies that are too large.
# Common Optimization Techniques:
# 1. Dynamic Import: const HeavyComponent = dynamic(() => import('./HeavyComponent'))
# 2. Replacing a Large Database: moment → dayjs(Minimize 95%)
# 3. Tree Shaking: import { Button } from 'antd' in place of import { Button } from 'antd/es/button'

Example of Before-and-After Optimization

Metric Before Optimization After Optimization Improvement
Above-the-fold JS size 285 KB 168 KB 41% reduction
Lighthouse Performance Score 62 94 32-point improvement
TTFB (Time to First Byte) 420 ms 180 ms 57% reduction
Build Time 3m 12s 1m 45s 45% reduction
CDN Hit Rate 52% 95% 43% increase

▶ Example 4: GitHub Actions CI Pipeline Configuration

Output:

TEXT 📖 Display only
Save the above YAML configuration to the specified file path. The settings will take effect on the next server restart.
YAML
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint-and-test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20]
    steps:
      - uses: actions/checkout@v4
      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck
      - run: npm test -- --coverage
      - name: Upload coverage
        if: matrix.node-version == 20
        uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage/

  build:
    needs: lint-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 build
      - name: Check bundle size
        run: |
          SIZE=$(du -sk .next/static | cut -f1)
          echo "Bundle size: ${SIZE}KB"
          if [ "$SIZE" -gt 500 ]; then
            echo "⚠️ Bundle exceeds 500KB threshold"
          fi

Output:

TEXT 📖 Display only
GitHub Actions: on push → npm install → npm test → npm run build → deploy. Automated pipeline on every PR/merge.

▶ Example 5: Comprehensive—Complete Configuration for Next.js Production Deployment

Output:

TEXT 📖 Display only
Save the above YAML configuration to the specified file path. The settings will take effect on the next server restart.
YAML
# .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run linter
        run: npm run lint

      - name: Type check
        run: npx tsc --noEmit

      - name: Run tests
        run: npm test

      - name: Build application
        run: npm run build
        env:
          NEXT_PUBLIC_API_URL: ${{ vars.NEXT_PUBLIC_API_URL }}
          DATABASE_URL: ${{ secrets.DATABASE_URL }}

      - name: Run Lighthouse audit
        uses: treosh/lighthouse-ci-action@v12
        with:
          urls: |
            http://localhost:3000
          uploadArtifacts: true
          budgetPath: ./lighthouse-budget.json

      - name: Deploy to Vercel
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
          vercel-args: '--prod'
          working-directory: ./

      - name: Notify deployment
        if: always()
        run: |
          STATUS="${{ job.status }}"
          curl -X POST "${{ secrets.SLACK_WEBHOOK }}" \
            -H 'Content-type: application/json' \
            -d "{\"text\":\"Deploy $STATUS on $(date -u +%Y-%m-%dT%H:%MZ)\"}"

Output:

TEXT 📖 Display only
$ vercel deploy → Production URL: https://my-app.vercel.app. Auto-preview on PR. Edge functions, ISR, image optimization.
JSON
// lighthouse-budget.json
[
  {
    "path": "/*",
    "options": { "first-contentful-paint": { "maxNumericValue": 2000 } },
    "budgets": [
      { "resourceSizes": [{ "resourceType": "script", "budget": 200 }, { "resourceType": "stylesheet", "budget": 50 }, { "resourceType": "image", "budget": 300 }, { "resourceType": "total", "budget": 600 }] }
    ]
  }
]

❓ FAQ

Q What’s the difference between Vercel and traditional server deployment?
A Vercel is a serverless platform—you don’t need to manage servers, it scales automatically, is billed per request, offers global CDN acceleration, and supports preview deployments. Traditional deployment requires you to purchase your own servers, set up Nginx, manage SSL certificates, and configure load balancing. Vercel is better suited for front-end and full-stack projects, while traditional deployment is better suited for scenarios requiring custom backend configurations. Vercel’s free tier is more than sufficient for personal projects.
Q How do I configure environment variables in Vercel?
A In the Vercel Dashboard, select your project -> Settings -> Environment Variables. You can configure them separately for each environment (Production/Preview/Development). Variables prefixed with NEXT_PUBLIC_ will be bundled into the browser-side JavaScript; variables without a prefix are only available on the server side. Pass sensitive variables via secrets in GitHub Actions, and reference them via ${{ secrets.XXX }} in your workflow.
Q What should I do if a test fails in the CI/CD process?
A By default, GitHub Actions will terminate subsequent jobs if a test fails (the deploy job will not run). You can view the Actions run logs in the PR to troubleshoot the cause of the failure. Common issues include: missing test environment variables, Node.js version mismatches, and failed dependency installations. We recommend running npm test locally first to confirm success before pushing. You can also configure continue-on-error: true to allow the job to continue even if certain jobs fail.
Q What metrics should you focus on for build optimization?
A Focus on three core metrics: first-screen JavaScript size (ideally <200 KB), Lighthouse performance score (ideally >90), and Time to First Byte (TTFB) (ideally <200 ms). Use @next/bundle-analyzer to analyze the bundle size of each dependency and identify large libraries that can be replaced or dynamically imported. Image optimization is usually the area where improvements are most easily achieved—use next/image to automatically generate WebP format and responsive image sizes.
Q Is Vercel’s free tier sufficient? Are there any limitations?
A The Hobby plan’s free tier includes: 100 GB of bandwidth per month, a 10-second execution time per Serverless Function, and 1,000 build sessions per month. This is more than enough for personal blogs and small projects. Main limitations: Serverless functions have a 10-second timeout (60 seconds on the Pro plan), batch operations for on-demand ISR refreshes are not supported, and Preview deployment links expire after 30 days. For commercial projects, we recommend upgrading to the Pro plan ($20/month).

📖 Summary


📝 Exercises

  1. Push your Next.js project to a GitHub repository, then import it into Vercel (Import Git Repository). Observe the automated deployment process and verify that the Preview URL and Production URL are correct after deployment. Review the deployment logs in the Vercel Dashboard to understand each step of the build process. Configure a custom domain (optional) and enable HTTPS.
  2. Create .github/workflows/ci.yml in the project and configure a CI workflow: when a commit is pushed to any branch, automatically run npm ci -> npm run lint -> npm test -> npm run build. Intentionally introduce a lint error or a test failure, then push the changes to see if the CI fails and displays an error message. Afterward, fix the error and verify that the CI passes.
  3. Configure build optimization for the project: Use @next/bundle-analyzer to analyze the package size of the current project and identify the top 3 largest dependencies. Implement dynamic importing for one of these large components (dynamic(() => import(...))), and compare the change in JavaScript size before and after optimization. In next.config.ts, enable image optimization (configure remotePatterns) and compression settings. Finally, use Chrome DevTools’ Lighthouse to test the performance scores before and after optimization, and record the improvement in first-screen JavaScript size and performance scores.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏