Nội dung bài viết
Sau nhiều năm dựng website bằng WordPress rồi chuyển sang React, tôi nhận ra một điều: phần lớn các trang tôi làm không cần nhiều JavaScript đến thế. Một trang giới thiệu công ty, một blog, một portfolio, chúng chủ yếu là nội dung tĩnh. Nhưng nếu dùng Next.js hay Nuxt, người dùng vẫn phải tải về hàng trăm KB JavaScript chỉ để đọc một bài viết.
Astro giải quyết đúng vấn đề đó. Mặc định nó không gửi một dòng JavaScript nào xuống trình duyệt, và chỉ hydrate những phần bạn chủ động khai báo là cần tương tác. Đó là mô hình “islands”.
Kiến trúc islands là gì
Hãy hình dung một trang chi tiết sản phẩm. Phần lớn diện tích là ảnh, mô tả, thông số kỹ thuật, những thứ không bao giờ thay đổi sau khi tải xong. Chỉ có vài chỗ thực sự cần JavaScript: bộ chọn số lượng, nút thêm vào giỏ hàng, thư viện ảnh có thể vuốt.
Với một ứng dụng React truyền thống, toàn bộ trang là một cây component được hydrate từ đầu tới cuối. Trình duyệt phải tải React, tải code của bạn, dựng lại toàn bộ cây rồi mới gắn được sự kiện.
Với Astro, mỗi phần tương tác là một hòn đảo riêng biệt nằm giữa biển HTML tĩnh:
---
import ProductGallery from "../components/ProductGallery.jsx";
---
<h1>{product.name}</h1>
<p>{product.description}</p>
<!-- Chỉ component này được hydrate -->
<ProductGallery client:visible images={product.images} />
Chỉ thị client:visible nói với Astro: chỉ tải JavaScript cho component này khi người dùng cuộn tới nó. Nếu khách chỉ đọc mô tả rồi thoát, họ không tải một byte JavaScript nào.
Astro có vài chỉ thị khác, và chọn đúng cái nào quan trọng hơn nhiều người nghĩ:
client:loadhydrate ngay khi trang tải. Dùng cho thứ phải tương tác được lập tức, ví dụ menu di động.client:idlechờ tới khi trình duyệt rảnh. Hợp với những thứ quan trọng nhưng không gấp.client:visiblechờ tới khi component lọt vào khung nhìn. Đây là lựa chọn tôi dùng nhiều nhất.client:mediachỉ hydrate khi một media query khớp. Rất hợp cho component chỉ tồn tại trên di động.client:onlybỏ qua render phía server hoàn toàn. Cần khi component phụ thuộc vàowindowhoặclocalStorage.
Content Collections
Đây là tính năng khiến tôi ở lại với Astro. Thay vì đọc file Markdown rồi tự đoán xem frontmatter có những trường gì, bạn khai báo schema bằng Zod:
import { defineCollection, reference, z } from "astro:content";
import { glob } from "astro/loaders";
const post = defineCollection({
loader: glob({ pattern: "**/*.md", base: "./src/content/post" }),
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
category: reference("category"),
tags: z.array(z.string()).optional(),
draft: z.boolean().default(false),
}),
});
Giá trị thực sự nằm ở chỗ: nếu tôi gõ sai tên category trong một bài viết, hoặc quên trường description, build sẽ thất bại kèm thông báo chỉ đúng file và đúng dòng. Lỗi được bắt lúc build chứ không phải lúc khách hàng phát hiện trang trắng.
Hàm reference("category") còn tạo liên kết thực sự giữa hai collection. Astro kiểm tra category được tham chiếu có tồn tại hay không, và trong template bạn lấy dữ liệu bằng getEntry() với đầy đủ gợi ý kiểu.
z.coerce.date() cũng đáng nói: nó tự chuyển chuỗi 2026-05-28 trong frontmatter thành đối tượng Date, nên bạn sắp xếp và định dạng ngày mà không phải tự parse.
Không phụ thuộc framework
Astro không bắt bạn chọn phe. Cùng một dự án có thể dùng React cho phần này, Svelte cho phần kia, và phần lớn còn lại là component .astro thuần không tốn JavaScript nào.
Trên thực tế tôi thấy mình dùng ít framework hơn hẳn kỳ vọng ban đầu. Khi đã có HTML tĩnh làm mặc định, rất nhiều thứ tưởng chừng cần React hóa ra chỉ cần vài dòng JavaScript thuần. Chính trang web này không dùng framework UI nào cả, chỉ Astro với Bootstrap và Sass.
Những gì tôi rút ra khi dựng trang này
Component .astro nên là mặc định. Chỉ dùng component React hay Svelte khi thực sự cần trạng thái phía client. Mỗi lần bạn thêm một island là thêm một gói JavaScript.
Cẩn thận với ranh giới server và client. Phần frontmatter giữa hai dấu --- chạy lúc build, trên máy chủ. Nó có thể đọc file, gọi API, truy vấn database. Nhưng props truyền vào một island phải serialize được, nên đừng cố truyền hàm hay class instance qua đó.
Kiểm tra kích thước gói ngay từ đầu. Rất dễ vô tình kéo theo một thư viện nặng vào island. Chạy astro build rồi nhìn vào thư mục dist để biết mình đang gửi gì xuống trình duyệt.
View Transitions cho cảm giác như SPA. Astro hỗ trợ sẵn chuyển trang mượt mà mà không cần biến trang thành ứng dụng một trang. Bạn giữ được HTML tĩnh và vẫn có hiệu ứng chuyển cảnh.
Khi nào không nên dùng Astro
Công bằng mà nói, Astro không hợp với mọi thứ. Nếu bạn đang xây một ứng dụng mà gần như toàn bộ màn hình đều có trạng thái, ví dụ bảng điều khiển quản trị, công cụ chỉnh sửa, hệ thống đặt phòng có lịch tương tác liên tục, thì mô hình islands sẽ trở nên gượng ép. Lúc đó Next.js, Remix hay một SPA React thuần là lựa chọn tự nhiên hơn.
Ranh giới tôi thường dùng để quyết định: nếu tôi thấy mình đang biến quá nửa số component thành island, đó là dấu hiệu chọn sai công cụ.
Kết quả thực tế
Trang web này build ra 26 trang tĩnh trong khoảng 10 giây, gửi xuống trình duyệt gần như chỉ có HTML và CSS. Nó chạy trên gói miễn phí của Cloudflare Pages và không có gì để mà chậm được.
Với một trang thương hiệu cá nhân, Astro chạm đúng điểm cân bằng giữa trải nghiệm lập trình và tốc độ trang.