Quay lại blog
13 tháng 8, 2026Sergei Solod27 phút đọc

Vì sao tab Next.js cũ có thể hỏng sau khi triển khai: HTML lỗi thời, chunk bị thiếu và lệch phiên bản

Sau một lần triển khai, hệ thống giám sát sản xuất của tôi ghi nhận một chunk thuộc chính ứng dụng Next.js không tải được. Nhật ký chứng minh sự cố đã xảy ra, chứ không chứng minh nguyên nhân. Từ trường hợp đó, bài viết phân tích tab mở lâu, HTML lỗi thời, tài nguyên /_next/static bị thiếu, lệch phiên bản, thời gian lưu giữ, deploymentId, thứ tự phát hành, giám sát và phục hồi có kiểm soát.

Next.jsTriển khaiLệch phiên bảnBộ nhớ đệm webĐộ tin cậy giao diệnTài nguyên tĩnh

Một trong những lỗi sản xuất hữu ích nhất mà tôi gặp sau khi triển khai trông gần như quá bình thường:

Failed to load script:
/_next/static/chunks/9253.647385b4be0958e4.js

Nó xuất hiện trong cùng luồng với lỗi của công cụ phân tích, tập lệnh quảng cáo, các thông báo chung như Script error. và việc phát video bị gián đoạn. Phần lớn luồng đó chỉ là nhiễu. Trường hợp này khác: tài nguyên tải thất bại thuộc chính ứng dụng Next.js của tôi. Nếu trình duyệt thực sự không lấy được nó, một phần trang có thể ngừng hoạt động.

Điều tôi không biết từ nhật ký đó là vì sao chunk thất bại. Có thể mạng gặp sự cố thoáng qua, máy chủ trung gian hoặc CDN gặp lỗi, hoặc tệp thật sự không tồn tại. Cũng có thể một trang cũ vẫn yêu cầu chunk của lần triển khai trước sau khi máy chủ đã thay bản dựng đó bằng bản mới.

Kịch bản cuối rất dễ bị xem nhẹ vì lần triển khai mới vẫn có thể hoàn toàn khỏe mạnh. Người truy cập mới nhận phiên bản mới và mọi thứ chạy bình thường, trong khi một tab đã mở nhiều giờ âm thầm vẫn là máy khách của phiên bản trước.

Bài viết này tập trung vào chính khoảng trống tương thích đó: vì sao tab Next.js cũ có thể hỏng sau khi triển khai, HTML lỗi thời và tài nguyên /_next/static bị thiếu tạo ra lệch phiên bản như thế nào, vì sao dọn dẹp quá mạnh làm vấn đề tệ hơn, và tôi sẽ thiết kế việc triển khai, lưu giữ tài nguyên, giám sát cùng phục hồi ra sao để một bản phát hành thành công không bỏ lại những người đã mở ứng dụng với một trang bị hỏng.

Bài học đầu tiên: không phải mọi lỗi tập lệnh đều là lỗi triển khai

Luồng lỗi ban đầu chứa nhiều loại sự cố hoàn toàn khác nhau. Tập lệnh phân tích và quảng cáo của bên thứ ba có thể bị chặn bởi trình chặn nội dung, bộ lọc DNS, tính năng riêng tư, lọc theo khu vực, phần mềm chống vi-rút hoặc mạng của người dùng. Lời hứa play() của video có thể bị một lệnh pause() sau đó ngắt mà ứng dụng không hề hỏng. Thông báo liên nguồn chung Script error. thường cung cấp quá ít thông tin để chẩn đoán.

Một chunk của chính ứng dụng Next.js tải thất bại cần được ưu tiên khác. Ranh giới hữu ích không phải “có lỗi JavaScript hay không”, mà gần với cách phân loại này hơn:

tài nguyên bên thứ ba tải thất bại
    -> thường chỉ ảnh hưởng đo lường hoặc chức năng tùy chọn

/_next/static/*.js của chính ứng dụng tải thất bại
    -> mã ứng dụng có thể không còn khả dụng

Phân biệt này quan trọng vì một hệ thống báo lỗi quá ồn có thể che khuất các sự cố thực sự đi cùng trang bị hỏng. Trong trường hợp của tôi, sự kiện đáng chú ý là yêu cầu tới /_next/static/chunks/9253.647385b4be0958e4.js. Nhật ký chứng minh một tập lệnh của chính ứng dụng không tải được. Nó không chứng minh lệch phiên bản do triển khai là nguyên nhân.

Tôi cố ý giữ ranh giới bằng chứng này thật rõ: nguyên nhân hợp lý chưa phải là nguyên nhân đã được xác nhận.

Một tab trình duyệt đang mở thực chất là máy khách của bản phát hành cũ

Mô hình tư duy giúp tôi nhìn vấn đề rõ hơn rất đơn giản: sau khi triển khai, nhiều phiên bản ứng dụng có thể cùng tồn tại trong một khoảng thời gian.

Giả sử bản A hoạt động lúc 10:00. Người dùng mở một trang và nhận HTML cùng JavaScript cần cho tuyến đó. Đến 10:30, bản B thay thế A. Người truy cập mới nhận B. Nhưng tab đã mở không tự biến thành B chỉ vì máy chủ thay đổi.

Tab đó vẫn có thể chứa:

  • môi trường chạy JavaScript đã tải từ bản A;
  • các tham chiếu tuyến và chunk do bản A tạo;
  • dữ liệu điều hướng đã tải trước từ bản A;
  • trạng thái React được tạo khi bản A đang chạy;
  • các mô-đun tách mã từ A đã được tải xuống;
  • tham chiếu tới các mô-đun của A chưa được tải xuống.

Điểm cuối là nơi sự cố thường lộ ra.

Nếu mọi chunk mà trang có thể cần về sau đều đã có trong bộ nhớ đệm của trình duyệt, người dùng có thể tiếp tục mà không nhận ra điều gì. Nhưng ứng dụng Next.js hiện đại chia nhỏ mã. Chuyển tuyến, nhập động, hộp thoại, trình soạn thảo hoặc một tính năng chỉ dùng sau đó có thể cần thêm tệp JavaScript. Môi trường chạy cũ sẽ yêu cầu một tài nguyên bằng URL từng hợp lệ ở bản A.

Nếu máy chủ vẫn giữ tài nguyên đó, mọi thứ có thể tiếp tục bình thường. Nếu lần triển khai đã xóa nó, máy khách cũ có thể nhận 404 dù bản B hoàn toàn khỏe mạnh.

Chunk có hàm băm nội dung được thiết kế để lưu đệm lâu dài

Next.js cố ý áp dụng thời gian lưu đệm dài cho những tài nguyên thực sự bất biến. Tài liệu tự lưu trữ hiện tại cho biết tài nguyên bất biến có hàm băm SHA trong tên tệp được phục vụ với chính sách một năm, chẳng hạn:

Cache-Control: public, max-age=31536000, immutable

Chính sách này hợp lý vì URL thay đổi khi nội dung thay đổi. Tệp có tên bắt nguồn từ nội dung không cần được xác thực lại ở mọi yêu cầu. Nếu bản dựng sau tạo ra các byte khác, nó cũng phải có URL tài nguyên khác.

Hệ quả quan trọng rất dễ bị bỏ qua: URL cũ vẫn có ý nghĩa chừng nào một tài liệu cũ hoặc môi trường chạy cũ còn có thể tham chiếu tới nó.

Khả năng giữ một tài nguyên có hàm băm trong bộ nhớ đệm suốt một năm không giúp ích nếu trình duyệt chưa từng tải chính tài nguyên đó trước khi triển khai và máy chủ nguồn đã xóa nó khi yêu cầu đầu tiên xuất hiện.

Vì vậy, “các tệp tĩnh của chúng ta là bất biến” và “có thể xóa ngay tệp tĩnh của bản phát hành trước” không phải hai phát biểu tương đương. Tính bất biến khiến việc giữ tài nguyên cũ trở nên an toàn; nó không làm máy khách cũ ngừng yêu cầu chúng.

Hướng dẫn tự lưu trữ Next.js hiện tại nêu rõ tệp JavaScript hoặc CSS bị thiếu là một biểu hiện của lệch phiên bản trong triển khai nhiều máy chủ hoặc triển khai cuốn chiếu. Đây vẫn là cùng một họ vấn đề khi độ lệch nằm giữa tab cũ và máy chủ nguồn vừa được cập nhật, thay vì giữa hai máy chủ cùng hoạt động.

Các phiên bản có thể lệch nhau theo nhiều cách

“Lỗi bộ nhớ đệm” quá mơ hồ để dùng làm chẩn đoán. Tôi tách ít nhất bốn cơ chế vì chúng cần cách khắc phục khác nhau.

1. Tab cũ yêu cầu tài nguyên chưa từng tải trước khi triển khai

Đây là trường hợp kinh điển của tab mở lâu. Tài liệu và môi trường chạy đến từ bản A. Bản B thay tệp trên máy chủ. Sau đó người dùng thực hiện thao tác kích hoạt một chunk của A chỉ được tải khi cần. Nếu tài nguyên của A đã bị xóa, yêu cầu thất bại.

2. HTML lỗi thời trỏ tới các chunk không còn tồn tại

CDN, máy chủ trung gian ngược, Service Worker, bộ nhớ đệm trình duyệt hoặc lớp lưu trữ tĩnh có thể giữ tài liệu HTML cũ lâu hơn dự kiến. HTML đó vẫn có thể chứa tham chiếu của bản A trong khi máy chủ nguồn chỉ còn bản B.

Điều này đặc biệt nguy hiểm khi HTML bị gán nhầm chính sách immutable dài. JavaScript có hàm băm và HTML không nên được xem là cùng một loại đối tượng lưu đệm. Chunk có thể bất biến vì URL của nó được phiên bản hóa theo nội dung. Chính HTML quyết định những URL chunk nào thuộc cùng một tập.

3. Triển khai cuốn chiếu hoặc nhiều phiên bản máy chủ cùng phục vụ các bản phát hành khác nhau

Hãy hình dung hai tiến trình Next.js sau bộ cân bằng tải. Một tiến trình đã chạy bản B, tiến trình kia vẫn là A. Tài liệu có thể đến từ một bản, còn yêu cầu điều hướng sau đó lại rơi vào bản kia. Tài liệu Next.js hiện tại gọi đây là lệch phiên bản và lưu ý nó có thể gây thiếu tài nguyên, không khớp Server Functions và lỗi điều hướng.

Mặc định an toàn nhất là chỉ dựng một lần rồi chạy cùng một sản phẩm dựng trên mọi tiến trình thuộc cùng đợt triển khai. Tài liệu tự lưu trữ Next.js cũng khuyên dùng cùng một bản dựng và mã bản dựng nhất quán giữa các vùng chứa, thay vì dựng độc lập cho từng bản sao.

4. Bản thân quá trình triển khai công bố tệp theo sai thứ tự

Ngay cả khi không có tab cũ, tải lên không nguyên tử vẫn có thể tạo ra một trạng thái tạm thời không thể đúng:

HTML mới đã được phục vụ
+
tệp chunk mới chưa sẵn sàng

hoặc ngược lại:

HTML cũ vẫn được phục vụ
+
tệp chunk cũ đã bị xóa

Chỉ cần một khoảng rất ngắn. Người dùng chỉ phải rơi đúng vào khoảng đó một lần.

Mẫu triển khai nguy hiểm là “thay mọi thứ rồi xóa toàn bộ cây cũ”

Một tập lệnh triển khai đơn giản thường bắt đầu giống thế này:

build
rsync --delete new-output/ production/
restart

Nó hấp dẫn vì thư mục sản xuất luôn khớp chính xác với bản dựng mới nhất. Nhưng cách này không thân thiện với máy khách tồn tại lâu.

Với tài nguyên tĩnh có hàm băm, dọn thư mục đến mức chỉ còn một bản phát hành đem lại rất ít lợi ích cho trình duyệt. Tệp cũ không xung đột với tệp mới vì URL khác nhau. Xóa chúng chủ yếu tiết kiệm dung lượng đĩa, nhưng đồng thời biến mọi tham chiếu vẫn còn hợp lệ trong máy khách cũ thành một lỗi 404 tiềm tàng.

Giờ tôi xem chunk cũ là vật liệu bảo đảm khả năng tương thích khi triển khai, không phải rác.

Điều đó không có nghĩa phải giữ mọi bản dựng mãi mãi. Việc dọn dẹp nên là một chính sách lưu giữ riêng, không phải tác dụng phụ tình cờ của việc phát hành phiên bản mới nhất.

Lưu giữ tài nguyên cũ hữu ích, nhưng không có khoảng thời gian hữu hạn nào là giải pháp trọn vẹn

Trong môi trường tự lưu trữ, có thể giữ tài nguyên /_next/static cũ trong một khoảng đệm. Thời lượng chính xác phụ thuộc vào cách sử dụng. Một trang nơi người dùng mở ra, đọc hai phút rồi rời đi có rủi ro khác với ứng dụng được để mở cả ngày làm việc.

Một cách hữu ích để ước lượng thời gian lưu giữ tối thiểu là:

thời gian lưu giữ >=
    thời gian HTML lỗi thời dự kiến còn tồn tại
    + thời gian thực tế của tab mở lâu
    + khoảng thời gian có thể quay lại bản trước
    + biên độ lan truyền của đợt triển khai

Đây không phải bảo đảm toán học. Một tab trình duyệt có thể mở hàng tuần. Không có số giờ hữu hạn nào khiến lỗi ở máy khách cũ trở thành bất khả thi.

Vì vậy tôi thích thiết kế nhiều lớp:

  1. giữ tài nguyên bất biến của các bản trước đủ lâu để những phiên làm việc cũ thông thường tiếp tục hoạt động;
  2. phát hiện lệch phiên bản để máy khách có thể chuyển sang bản hiện tại;
  3. cung cấp một lần tải lại an toàn hoặc lối phục hồi rõ ràng cho người dùng khi tài nguyên thực sự không còn;
  4. theo dõi các chunk của chính ứng dụng bị thiếu để điều chỉnh thời gian lưu giữ bằng dữ liệu thực tế.

Lớp lưu giữ ngăn phần lớn sự cố. Lớp phục hồi xử lý phần đuôi mà không khoảng thời gian hữu hạn nào loại bỏ hoàn toàn được.

Đừng dọn chunk cũ chỉ bằng quy tắc tuổi tệp

Một quy tắc đơn giản như “xóa mọi tệp cũ hơn bảy ngày” cũng có thể sai. Bản hiện tại có thể dùng lại một tệp có hàm băm cũ với thời gian sửa đổi từ lâu chỉ vì nội dung của nó không thay đổi.

Mô hình thu gom tệp không còn dùng tốt hơn cần nhận biết các bản phát hành:

  1. giữ tệp kê khai hoặc danh sách tài nguyên của mọi bản vẫn nằm trong cửa sổ tương thích;
  2. tạo hợp của tất cả đường dẫn tài nguyên mà các bản đó tham chiếu;
  3. không bao giờ xóa thứ gì trong tập được bảo vệ này;
  4. chỉ xóa tài nguyên không còn được tham chiếu sau thêm một khoảng đệm.

Nếu cơ chế đó quá phức tạp cho một hệ thống nhỏ, chủ động để thư mục tài nguyên tĩnh rộng rãi thường rẻ hơn việc điều tra những lỗi hiếm ở máy khách. Tệp có hàm băm đặc biệt phù hợp vì nội dung giống nhau tự nhiên tái sử dụng URL ổn định hoặc ít nhất không thể ghi đè nội dung không liên quan dưới cùng một tên có hàm băm.

Quy tắc tôi sẽ tránh rất đơn giản: không đưa --delete trên cây /_next/static dùng chung vào cùng thao tác kích hoạt bản phát hành mới.

Next.js có cơ chế bảo vệ rõ ràng khỏi lệch phiên bản, nhưng nó không phải kho lưu tài nguyên cũ

Next.js hiện tại hỗ trợ deploymentId để bảo vệ khỏi lệch phiên bản. Cấu hình có thể như sau:

// next.config.js
const nextConfig = {
  deploymentId: process.env.DEPLOYMENT_VERSION,
}

module.exports = nextConfig

Theo tài liệu Next.js hiện tại về deploymentId, khi cấu hình trường này, URL tài nguyên tĩnh do khung quản lý sẽ có tham số ?dpl=<deploymentId>, yêu cầu điều hướng phía máy khách mang thông tin triển khai và máy chủ báo mã triển khai của nó trong phản hồi. Khi Next.js phát hiện không khớp trong lúc điều hướng, nó có thể thực hiện điều hướng toàn trang thay vì tiếp tục chuyển trang phía máy khách với dữ liệu không tương thích.

?dpl=<deploymentId>
x-deployment-id
x-nextjs-deployment-id
data-dpl-id

Điều đó có giá trị, nhưng không nên gán cho tính năng này khả năng mà nó không có. Tài liệu nói rõ Next.js không dùng tham số ?dpl= đi vào để định tuyến yêu cầu tới một phiên bản cụ thể. Tham số này dùng để tránh bản sao cũ trong bộ nhớ đệm. Nếu máy chủ nguồn tự lưu trữ đã xóa vật lý tài nguyên cũ, tham số truy vấn không thể tạo lại tệp.

Vì vậy tôi xem deploymentId là cơ chế phát hiện lệch phiên bản và phục hồi, không phải thứ thay thế cho quy trình triển khai gọn gàng hoặc việc lưu giữ tài nguyên.

Trên các nền tảng có định tuyến theo phiên bản, hạ tầng có thể làm nhiều hơn. Chẳng hạn, tài liệu hiện tại của Vercel về bảo vệ lệch phiên bản mô tả việc khóa phiên bản, giúp các yêu cầu do khung quản lý tiếp tục đến đúng bản triển khai đã phục vụ máy khách ban đầu. Đây là khả năng của nền tảng; tôi không mặc định nó tồn tại trong một cấu hình Nginx hoặc CDN bất kỳ.

Mã bản dựng và mã triển khai giải quyết các vấn đề liên quan nhưng khác nhau

Next.js cũng tạo một mã bản dựng trong khi chạy next build. Nếu nhiều vùng chứa cùng phục vụ một đợt triển khai, chúng không nên âm thầm trở thành các bản dựng khác nhau chỉ vì mỗi máy chủ tự chạy bước dựng riêng.

Một mã bản dựng xác định được có thể gắn với mã bản phát hành, chẳng hạn một bản sửa đổi Git:

// next.config.js
const nextConfig = {
  generateBuildId: async () => process.env.GIT_SHA,
  deploymentId: process.env.GIT_SHA,
}

module.exports = nextConfig

Ví dụ này chỉ nhằm minh họa, không được sao chép từ mã sản xuất của tôi. Quy tắc kiến trúc quan trọng là một bản phát hành logic phải có một sản phẩm dựng nhất quán và một danh tính triển khai duy nhất trên mọi tiến trình phục vụ nó.

generateBuildId nhận diện bản dựng Next.js. deploymentId được tài liệu hóa cụ thể cho việc bảo vệ khỏi lệch phiên bản và tránh bộ nhớ đệm của đợt triển khai khác. Chúng có liên quan, nhưng xem hai tên này là đồng nghĩa chỉ làm việc gỡ lỗi khó hơn.

Tôi sẽ công bố tài nguyên trước khi chuyển lưu lượng sang tài liệu mới

Một trình tự triển khai an toàn hơn cố ý không đối xứng. Tài nguyên bất biến mới có thể tồn tại trước khi bất kỳ ai tham chiếu đến chúng. Ngược lại, HTML mới không được trỏ tới tài nguyên chưa sẵn sàng.

Về nguyên tắc, tôi muốn thứ tự này:

1. dựng bản B đúng một lần
2. tải các tài nguyên /_next/static của B lên
3. xác minh các tài nguyên cần thiết thực sự có thể tải xuống
4. khởi động hoặc chuẩn bị máy chủ và môi trường chạy của B
5. kiểm tra trạng thái của B
6. chuyển nguyên tử lưu lượng tài liệu mới sang B
7. giữ tài nguyên tĩnh của A tiếp tục khả dụng
8. theo dõi B
9. dọn tài nguyên cũ sau

Nếu ứng dụng là bản xuất tĩnh, nguyên tắc vẫn giống nhau: tải tài nguyên có phiên bản lên trước, rồi mới công bố HTML tham chiếu tới chúng. Nếu là kết xuất phía máy chủ sau một máy chủ trung gian ngược, hãy chuẩn bị máy chủ mới và chỉ chuyển lưu lượng khi nó đã được kiểm tra là khỏe mạnh.

Quay lại bản trước cũng nên đối xứng. Giữ thư mục của bản trước và tài nguyên tĩnh của nó cho phép quay lại mà không phải cố tái tạo tệp cũ sau khi sự cố xảy ra.

Điều này không làm mọi lần quay lại đều an toàn. Di chuyển cơ sở dữ liệu hoặc hợp đồng không tương thích của dịch vụ phía sau có thể khiến phiên bản ứng dụng cũ không chạy được ngay cả khi JavaScript của nó vẫn tồn tại. Giữ tài nguyên tĩnh giải quyết vấn đề tương thích tĩnh, không giải quyết mọi vấn đề tương thích giữa các bản của toàn hệ thống.

Thư mục dùng chung cho tài nguyên bất biến phù hợp với tự lưu trữ đơn giản

Với một đợt triển khai Nginx nhỏ, cách đơn giản là tách bản ứng dụng hiện tại khỏi kho tài nguyên tĩnh dùng chung.

Một bố cục minh họa có thể là:

/srv/app/releases/2026-08-13-a/
/srv/app/releases/2026-08-13-b/
/srv/app/current -> /srv/app/releases/2026-08-13-b/

/srv/app/shared/_next/static/...

Mỗi lần triển khai thêm các tệp /_next/static mới vào thư mục dùng chung mà không xóa tệp của những bản trước vẫn đang được giữ. Nginx có thể phục vụ đường dẫn đó với chính sách bất biến:

location ^~ /_next/static/ {
    root /srv/app/shared;
    add_header Cache-Control "public, max-age=31536000, immutable";
}

Cấu hình này chỉ là ví dụ, không phải tuyên bố về cấu hình Nginx chính xác mà tôi đã dùng. Triển khai thực tế phải tính đến quyền tệp, loại MIME, các biến thể nén, hành vi của CDN và bố cục đầu ra cụ thể.

Điểm quan trọng nằm ở kiến trúc: con trỏ có thể thay đổi tới bản hiện tại và kho tài nguyên có phiên bản chủ yếu chỉ được bổ sung có vòng đời khác nhau.

HTML cần chính sách lưu đệm khác với chunk có hàm băm

Cách dễ nhất để tái tạo vấn đề là lưu HTML như thể nó là tài nguyên có hàm băm nội dung.

Với trang Next.js được kết xuất động, khung thường dùng phản hồi không được lưu đệm cho nội dung động riêng theo người dùng. Trang tĩnh và ISR theo các chính sách khác, và CDN có thể lưu chúng một cách hợp lệ. Bản xuất tĩnh do Nginx phục vụ còn phụ thuộc nhiều hơn vào các tiêu đề mà người vận hành cấu hình.

Vì vậy tôi không dùng một quy tắc lưu đệm duy nhất cho “cả trang web”. Tôi phân loại đối tượng:

tài nguyên có hàm băm trong /_next/static
    max-age dài
    immutable
    an toàn để giữ lại

HTML / tài liệu tuyến
    phải có thể chuyển sang bản mới
    chính sách phụ thuộc mô hình kết xuất
    không được tồn tại lâu hơn các tài nguyên mà nó tham chiếu

RSC / dữ liệu điều hướng / dữ liệu API
    quy tắc tương thích và độ mới riêng

Nếu có CDN, tùy thiết kế bộ nhớ đệm mà sau khi triển khai có thể cần làm mới đường dẫn của tài liệu mới. Xóa các chunk cũ có hàm băm khỏi CDN chỉ vì có bản mới thường phản tác dụng: nếu nguồn cũng đã xóa chúng, bạn vừa xóa nốt bản sao cuối cùng có thể cứu máy khách cũ.

Hướng dẫn Next.js về lưu đệm qua CDN hữu ích ở đây vì nó tách lưu đệm trang khỏi chính sách immutable một năm dùng cho tài nguyên /_next/static.

Tự động tải lại là công cụ phục hồi, không phải chiến lược triển khai chính

Một phản ứng phổ biến khi chunk lỗi là “cứ tải lại trang”. Điều này thường có tác dụng vì điều hướng toàn trang lấy tài liệu hiện tại, vốn tham chiếu tới bản dựng hiện tại.

Nhưng tải lại mù quáng sau mọi lỗi tập lệnh tạo ra các vấn đề mới:

  • lỗi tập lệnh bên thứ ba có thể gây một lần tải lại vô ích;
  • sự cố máy chủ thật có thể tạo vòng lặp tải lại vô tận;
  • biểu mẫu chưa lưu có thể mất dữ liệu người dùng;
  • trạng thái thành phần React biến mất khi điều hướng toàn trang;
  • chính bản triển khai bị lỗi đó có thể lại thất bại.

Tài liệu Next.js hiện tại cũng cảnh báo rằng điều hướng toàn trang dùng để phục hồi khi lệch phiên bản có thể làm mất trạng thái thành phần như useState, trong khi trạng thái trong URL hoặc bộ nhớ trình duyệt bền vững có thể còn lại.

Nếu thêm phục hồi phía máy khách, tôi muốn nó hẹp và chỉ thử một lần. Một cách triển khai minh họa có thể là:

const RECOVERY_KEY = 'next-chunk-recovery-attempted'

function isOwnNextAsset(url: string) {
  try {
    const parsed = new URL(url, window.location.href)
    return (
      parsed.origin === window.location.origin &&
      parsed.pathname.startsWith('/_next/static/')
    )
  } catch {
    return false
  }
}

window.addEventListener(
  'error',
  (event) => {
    const target = event.target
    if (!(target instanceof HTMLScriptElement)) return
    if (!isOwnNextAsset(target.src)) return

    reportChunkFailure({
      page: window.location.href,
      asset: target.src,
    })

    if (sessionStorage.getItem(RECOVERY_KEY)) return

    sessionStorage.setItem(RECOVERY_KEY, '1')
    window.location.reload()
  },
  true,
)

Đây cố ý chỉ là ví dụ. Bản sản xuất còn phải tính tới chunk của biểu định kiểu, các dạng lỗi đã biết của khung, luồng làm việc nơi tải lại làm mất công việc, và cách xóa dấu phục hồi sau một lần tải khỏe mạnh.

Với trình soạn thảo, quy trình thanh toán hoặc biểu mẫu dài, tôi có thể thích một thông báo “Có phiên bản mới; hãy lưu công việc rồi tải lại trang” hơn là bắt buộc làm mới.

Dữ liệu giám sát phải cho biết đây có thật sự là lệch phiên bản hay không

Một thông báo chỉ nói “không tải được tập lệnh” là chưa đủ. Để phân biệt chunk cũ đã bị xóa với lỗi mạng ngẫu nhiên, tôi cần ngữ cảnh gắn với đợt triển khai.

Các trường hữu ích gồm:

  • URL của tài nguyên tải thất bại;
  • URL của trang hiện tại;
  • tài nguyên có thuộc chính ứng dụng hay không;
  • mã bản phát hành hoặc mã triển khai mà máy khách nhìn thấy;
  • trình duyệt và hệ điều hành;
  • navigator.onLine như một tín hiệu yếu, không phải bằng chứng kết nối;
  • thời gian kể từ khi trang tải;
  • lỗi có xảy ra ngay sau một đợt triển khai hay không;
  • đây có phải lần phục hồi đầu tiên hay không;
  • trạng thái HTTP khi có thể quan sát ở phía máy chủ;
  • bản phát hành đang phục vụ yêu cầu tại nguồn hoặc máy chủ trung gian.

Khi đó các mẫu trở nên có ý nghĩa hơn nhiều.

Nếu nhiều người dùng ở các mạng khác nhau yêu cầu URL chunk cũ có hàm băm và máy chủ nguồn trả 404 ngay sau bản phát hành, việc không giữ tài nguyên cũ trở thành một lời giải thích mạnh. Nếu chỉ một người thấy lỗi mức mạng mà không có phản hồi HTTP, khả năng lệch phiên bản kém chắc chắn hơn nhiều. Nếu chunk trả 200 nhưng sai loại MIME hoặc trả một trang lỗi HTML, vấn đề nằm ở định tuyến hoặc cấu hình máy chủ trung gian, không chỉ ở việc lưu giữ.

Tôi cũng sẽ cảnh báo riêng lỗi chunk của chính ứng dụng với lỗi tài nguyên bên thứ ba. Đây là thay đổi giám sát được hỗ trợ trực tiếp nhất bởi nhật ký ban đầu của tôi: tín hiệu quan trọng bị trộn với rất nhiều nhiễu trình duyệt không liên quan tới ứng dụng bị hỏng.

Thử nghiệm tái tạo rất đơn giản, nhưng phải giữ nguyên tab cũ

Loại lỗi này dễ bị bỏ sót trong kiểm thử phát hành thông thường vì kỹ sư có xu hướng làm mới ngay sau khi triển khai. Làm vậy phá hủy chính điều kiện cần kiểm tra.

Một thử nghiệm thủ công tốt hơn là:

  1. triển khai bản A;
  2. mở một tab trong điều kiện gần giống sản xuất, với bộ nhớ đệm trình duyệt bật;
  3. chỉ dùng một phần ứng dụng để một số tuyến hoặc tính năng tải khi cần vẫn chưa được tải;
  4. để tab đó mở;
  5. triển khai bản B;
  6. không làm mới tab cũ;
  7. kích hoạt một tuyến hoặc tính năng động cần mã chưa được tải trước đó;
  8. kiểm tra Network và Console;
  9. xác minh URL tài nguyên cũ vẫn trả 200;
  10. xác minh cơ chế phát hiện lệch phiên bản thực hiện điều hướng toàn trang có kiểm soát khi cần.

Tôi sẽ lặp lại thử nghiệm với CDN phía trước, với hai tiến trình máy chủ trong triển khai cuốn chiếu và sau khi cửa sổ lưu giữ đã cấu hình hết hạn.

Một lỗi kiểm thử tinh tế là bật “Disable cache” trong DevTools cho mọi thứ. Cách đó hữu ích cho một số chẩn đoán nhưng làm thay đổi hành vi trình duyệt. Kịch bản tab mở lâu cũng phải được thử với bộ nhớ đệm thực tế, vì bộ nhớ đệm trình duyệt là một phần của hệ thống.

Không phải lỗi chunk nào cũng được sửa bằng cách giữ tệp cũ

Việc lưu giữ hữu ích chính vì nó giải quyết một cơ chế hẹp. Nó không nên trở thành một lời giải thích phổ quát khác.

Chunk của chính ứng dụng có thể thất bại vì:

  • yêu cầu chưa bao giờ tới máy chủ;
  • kết nối bị ngắt;
  • tiện ích trình duyệt chặn nó;
  • một nút CDN gặp lỗi thoáng qua;
  • Nginx định tuyến đường dẫn sai;
  • máy chủ trả tài liệu lỗi HTML thay vì JavaScript;
  • nén hoặc Content-Encoding bị hỏng;
  • quyền tệp sai;
  • một lần triển khai không đầy đủ chưa từng tải chunk lên;
  • tệp có tồn tại nhưng bị xóa quá sớm;
  • máy khách và máy chủ chạy các bản phát hành không tương thích.

Mã phản hồi và thời điểm rất quan trọng. Một lỗi 404 lặp lại đối với URL cũ có hàm băm sau mỗi lần phát hành kể câu chuyện hoàn toàn khác với ERR_CONNECTION_RESET trên một mạng di động duy nhất.

Vì thế tôi sẽ không viết lại sự cố ban đầu thành “tôi đã chứng minh HTML lỗi thời làm hỏng trang”. Tôi không chứng minh điều đó. Tôi đã quan sát một lỗi thật ở chunk của chính ứng dụng và xác định lệch phiên bản là một cơ chế nghiêm trọng đáng được loại trừ bằng thiết kế.

Triển khai an toàn nhất coi máy khách cũ là một phần của bề mặt phát hành

Sai lầm sâu hơn là nghĩ rằng triển khai thay bản A bằng bản B tại đúng một thời điểm.

Trên máy chủ, liên kết tượng trưng hoặc hệ thống điều phối vùng chứa có thể khiến nó trông như vậy. Trên mạng, đối tượng CDN cũ có thể còn tồn tại. Trong trình duyệt, tài liệu của A có thể tiếp tục chạy lâu sau khi B hoạt động. Khi triển khai cuốn chiếu, cả hai phiên bản máy chủ có thể cùng hoạt động. Khi quay lại, B có thể biến mất và A lại trở thành bản hiện tại.

Vì vậy bề mặt phát hành thật là một khoảng thời gian, không phải một điểm.

Các quy tắc triển khai Next.js của tôi hiện xoay quanh ý tưởng này:

  • Chỉ dựng một lần cho mỗi bản phát hành logic. Không để các bản sao âm thầm tạo đầu ra khác nhau.
  • Công bố tài nguyên bất biến trước khi công bố các tham chiếu tới chúng.
  • Giữ tài nguyên cũ có hàm băm trong một cửa sổ tương thích được chọn có chủ ý.
  • Không áp dụng cho HTML có thể thay đổi cùng chính sách lưu đệm của chunk có hàm băm.
  • Dùng deploymentId khi mô hình triển khai có thể tạo lệch phiên bản.
  • Chỉ dùng bảo vệ ở cấp nền tảng khi nền tảng lưu trữ thực sự có định tuyến theo phiên bản.
  • Chỉ thử phục hồi một lần và phải tính tới trạng thái người dùng.
  • Theo dõi lỗi chunk của chính ứng dụng như một tín hiệu sản xuất riêng.
  • Kiểm thử triển khai với một tab cũ vẫn mở.
  • Dọn tài nguyên cũ sau, không dọn trong lúc kích hoạt bản mới.

Quy tắc tôi dùng hiện nay

Một bản dựng xanh và một trang mới hoạt động khỏe mạnh không chứng minh rằng đợt triển khai an toàn cho những người đã ở trong ứng dụng từ trước.

Tab cũ không phải phần thừa lỗi thời. Nó là một máy khách thật đang chạy một bản phát hành trước đó thật.

Khi tôi bắt đầu nghĩ về triển khai theo cách này, vấn đề chunk trở nên ít bí ẩn hơn. Hàm băm nội dung cho tài nguyên một danh tính ổn định. Lưu đệm dài hạn làm danh tính đó hiệu quả. Nhưng hệ thống triển khai phải tôn trọng danh tính đủ lâu hoặc cung cấp cách có kiểm soát để máy khách tiến sang bản mới.

Tôi không cần giữ mọi bản cũ mãi mãi. Tôi cần hệ thống vượt qua giai đoạn mà máy khách cũ và máy chủ mới cùng tồn tại một cách hoàn toàn hợp lệ.

Đó là hợp đồng triển khai tôi quan tâm: người dùng mới nhận bản mới, người dùng cũ không mất những tệp mà phiên bản hiện tại của họ vẫn biết cách yêu cầu, và mọi lệch phiên bản còn lại đi vào một đường phục hồi có chủ đích thay vì kết thúc ở trang bị hỏng.