Quay lại
Frontend✦ Nổi bật
5 phút đọc30 tháng 3, 202611

Version Skew trong Next.js: Lỗi ngớ ngẩn mà ai cũng gặp

User click nút mà app lăn ra lỗi sau mỗi lần deploy? Đó là version skew. Đây là cách xử lý triệt để trên mọi nền tảng.

N

Nguyen Nhat Long

@longnn

A technical diagram showing version skew: on the left a browser with old JS bundle v1 sending a request, on the right a server running new code v2, a red X between them indicating mismatch, arrows showing failed request/response, clean flat design with dark background

Version Skew trong Next.js: Lỗi ngớ ngẩn mà ai cũng gặp

Bạn vừa deploy xong, CI/CD xanh lè, Slack báo thành công. Bạn thở phào.

5 phút sau, support gửi screenshot: user click nút "Thanh toán" thì app trắng xóa, console đỏ lòm. Bạn test lại — mọi thứ chạy ngon. Refresh trang của user — hết lỗi.

Chào mừng bạn đến với version skew — cái bug mà bạn không viết ra nhưng vẫn phải chịu trách nhiệm.

Version skew là gì và tại sao Next.js lại dính nặng?

Version skew xảy ra khi browser của user vẫn chạy JavaScript bundle cũ, trong khi server đã chuyển sang version mới. Nghe đơn giản, nhưng hậu quả thì không đơn giản chút nào.

Tưởng tượng thế này: bạn deploy lúc 2 giờ chiều. Anh Minh ở phòng kế toán mở app từ sáng, tab vẫn mở. Anh ấy click vào Server Action → server nhận payload được encrypt bằng key cũ → boom, lỗi không ai hiểu nổi.

A technical diagram showing version skew: on the left a browser with old JS bundle v1 sending a request, on the right a server running new code v2, a red X between them indicating mismatch, arrows showing failed request/response, clean flat design with dark background

Cụ thể trong Next.js, version skew gây ra:

  • Server Actions fail với lỗi encryption — vì key thay đổi mỗi build
  • Dynamic imports lỗi — chunk hash thay đổi, file cũ không còn trên server
  • Hydration mismatch — server render HTML mới, client cố hydrate bằng code cũ
  • API response sai format — client expect shape cũ, server trả shape mới

Theo kinh nghiệm của mình, Server Actions là thứ dính nặng nhất. Với API call thông thường, bạn còn bắt được lỗi rõ ràng. Còn Server Actions fail thì error message cực kỳ khó hiểu — mình từng mất 2 tiếng debug trước khi nhận ra đó chỉ là version skew.

BUILD_ID và deploymentId — cơ chế phía sau

Mỗi lần build, Next.js tạo một BUILD_ID duy nhất trong .next/BUILD_ID. Từ Next.js 14.1.4, bạn có thể set deploymentId trong config:

TypeScript
1// next.config.ts
2const config = {
3 experimental: {
4 deploymentId: process.env.VERCEL_DEPLOYMENT_ID
5 || process.env.CF_PAGES_COMMIT_SHA
6 || 'local-dev'
7 }
8}

ID này được gửi kèm request, giúp server phát hiện client đang chạy version nào. Đây là nền tảng cho mọi giải pháp skew protection.

Mẹo với Server Actions: Set NEXT_SERVER_ACTIONS_ENCRYPTION_KEY thành một giá trị cố định giữa các deployment. Điều này giữ encryption key ổn định qua các lần deploy. Nhưng lưu ý — bạn đang đánh đổi một phần security để lấy stability. Chỉ làm khi bạn hiểu rõ trade-off.

Giải pháp từ các hosting platform

Tin vui: nếu bạn đang dùng một platform lớn, khả năng cao họ đã xử lý giúp bạn rồi.

A comparison table visualization showing four hosting platforms side by side: Vercel, AWS Amplify, Netlify, and Cloudflare Pages, each with their skew protection status shown as cards, with checkmarks or configuration icons, modern flat design with blue and green accent colors on white background

Vercel Skew Protection

Có sẵn trên Pro và Enterprise. Cơ chế: Vercel tag mỗi response với deployment ID, client gửi ID này trong request header, Vercel route request về đúng deployment tương ứng. Không cần viết code gì.

Terminal
1vercel env add VERCEL_SKEW_PROTECTION_ENABLED 1

Điều mình thấy hay là Vercel giữ deployment cũ "warm" trong một khoảng thời gian configurable, nên user không bị lỗi mà vẫn dùng được app — chỉ là dùng version cũ thôi.

AWS Amplify

Tự động set AWS_AMPLIFY_DEPLOYMENT_ID và route request tương tự Vercel. Enable trong Amplify console, mục App settings > Build settings.

Netlify

Thêm một dòng config:

TOML
1NETLIFY_NEXT_SKEW_PROTECTION = true

Netlify dùng COMMIT_REF để identify version.

Cloudflare Pages (qua OpenNext)

Không có native support, nhưng OpenNext adapter hỗ trợ:

TypeScript
1// open-next.config.ts
2export default {
3 dangerous: {
4 enableSkewProtection: true
5 }
6}

Khi platform solution chưa đủ

Các giải pháp platform có một hạn chế chung: chúng im lặng route user về code cũ. Không lỗi, nhưng user cũng không biết mình đang dùng version cũ.

Với nhiều app thì OK. Nhưng nếu bạn cần user chạy version mới nhất — security patch, hotfix, feature quan trọng — thì bạn cần chủ động thông báo cho họ reload.

Tự build version checking cho self-hosting

Nếu bạn đang host trên Railway, Render, Fly.io, hoặc Docker, không có skew protection sẵn. Mình thường làm theo 3 bước:

A step-by-step flowchart showing custom version skew protection implementation: Step 1 API endpoint returns BUILD_ID, Step 2 client hook polls endpoint every 60 seconds comparing versions, Step 3 toast notification prompts user to reload, arrows connecting each step in sequence, clean minimal flowchart style with purple accents on light background

Bước 1 — Version endpoint:

TypeScript
1// app/api/version/route.ts
2export const dynamic = 'force-static';
3
4export function GET() {
5 return Response.json({
6 buildId: process.env.BUILD_ID || Date.now().toString()
7 });
8}

Bước 2 — Client hook polling:

TypeScript
1// hooks/use-version-check.ts
2import { useEffect, useRef, useState } from 'react';
3
4export function useVersionCheck(intervalMs = 60_000) {
5 const [needsUpdate, setNeedsUpdate] = useState(false);
6 const initialVersion = useRef<string | null>(null);
7
8 useEffect(() => {
9 const check = async () => {
10 const res = await fetch('/api/version');
11 const { buildId } = await res.json();
12
13 if (!initialVersion.current) {
14 initialVersion.current = buildId;
15 return;
16 }
17
18 if (buildId !== initialVersion.current) {
19 setNeedsUpdate(true);
20 }
21 };
22
23 check();
24 const id = setInterval(check, intervalMs);
25 return () => clearInterval(id);
26 }, [intervalMs]);
27
28 return needsUpdate;
29}

Bước 3 — Toast component:

TSX
1export function UpdateBanner() {
2 const needsUpdate = useVersionCheck();
3 if (!needsUpdate) return null;
4
5 return (
6 <div className="fixed bottom-4 right-4 bg-blue-600 text-white p-4 rounded-lg shadow-lg">
7 <p>Có phiên bản mới. Vui lòng tải lại trang.</p>
8 <button onClick={() => window.location.reload()}>
9 Reload ngay
10 </button>
11 </div>
12 );
13}

Đơn giản, hiệu quả, không phụ thuộc platform nào.

A browser mockup showing a Next.js application with a toast notification in the bottom-right corner, the toast has a blue background with white text reading 'New version available' and a 'Reload now' button, the main app content is slightly dimmed in the background, modern UI with rounded corners and subtle shadow on the toast

Những điều cần nhớ

  • Version skew không phải bug của bạn, nhưng là trách nhiệm của bạn. User không quan tâm tại sao app lỗi.
  • Server Actions dính nặng nhất vì encryption key thay đổi mỗi build. Cân nhắc set NEXT_SERVER_ACTIONS_ENCRYPTION_KEY cố định nếu chấp nhận trade-off.
  • Platform solution chỉ ngăn lỗi, không đẩy user lên version mới. Nếu cần user chạy code mới nhất, bạn vẫn phải tự build version checking.
  • Polling 60 giây là đủ cho hầu hết app. Đừng poll quá nhanh — tốn resource mà không thêm giá trị.
  • Kết hợp cả hai: dùng platform skew protection để ngăn lỗi + custom version check để prompt user reload. Đây là setup mình đang dùng cho production app.

Version skew là một trong những thứ mà bạn không nghĩ tới cho đến khi nó cắn bạn lúc 11 giờ đêm. Hy vọng bài này giúp bạn xử lý trước khi nó xảy ra. Deploy vui nhé! 🚀

NN

Nguyen Nhat Long

@longnn

Thấy hay? Chia sẻ cho bạn bè!

Bài viết liên quan

Có thể bạn cũng thích

Xem tất cả