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
- Vercel Automated Deployment (Git Integration, Domain Configuration, Team Collaboration)
- Comparison of Various Deployment Options: Netlify, Docker, and Traditional Servers
- Setting Up a GitHub Actions CI/CD Pipeline
- Environment variável management (isolation of development, preview, and production environments)
- Develop optimization strategies (packet size analysis, CDN, compression, caching)
@next/bundle-analyzerSpecific Methods for Visualizing Package Volumenext/imageHow the Component Automatically Optimizes Image Loading: Principles and Configuration
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.
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)
- Click
Add New -> Projecton the Vercel Dashboard - Select a GitHub repository and authorize Vercel to access it
- Vercel automatically detects the Next.js framework and uses the default configuration
- Add the necessary environment variables in "Environment Variables"
- Click "Deploy" and wait about 1–2 minutes for the deployment to complete.
- Once deployment is complete, Vercel automatically generates the
.vercel.appdomain name - Add a custom domain in Settings -> Domains
Vercel Deployment Steps (CLI Method)
# 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:
added <n> packages in <time>
Command executed
Command executed
Command executed
<directory listing>
Command executed
# 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:
added <n> packages in <time>
// 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
// 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:
Save the above YAML configuration to the specified file path. The settings will take effect on the next server restart.
# .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:
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:
Missing deps → stale closures. Extra deps → unnecessary runs. ESLint exhaustive-deps rule catches both. Always include all referenced values.
// 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:
Stale closure bug: useEffect reads old value from closure. Fix: add to deps array or use functional update setState(prev => ...)
# 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:
Save the above YAML configuration to the specified file path. The settings will take effect on the next server restart.
# .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:
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:
Save the above YAML configuration to the specified file path. The settings will take effect on the next server restart.
# .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:
$ vercel deploy → Production URL: https://my-app.vercel.app. Auto-preview on PR. Edge functions, ISR, image optimization.
// 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
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.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.@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.📖 Summary
- Vercel is the best deployment platform for Next.js, supporting automated deployment via Git integration, preview environments, serverless functions, and a global CDN
- GitHub Actions workflows are defined in
.github/workflows/*.ymland support multi-job orchestration and dependencies (needscontrols the execution order) - Standard CI/CD process: Code review → Type checking → Running tests → Build → Deployment (each stage can be performed in parallel or sequentially)
- Vercel environment variables are isolated by environment (Production, Preview, and Development); sensitive variables can be configured via the Dashboard or CLI.
- The Three-Part Optimization Suite: Package Size Analysis (
@next/bundle-analyzer), Image Optimization (next/image), and CDN Caching (Cache-Control) dynamicDynamic import andoptimizePackageImportscan effectively reduce the size of JavaScript on the first screen- Preview deployment provides a unique URL for each PR, making it easier for the team to collaborate on reviews
next.config.ts,compress,poweredByHeader,images, and other configurations affect security and performance- Deployment options: Vercel is best suited for Next.js, Netlify is best suited for static sites, Docker is best suited for team projects, and traditional deployment is best suited for enterprise scenarios
- Core Value of CI/CD: Automated code quality checks to ensure that only validated code is deployed to the production environment
- Environment variables are scoped by prefix: no prefix (server-side),
NEXT_PUBLIC_(browser-side),NEXT_PRIVATE_(explicitly declared on the server-side) next/imageComponent automatically optimizes image loading: WebP/AVIF format conversion, responsive sizing, lazy loading, and CDN caching- TTFB, Lighthouse score, and the size of the JavaScript code loaded on the first screen are key metrics for assessing deployment quality
📝 Exercises
- 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.
- Create
.github/workflows/ci.ymlin the project and configure a CI workflow: when a commit is pushed to any branch, automatically runnpm 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. - Configure build optimization for the project: Use
@next/bundle-analyzerto 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. Innext.config.ts, enable image optimization (configureremotePatterns) 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.