Vì sao Zola Smoke Test chạy lâu và cách tối ưu CI
Vì sao Zola Smoke Test chạy lâu? Nhật ký tối ưu một blog 500+ trang và kế hoạch đưa ảnh lên Cloudflare R2
Có một cảm giác rất quen với người làm blog kỹ thuật: lúc đầu mọi thứ chạy rất nhanh. Một bài viết mới, một vài template, vài tấm ảnh, vài file CSS. Bấm push là CI chạy vèo vèo. Nhưng sau vài tháng, blog không còn là “một website tĩnh nhỏ” nữa. Nó thành một hệ sinh thái.
Với SEOMONEY, câu chuyện bắt đầu khi tôi nhìn thấy Zola build smoke test đứng khá lâu trong GitHub Actions. Không fail ngay, không báo lỗi ngay, chỉ im lặng ở dòng:
text Building site... -> Creating 501 pages (0 orphan) and 12 sections
Lúc đó cảm giác rất khó chịu: không biết nó đang build thật, đang treo, hay đang chuẩn bị nổ lỗi. Sau vài lần debug, tôi nhận ra vấn đề không đơn giản là “Zola chậm”. Vấn đề nằm ở toàn bộ pipeline CI/CD đã lớn lên cùng blog.
Smoke Test là gì, và vì sao nó làm tôi sốt ruột?
Trong CI/CD, “smoke test” là bài kiểm tra nhanh để biết bản build có cháy nhà hay không. Với một blog Zola, smoke test thường là chạy:
bash zola build
Ý tưởng rất hợp lý: nếu zola build chạy được thì site có khả năng render được lên production. Nhưng điểm quan trọng là zola build không chỉ build một file vừa sửa. Nó render toàn bộ site tĩnh ra thư mục output, thường là public [1].
Khi site còn vài chục trang, việc này gần như không đáng kể. Khi site đã có hơn 500 trang, nhiều section, nhiều file data/*.json, nhiều widget, nhiều dashboard, nhiều script build dữ liệu trước khi render, smoke test bắt đầu trở thành một bài kiểm tra nặng.
Case thật: lỗi không nằm ở chỗ tôi tưởng
Trong một lần merge UI cho trang Blog Heart Beat, GitHub Actions báo fail ở bước Zola build smoke test. Log cuối cùng như sau:
text ERROR Failed to render page 'content/tools/blog-heart-beat.md' ERROR Reason: Failed to render 'page.html' ERROR Reason: Filter call 'date' failed ERROR Reason: Filter `date` received an incorrect type for arg `value`: got `Null`
Lỗi thật ra rất nhỏ: page mới thiếu date trong front matter, trong khi template page.html đang gọi filter date.
Tức là: không phải Zola sai, không phải server lỗi, cũng không phải GitHub Actions bị điên. Chỉ là một page tool mới không có metadata mà template đang kỳ vọng.
Nhưng cái làm tôi mất thời gian không phải lỗi date. Cái làm tôi mất thời gian là build mất vài phút mới đi tới lỗi đó. Một lỗi nhỏ, nhưng feedback loop quá dài.
Đó là dấu hiệu đầu tiên cho thấy blog đã đến lúc phải tối ưu pipeline.
Vì sao blog Zola lớn dần lại build chậm?
Zola là static site generator. Nó dùng template engine Tera để render nội dung thành HTML tĩnh [2]. Kiến trúc này rất nhanh khi site gọn. Nhưng khi blog phát triển, tốc độ build không chỉ phụ thuộc vào Zola nữa.
Với một blog vận hành nghiêm túc, pipeline thường có thêm nhiều bước:
text - Build reference index - Build related posts - Build GitHub activity data - Build Google Rank / SEO score - Build OG images - Check internal links - Check 404 - Validate social share thumbnails - Security audit public surface - QA vaccine rules - Legacy scripts còn sót lại
Vậy nên khi thấy Zola build smoke test lâu, đừng vội kết luận Zola chậm. Nên hỏi lại:
text 1. Trước khi zola build, CI đã generate bao nhiêu data? 2. Template có load_data quá nhiều không? 3. Có file JSON lớn nào bị render lặp lại ở nhiều page không? 4. Có bước cũ không còn dùng nhưng vẫn chạy không? 5. Có ảnh/media nặng nằm trong repo làm clone/build/cache nặng không? 6. Có đang chạy full build cho mọi PR dù chỉ sửa một file nhỏ không?
Trong case của tôi, có một chi tiết rất đáng chú ý: chức năng premium/paywall đã bỏ, nhưng CI vẫn còn bước:
text Strip premium content before build
Nó không phải nguyên nhân trực tiếp gây fail lần này, nhưng là dấu hiệu của “pipeline già đi”: tính năng đã bỏ nhưng bước kiểm tra vẫn còn. Cứ mỗi lần CI chạy, những bước legacy như vậy lại cộng thêm độ phức tạp, thời gian và khả năng gây nhiễu.
Bài học 1: đừng chỉ dùng một loại QA cho mọi thứ
Sai lầm phổ biến là bắt mọi pull request chạy toàn bộ bài kiểm tra nặng nhất. Cách này an toàn, nhưng càng về sau càng tốn thời gian.
Tôi muốn tách pipeline thành hai tầng:
Heavy Gate: - zola build full - 404 checker - social thumbnail validation - OG image validation - public-surface security audit - build GitHub activity - build dashboard data ```
Quick Gate chạy cho hầu hết PR. Heavy Gate chạy khi merge vào `main`, chạy theo lịch, hoặc khi file thay đổi có ảnh hưởng thật tới render toàn site.
Ví dụ, nếu PR chỉ sửa một bài Markdown bình thường, không nhất thiết phải gọi toàn bộ GitHub API activity, build profile badges, generate SEO report và kiểm tra toàn bộ ảnh social. Nhưng nếu PR sửa `templates/page.html`, `templates/base.html`, `config.toml`, `sass/`, `static/js/`, hoặc `content/tools/`, thì full build là cần thiết.
## Bài học 2: Zola build lâu thì phải đo từng bước, không đo cảm tính
Một pipeline tốt nên in thời gian từng bước. Thay vì chỉ nhìn “QA mất 8 phút”, cần biết:
```text - Checkout mất bao lâu? - Install dependency mất bao lâu? - Build GitHub activity mất bao lâu? - Build reference index mất bao lâu? - Zola build mất bao lâu? - 404 checker mất bao lâu? - OG image validation mất bao lâu? ```
Từ đó mới biết nên tối ưu ở đâu. Nếu `zola build` chỉ mất 40 giây nhưng GitHub activity mất 2 phút, tối ưu template là sai hướng. Nếu `zola build` mất 4 phút vì template gọi `load_data` lặp lại ở nhiều page, phải tối ưu template/data. Nếu install dependency tốn quá lâu, phải cache Python/Node dependency.
Một rule tôi muốn thêm vào workflow:
```bash start=$(date +%s) # run command end=$(date +%s) echo "STEP_DURATION=$((end-start))s" ```
Rất đơn giản, nhưng giúp chẩn đoán nhanh hơn rất nhiều.
## Bài học 3: dọn tính năng cũ quan trọng không kém thêm tính năng mới
Một website sống lâu sẽ có rác lịch sử. Với SEOMONEY, premium/paywall từng là một phần của hệ thống, nhưng hiện đã bỏ. Vậy mà pipeline vẫn còn test, script và step liên quan.
Việc cần làm không phải là xoá bừa. Cách an toàn hơn là:
```text 1. Đánh dấu tính năng premium/paywall là retired. 2. Gỡ step “Strip premium content before build” khỏi QA/build workflow. 3. Gỡ paywall unit tests khỏi default CI. 4. Nếu script cũ còn giá trị tham khảo, chuyển vào docs/archive hoặc đánh dấu deprecated. 5. Đảm bảo qa_check.py không còn cảnh báo thiếu MoMo/paywall config. 6. Không để workflow build tự sửa content theo logic paywall nữa. ```
Khi một tính năng đã nghỉ hưu, CI cũng phải nghỉ hưu phần tương ứng. Nếu không, mỗi lần build là một lần kéo theo quá khứ.
## Vậy Cloudflare R2 giúp gì?
Cloudflare R2 không giải quyết toàn bộ vấn đề CI, nhưng nó giải quyết một mảng rất đáng kể: media và static asset nặng.
R2 là object storage, tương thích kiểu S3, dùng để lưu các file như ảnh, video, file download, dữ liệu tĩnh lớn. Điểm hấp dẫn nhất là R2 không tính phí egress ra Internet như nhiều dịch vụ object storage truyền thống [3]. R2 cũng hỗ trợ public bucket và custom domain, ví dụ có thể gắn bucket với một domain như:
```text https://assets.seomoney.org ```
Ý tưởng của tôi là:
```text - HTML vẫn host bằng GitHub Pages. - Ảnh blog nặng đưa sang Cloudflare R2. - Các file media lớn dùng assets.seomoney.org. - Repo Git chỉ giữ content, template, CSS/JS quan trọng và ảnh fallback cần thiết. ```
Đây là mô hình khá hợp lý cho static blog đang lớn dần. GitHub Pages lo phần HTML. Cloudflare lo phần asset delivery.
## Nhưng đừng đưa mọi thứ lên R2 một cách mù quáng
R2 không phải thùng rác để ném mọi file vào. Nếu move sai, có thể làm vỡ social share, vỡ OG image, vỡ cache, hoặc gây khó rollback.
Tôi sẽ chia asset thành 4 nhóm:
```text Nhóm 1 — Giữ local: - OG fallback mặc định - Logo quan trọng - Icon hệ thống - CSS/JS critical - Placeholder nội bộ
Nhóm 2 — Có thể đưa lên R2: - Ảnh minh hoạ bài viết lớn - Ảnh gallery - Ảnh ít thay đổi - File download tĩnh
Nhóm 3 — Cần cân nhắc: - Ảnh social share riêng từng bài - Ảnh cần watermark/compliance - Ảnh nằm trong structured data
Nhóm 4 — Không nên đưa ngay: - File đang được script build tự động tạo - File có đường dẫn được hardcode trong template - File chưa có manifest mapping ```
Trước khi move, phải có report:
```text old path: static/img/example.webp new URL: https://assets.seomoney.org/img/example.webp used by: content/posting/... size: 820KB risk: low / medium / high rollback: restore local path ```
## Kế hoạch R2 cho SEOMONEY
Tôi hình dung kế hoạch triển khai R2 theo từng bước nhỏ:
### Bước 1: audit asset
Chạy script quét `static/img`, tìm các file lớn:
```bash find static/img -type f -size +300k -print ```
Sau đó xuất report Markdown:
```text reports/r2-asset-offload-plan.md ```
Report này cần có:
```text - Tổng dung lượng static/img - Top 50 file lớn nhất - File nào đang được bài viết dùng - File nào là OG/social asset - File nào nên giữ local - File nào là candidate đưa lên R2 ```
### Bước 2: tạo bucket và custom domain
Tạo bucket R2, ví dụ:
```text seomoney-assets ```
Sau đó gắn custom domain:
```text assets.seomoney.org ```
Khi dùng public bucket/custom domain, cần hiểu rõ bucket sẽ public qua domain đó. Nếu file private thì phải cấu hình Access trước khi public domain [4].
### Bước 3: upload thử một nhóm nhỏ
Không move toàn bộ ảnh. Chọn 5–10 ảnh lớn, ít rủi ro, không phải OG fallback.
Sau đó cập nhật bài viết tương ứng từ:
```markdown  ```
sang:
```markdown  ```
### Bước 4: đo lại
Sau khi move thử, đo lại:
```text - Repo size - Checkout time - Zola build time - Page weight - LCP ảnh chính - Cache hit - 404 image - Social thumbnail ```
Nếu ổn mới mở rộng.
## Tối ưu CI/CD trước khi tối ưu hosting
Một nhầm lẫn dễ gặp là nghĩ “đưa ảnh sang R2 là build sẽ nhanh ngay”. Không hẳn.
Nếu build chậm vì template xử lý nhiều data, R2 không giúp nhiều. Nếu build chậm vì GitHub API call, R2 cũng không giúp. Nếu build chậm vì 404 checker quét quá rộng, R2 càng không liên quan.
Vậy nên thứ tự tối ưu nên là:
```text 1. Sửa lỗi build rõ ràng. 2. Dọn workflow legacy. 3. Tách Quick Gate và Heavy Gate. 4. Thêm timing report từng step. 5. Cache dependency và data. 6. Audit asset. 7. Offload ảnh lớn sang R2. 8. Đo lại bằng số liệu. ```
Tối ưu tốt là tối ưu có đo lường, không phải tối ưu bằng cảm giác.
## Bộ lệnh tôi muốn giữ để debug lần sau
Khi CI báo Zola build fail, tôi sẽ không đoán nữa. Tôi sẽ chạy:
```bash gh run view <RUN_ID> --log-failed ```
Nếu cần xem step nào fail:
```bash gh run view <RUN_ID> --json jobs --jq '.jobs[] as $j | $j.steps[] | select(.conclusion=="failure") | "\($j.databaseId) \($j.name) :: \(.name)"' ```
Nếu local build im lặng quá lâu:
```bash python3 - <<'PY' import subprocess, sys, time
cmd = ["zola", "build"] print("RUN:", " ".join(cmd), flush=True) t0 = time.time()
try: p = subprocess.run(cmd, text=True, capture_output=True, timeout=480) print(p.stdout) print(p.stderr, file=sys.stderr) print(f"DONE in {time.time()-t0:.1f}s, code={p.returncode}", flush=True) sys.exit(p.returncode) except subprocess.TimeoutExpired as e: print("TIMEOUT: zola build quá 480s", file=sys.stderr) print((e.stdout or b"").decode(errors="ignore")[-4000:]) print((e.stderr or b"").decode(errors="ignore")[-4000:], file=sys.stderr) sys.exit(124) PY ```
Nếu nghi một page mới gây lỗi, cô lập page đó ra để build thử:
```bash mv content/tools/blog-heart-beat.md /tmp/blog-heart-beat-debug.md zola build mv /tmp/blog-heart-beat-debug.md content/tools/blog-heart-beat.md ```
Nhưng bài học xương máu là: debug kiểu này phải restore file trước khi commit. Nếu không, rất dễ lỡ tay xoá luôn page khỏi production.
## Kết luận
Zola vẫn là một static site generator rất đáng tin. Vấn đề không phải “Zola chậm”, mà là blog đã lớn lên. Khi blog có hàng trăm trang, nhiều dashboard, nhiều workflow, nhiều dữ liệu tự sinh và nhiều lớp kiểm tra, CI/CD phải được thiết kế lại.
Với SEOMONEY, hướng đi tiếp theo của tôi là:
```text - Khôi phục và bảo vệ Blog Heart Beat tool. - Bỏ premium/paywall strip khỏi CI vì tính năng đã retired. - Tách Quick Gate và Heavy Gate. - Thêm timing report cho từng bước. - Cache các data build chậm. - Audit ảnh/media lớn. - Đưa asset nặng sang Cloudflare R2 qua assets.seomoney.org. ```
Một blog tốt không chỉ là có bài viết hay. Nó còn phải có pipeline sạch, build ổn, rollback được, và không để mỗi lần push code trở thành một cuộc đoán mò.
Đến một lúc nào đó, “blog cá nhân” cũng cần WebOps như một sản phẩm thật. Và đó là lúc mình bắt đầu học được nhiều nhất.
## Checklist
- Kiểm tra branch trước khi viết.
- Tạo file Markdown đúng thư mục.
- Chạy `zola check`.
- Chạy `python3 qa_check.py`.
- Commit đúng file.
- Push branch và tạo PR.
💬 BÌNH LUẬN
Đăng nhập GitHub để comment. Hỗ trợ markdown, reaction, reply.