Nâng cấp Astro 6: cái bẫy 10MB và bài học deploy cho chắc
Bản nâng cấp tưởng nửa ngày hoá ra đụng một cái blocker thật: worker phình lên 53MB, vượt giới hạn 10MB của Cloudflare. Đây là cách mình gỡ, và vì sao 'build pass' không bao giờ là 'xong'.
Site này (arealisticdreamer.com) chạy trên Astro + Cloudflare. Astro vừa ra bản 6, mình nâng cấp từ 5.17 lên 6.4. Tưởng là một buổi nhẹ nhàng — bump version, sửa vài cái breaking change, deploy. Hoá ra đụng đúng một cái bẫy mà chỉ khi deploy thật lên Cloudflare mới lòi ra. Bài này kể lại nguyên cái hố đó, cách lấp, và bài học mình rút ra về chuyện “thế nào mới gọi là xong”.
Tại sao nâng cấp
Astro 6 không chỉ là số version tăng. Mấy thứ thật sự đáng cho stack của mình:
- Dev server chạy
workerdthật. Trước đâyastro devchỉ giả lập môi trường Cloudflare Worker. Astro 6 chạy đúng runtime, đúng binding (R2, KV) ngay trên máy — hết cảnh “chạy ngon ở dev, chết ở prod”. - Render nhanh ~2x nhờ “queued rendering” — đỡ tốn CPU mỗi request, mà site mình thì nặng nội dung.
- Live Content Collections, Fonts API, CSP API thành stable.
Đủ lý do để làm. Vấn đề là Astro 5 → 6 là major version, không phải drop-in.
Phần dễ: đúng như sách
May mắn là codebase mình đã viết theo lối Astro 5 hiện đại, nên phần lớn breaking change chỉ là sửa máy móc:
<ViewTransitions />→<ClientRouter />- File config
src/content/config.ts→src/content.config.ts - Trang động có
getStaticPaths()phải thêmexport const prerender = true - Adapter Cloudflare 13:
maintrong wrangler trỏ vào@astrojs/cloudflare/entrypoints/serverthay vì file build; bỏ optionplatformProxy(đã xoá) - Lucide v1 bỏ mấy brand icon (Facebook/LinkedIn) vì lý do thương hiệu → mình tự inline SVG
Build cả hai edition (global + vn) sạch. Tới đây ai cũng tưởng xong rồi. Chưa.
Phần đau: worker 53MB, giới hạn 10MB
Khi mình thử upload bản preview lên Cloudflare, nó từ chối thẳng:
✘ Your Worker exceeded the size limit of 10 MiB.
Here are the 5 largest dependencies:
- dist/server/chunks/_astro_data-layer-content_*.mjs - 24358 KiB
Một cái chunk 24MB. Đào ra thì hiểu: Astro 6 gói toàn bộ “content data store” vào trong worker. Site mình có một collection tên stream-content — đây là full-text các bài recap mình lưu lại, ~9.7MB (riêng một file 6.9MB). Astro 5 không nhồi nguyên đống này vào worker; Astro 6 thì có. 9.7MB content → serialize thành 24MB → worker phồng lên 53MB → vượt trần 10MB của Cloudflare. Deploy bị chặn.
Tệ hơn: cái option escape hatch ngày xưa (cloudflareModules) đã bị xoá ở adapter 13, và adapter mới không có config nào để đẩy content ra ngoài bundle.
Đây không phải lỗi vặt. Đây là xung đột thật giữa thiết kế của mình và cách Astro 6 đóng gói.
Cách gỡ: content không thuộc về “content layer”
Câu hỏi đúng không phải “làm sao nén nhỏ lại”, mà là: stream-content có cần nằm trong content layer không?
Không. Nó tồn tại chỉ để lazy-load lúc runtime — khi người đọc bấm “xem full content”, frontend gọi /api/stream-content/{slug} để lấy về. Nó chưa bao giờ được render tĩnh lúc build. Một đống 9.7MB như vậy không nên nằm trong content layer.
Lời giải: chuyển nó ra R2 (object storage của Cloudflare).
- 138 file recap → upload hết lên R2 (
stream-content/{slug}.md) - API route đọc từ R2 thay vì content collection
- Gỡ
streamContentkhỏicontent.config.ts→ cái chunk 24MB tụt còn 1.9MB - Worker ingest (chỗ sinh ra recap) ghi thẳng vào R2 thay vì commit file vào GitHub
Kết quả: worker còn 6.25 MiB nén — lọt thỏm dưới trần 10MB.
Bài học kiến trúc: content layer là để render, không phải để làm kho. Cái gì chỉ cần fetch lúc runtime thì để ở storage (R2/KV), đừng nhét vào bundle.
Vì sao “build pass” không phải “xong”
Đây là phần mình muốn nhấn mạnh. Sau khi build sạch và worker đã nhỏ lại, mình vẫn chưa deploy. Mình chạy wrangler dev --remote — tức là chạy worker thật trên edge của Cloudflare với binding R2 thật — rồi curl thử cái API. Và nó 404.
Log chỉ ra ngay:
Error: Astro.locals.runtime.env has been removed in Astro v6.
Use 'import { env } from "cloudflare:workers"' instead.
Mình viết API route theo lối Astro 5 (locals.runtime.env). Astro 6 đổi cách lấy binding rồi. Build không bắt được lỗi này — vì nó là lỗi runtime, không phải lỗi compile. Nếu mình tin “build pass = xong” và deploy thẳng, người dùng sẽ gặp một cái 404 câm lặng: bấm “xem full content” và không có gì hiện ra.
Sửa một dòng import, build lại, test lại trên remote: 200, JSON đúng, content render ra HTML. Lúc đó mới gọi là verified.
Chưa hết — lúc deploy còn lòi thêm hai cái nữa:
- CI dùng
npm ci(không có--legacy-peer-deps) chết vì một package (@vite-pwa/astro) chưa khai báo tương thích Astro 6 → thêm.npmrc. - Adapter 6 mặc định bật session, tự đòi tạo KV namespace → mình bind sẵn namespace có sẵn.
Mỗi cái này đều chỉ xuất hiện ở một tầng cụ thể: một cái ở runtime, một cái ở CI, một cái ở lúc provision. Không tầng nào thấy được lỗi của tầng kia.
Outcome
Astro 6 giờ chạy thật trên production. Mình verify trực tiếp bằng curl:
- Trang chủ: 200
- API stream-content (đọc từ R2): 200 + JSON đúng
- Flashcards, About, Stream: 200 hết
Đổi lại mình có: dev server chạy workerd thật, render nhanh hơn, và một kiến trúc sạch hơn — content nặng nằm đúng chỗ của nó (R2), worker gọn nhẹ.
Ba điều mang về
- Major version migration: cái khó không nằm ở chỗ sách ghi. Mấy breaking change có trong upgrade guide đều dễ. Cái chặn mình là thứ không có trong guide — cách bundle content đụng giới hạn của hạ tầng.
- “Build pass” là điều kiện cần, không phải điều kiện đủ. Lỗi runtime (env API đổi) và lỗi hạ tầng (worker size, CI peer-dep) build đều không thấy. Phải chạy thật trên môi trường thật mới biết.
- Luôn có đường lùi trước khi tiến. Trước khi deploy, mình ghi lại version worker đang chạy + commit git + giữ backup content ở 3 nơi (R2, git history, branch cũ). Có lỗi là revert tức thì, không cần build lại. Triển khai mà không có nút undo thì không phải triển khai, đó là đánh bạc.