Tổng Quan
Next.js từ lâu là lựa chọn phổ biến cho các dự án Web, từ Web App B2B phức tạp (SaaS) đến các trang Portfolio cá nhân. Tuy nhiên, kiến trúc của Next.js App Router và React Server Components (RSC) có những hạn chế đo lường được đối với các trang thiên về nội dung tĩnh (Content-Driven Sites), chủ yếu do dung lượng JavaScript gửi xuống trình duyệt trên mỗi trang.
Bài viết này đi sâu phân tích và so sánh kỹ thuật giữa Next.js và Astro – một framework đang được đánh giá rất cao trong mảng phát triển website nội dung nhờ ưu thế tối ưu hóa chỉ số Lighthouse, giảm thiểu độ trễ TTFB và tăng cường trải nghiệm người dùng.
Case study này ghi nhận quá trình tái cấu trúc (Refactoring) toàn bộ hệ thống nicknguyen8.com từ Next.js 14 sang Astro 5. Quá trình bao gồm việc chuẩn hoá lại kiến trúc, routing, state management và CI/CD pipeline.
Công nghệ sử dụng:
- Astro 5.x
- Vanilla CSS (Custom properties, không dùng utility framework)
- React 18 (Chỉ dùng cho Interactive Islands)
- Nano Stores (Thay thế React Context)
- Cloudflare Pages (Hosting & CDN)
- Keystatic (Local Git CMS)
Phần 1: Phân Tích Hạn Chế Của Next.js Trong Việc Xây Dựng Trang Nội Dung
1.1 Vấn đề Hydration Waterfall
Dù sử dụng Next.js Pages Router hay App Router, đối với một trang nền tảng văn bản và hình ảnh như Blog, framework này mặc định vẫn thực thi quy trình Hydration phía Client.
Hydration là quá trình trình duyệt tải xuống bundle JavaScript (thường chứa React Runtime), sau đó chạy quá trình tái đồng bộ và gắn các event listeners vào cây DOM HTML đã được Server (hoặc bước SSG) render trước đó.
Biểu đồ dưới đây mô tả luồng tải trang truyền thống của React/Next.js so với luồng tải tĩnh:
sequenceDiagram
participant Browser
participant Server (Next.js)
Browser->>Server (Next.js): GET /blog/article
Server (Next.js)-->>Browser: Trả về HTML (Header, Nội dung, Footer)
Note over Browser: User thấy UI nhưng chưa tương tác được
Browser->>Server (Next.js): GET frontend-bundle.js (React Runtime)
Server (Next.js)-->>Browser: Trả về lượng lớn file JS
Note over Browser: Trùng lập xử lý (TBT Spike)
Browser->>Browser: Parse JS & Thực thi Hydration
Note over Browser: Trang web Interactive hoàn toàn
Hậu quả của quy trình này trên một trang blog:
- CPU Spike: Các thiết bị di động có cấu hình thấp phải tiêu tốn tài nguyên CPU để phân tích và thực thi hàng chục đến hàng trăm KB JavaScript, dù người dùng chỉ có nhu cầu đọc văn bản.
- Total Blocking Time (TBT) cao: Main thread của trình duyệt bận xử lý tác vụ Hydration, khiến giao diện có thể tạm thời không phản hồi các thao tác click của người dùng.
- Băng thông phân phối: Gửi kèm các file JS
.rscvà payload JSON làm tăng kích thước tải xuống thư mục tĩnh.
1.2 Giới Hạn Của RSC (React Server Components)
Next.js App Router cung cấp RSC nhằm giảm lượng JavaScript thiết lập phía Client. Tuy nhiên, hệ thống vẫn phải bắt buộc tải thư viện React Runtime và Client Router để phục vụ việc chuyển trang (Client-side Navigation) theo dòng SPA.
Đổi lại với Astro, trên các page văn bản, lượng JavaScript phân luồng tải xuống mặc định là 0 KB.
Phần 2: Astro Islands – Kiến Trúc Các UI Component Độc Lập
Khác với Next.js, triết lý xây dựng của Astro đặt trọng tâm vào khối “Zero JavaScript by Default”.
Astro Compiler sẽ render toàn thể phần React/Vue/Svelte layer sang cấu hình HTML tĩnh ở thời điểm Build. Khi gặp Component cần đảm nhận các User Interaction (tương tác người dùng), những module UI cụ thể này sẽ được khai báo rõ ràng để cô lập phạm vi tải JS – Astro định nghĩa cấu hình này là kiến trúc đảo (Islands Architecture).
Cấu trúc luồng tải được đối chiếu qua sơ đồ bên dưới:
graph TD
subgraph n1 ["Next.js SPA/RSC (Load Bundle JS)"]
N_Root[Root Component]
N_H[Header Nav]
N_M[Main Content]
N_P[Text Paragraph]
N_I[Image]
N_S[Search Bar]
N_Root --> N_H
N_Root --> N_M
N_M --> N_P
N_M --> N_I
N_H --> N_S
style N_Root fill:#ef4444,stroke:#991b1b,color:#fff
style N_H fill:#ef4444,stroke:#991b1b,color:#fff
style N_M fill:#ef4444,stroke:#991b1b,color:#fff
style N_P fill:#ef4444,stroke:#991b1b,color:#fff
style N_I fill:#ef4444,stroke:#991b1b,color:#fff
style N_S fill:#ef4444,stroke:#991b1b,color:#fff
end
subgraph n2 ["Astro Islands (Tĩnh tối đa)"]
A_Root[Astro Layout - Zero JS]
A_H[Astro Header - Zero JS]
A_M[Astro Content - Zero JS]
A_P[Markdown Text - Zero JS]
A_I[Astro Assets - Zero JS]
A_S[React Island - search.tsx]
A_Root --> A_H
A_Root --> A_M
A_M --> A_P
A_M --> A_I
A_H --> A_S
style A_Root fill:#10b981,stroke:#047857,color:#fff
style A_H fill:#10b981,stroke:#047857,color:#fff
style A_M fill:#10b981,stroke:#047857,color:#fff
style A_P fill:#10b981,stroke:#047857,color:#fff
style A_I fill:#10b981,stroke:#047857,color:#fff
style A_S fill:#3b82f6,stroke:#1d4ed8,color:#fff
end
Như sơ đồ mô tả: Trong khi Next.js mặc định render JS cho toàn bộ component tree, Astro chỉ gói gọn quá trình nạp JS vào một module cần thiết (ví dụ Search Bar).
Các cấu hình Hydration Directives trong Astro:
client:load: Tải và chạy JS tại thời điểm duyệt trang mở ra (Được dùng phổ biến cho phần Header Navbar hoặc tính năng Theme Mode).client:idle: Tải và gắn kết JS vào những lúc Thread phía Browser rảnh rỗi (Thường tối ưu cho các Tracking tags, Analytics scripts).client:visible: JS chỉ thực thi việc fetch khi vành đai Component lọt vào viewport xuất hiện (Sử dụng cho các chức năng Bình Luận cuối bài viết, Carousel thư viện Ảnh nhằm nâng cực điểm hiệu suất ban đầu).client:media="{max-width: 768px}": Chỉ tải phần module Interactive đối với những quy mô Device chuẩn bị Mobile view (Tối ưu để chặn file Hamburger Header chuyển đến Desktop UI).
Phần 3: Hướng Dẫn Chuyển Đổi Thực Tế (Migration Guide)
Việc di chuyển Framework đòi hỏi xây lại toàn bộ Data Flow và đường truyền Component. Dưới đây là các kỹ thuật thực tế để chuyển luồng code từ Next.js qua định dạng Astro.
3.1 Chuyển đổi Data Fetching: Loại Bỏ getStaticProps
Trong Astro, lập trình viên không phải setup bằng cách export một hàm Fetch async cố định ra ngoài chức năng render như Next.js. Mệnh lệnh Top-level Await có thể được triển khai trực tiếp vào khu vực Astro Component Script (nằm kẹp giữa cặp ---).
Mẫu Code Next.js trước đó (Dòng Pages Router):
export async function getStaticProps() {
const res = await fetch('https://api.github.com/repos/nicknguyen8/nicknguyen8.com');
const repo = await res.json();
return { props: { stars: repo.stargazers_count } };
}
export default function RepoInfo({ stars }) {
return <div>N8 Repo có {stars} sao</div>;
}
Mẫu Code Astro thay thế:
---
// src/pages/index.astro
// Toàn bộ dòng script nằm trong block `---` hoạt động chỉ ở Server-side Build.
// Đảm bảo không lộ thông tin logic API của dự án xuống phía Browser.
const res = await fetch('https://api.github.com/repos/nicknguyen8/nicknguyen8.com');
const repo = await res.json();
const stars = repo.stargazers_count;
---
<!-- Cấu trúc DOM thuần ngay dưới dùng JS biến môi trường -->
<div>N8 Repo có {stars} sao</div>
3.2 File-based Dynamic Routing
Next.js dùng [slug].tsx kết hợp getStaticPaths. Astro dùng [slug].astro với getStaticPaths() tương đương. Điểm khác biệt là data content được truyền trực tiếp qua props tại build time – không cần truy vấn thêm từ page component.
---
// src/pages/blog/[slug].astro
import { getCollection } from 'astro:content';
import BlogLayout from '../../layouts/BlogLayout.astro';
export async function getStaticPaths() {
const posts = await getCollection('insights');
return posts.map(post => ({
params: { slug: post.id },
props: { post }, // Data tĩnh được truyền thẳng vào build props
}));
}
const { post } = Astro.props;
const { Content, headings } = await post.render();
---
<BlogLayout title={post.data.title}>
<TableOfContents headings={headings} />
<article class="prose dark:prose-invert">
<Content />
</article>
</BlogLayout>
3.3 Tối Ưu Quản Lý Dữ Liệu Tĩnh Content Collections
Sự khó khăn khi duy trì Site dựa trên Markdown bằng Next.js là mảng quản trị Metadata files MD/MDX. Lập trình viên phụ thuộc các packages cộng đồng kiểu gray-matter, map File System Path và Setup typing TS bằng các quy chuẩn rườm rà.
Astro thế hệ sau hỗ trợ tính năng Content Collections cung cấp Validation dữ liệu chuẩn hóa hệ Type-Safe an toàn thông qua gói phân tích Zod.
File cấu hình ở src/content.config.ts (Astro 5):
import { z, defineCollection } from 'astro:content';
const insightsCollection = defineCollection({
type: 'content',
schema: ({ image }) => z.object({
title: z.string().max(100, "SEO Title vượt ngưỡng"),
description: z.string().min(50).max(160),
date: z.date(),
draft: z.boolean().default(false),
tags: z.array(z.string()).min(1),
coverImage: image().optional(),
}),
});
export const collections = {
'insights': insightsCollection,
};
Lưu ý: Nếu MDX Document ghi các dữ liệu Frontmatter bị nhầm Context Syntax (ví dụ sai giá trị Schema Date như
2026-03-XX), hệ điều hành Local môi trường Build Astro sẽ gửi trả báo lỗi Terminal Zod Validation và Cancel Command Build tức thời. Tính minh bạch này cô lập hoàn toàn các sai phạm cú pháp trước lúc lên CI/CD Production.
Phần 4: Vấn Đề State Management Trong Môi Trường Tĩnh Lập
Bài toán thiết lập Component Render với Data luân chuyển sẽ nảy sinh một rào cản: Tại sao khối code React Side Navigation có chức năng State Hook để báo đổi Theme Dark Mode, nhưng không thể cập nhật Real-time tới lớp <Header /> của dự án?
Đặc điểm Astro định dạng luồng trang sang các Islands cô lập độc quyền. Bộ Render Client bao quanh bằng vòng Context API Provider Next.js sẽ bị vỡ vụn khi Component tương tác bị gỡ khỏi Virtual DOM lớn.
Giải pháp chuẩn mực nhất dành riêng cho việc Share Data này là nền tảng Nano Stores. Một tập thư viện Framework-Agnostic chiếm tỷ lệ dung lượng siêu nhỏ dưới 1KB.
graph LR
NS[(Nano Store Instance)]
subgraph n3 ["Astro/React HTML DOM"]
R_IS[React Module - Theme Toggle]
H_IS[React Module - Header Navbar]
end
R_IS -- trigger state toggle() --> NS
NS -- lắng nghe và updates --> H_IS
style NS fill:#eab308,stroke:#a16207,color:#fff
style R_IS fill:#61dafb,stroke:#008b8b,color:#222
style H_IS fill:#61dafb,stroke:#008b8b,color:#222
Vì Store lưu trữ state bên ngoài vòng Render của React, mọi thay đổi được đồng bộ tự động sang các Islands liên quan mà không cần Provider dùng chung:
1. Khai Báo Biến Tồn Tại Bền Vững (Persistent Storage)
// src/store/themeStore.ts
import { persistentAtom } from '@nanostores/persistent';
export const theme = persistentAtom<'light' | 'dark'>('theme', 'dark');
2. Dựng Khối Client Island (Astro)
---
import Toggle from '../components/ThemeToggle.tsx';
---
<aside>
<!-- Mount React Node phía Browser -->
<Toggle client:load />
</aside>
3. Update File Tương Tác Cục Bộ
// src/components/ThemeToggle.tsx
import { useStore } from '@nanostores/react';
import { theme } from '../store/themeStore';
export default function ThemeToggle() {
const $theme = useStore(theme);
const toggle = () => {
theme.set($theme === 'light' ? 'dark' : 'light');
document.documentElement.classList.toggle('dark', $theme === 'light');
};
return (
<button onClick={toggle} className="p-2 bg-gray-200 dark:bg-gray-800 rounded">
{$theme === 'light' ? '🌙' : '☀️'}
</button>
);
}
Nano Stores giữ JS Bundle ở mức tối thiểu và hoạt động ổn định trên mọi framework frontend.
Phần 5: Tối Ưu Hình Ảnh Tại Build Time
Astro xử lý toàn bộ pipeline tối ưu hình ảnh tại build time thông qua astro:assets tích hợp sẵn, loại bỏ nhu cầu dùng dịch vụ on-demand như Cloudinary hay quota Image Optimization của Vercel.
Trong quá trình npm run build:
- Đọc các file ảnh gốc (JPEG/PNG kích thước gốc hàng MB).
- Nén và chuyển đổi sang
AVIFhoặcWebP. - Tạo
srcsetresponsive theo breakpoints cho từng kích thước thiết bị. - Tự động set
widthvàheight, loại bỏ Cumulative Layout Shift (CLS).
Toàn bộ assets được xuất vào /dist dưới dạng file tĩnh – CDN serve trực tiếp, không cần xử lý server-side mỗi request. Dung lượng ảnh thường giảm 70–80% so với file gốc.
Phần 6: View Transitions API
Kiến trúc MPA không đồng nghĩa với việc từ bỏ chuyển trang mượt mà. Astro tích hợp sẵn View Transitions API của trình duyệt, cho phép hiệu ứng fade và slide khi chuyển trang mà không cần Full Reload.
Chỉ cần thêm một import vào root layout là kích hoạt tính năng này trên toàn site:
---
// Import module tại Layout Main bao hệ thống
import { ViewTransitions } from 'astro:transitions';
---
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8" />
<title>Nick Nguyen</title>
<ViewTransitions />
</head>
<body>
<!-- Apply thuộc tính UI Animations -->
<main transition:animate="fade">
<slot />
</main>
</body>
</html>
Khi chuyển trang, trình duyệt chụp snapshot trang hiện tại, fetch HTML tiếp theo, rồi thực hiện DOM diff với hiệu ứng transition. JavaScript tiêu tốn chỉ vài KB cho logic chuyển trang, không hơn.
Phần 7: Benchmark SEO và Performance
Dữ liệu Lighthouse CI thu thập từ domain production nicknguyen8.com:
| Các Đo Lường Performance | Mô Hình Kế Thừa Next.js (SSG) | Cấu Trúc Khối Astro (Static + CDN) | Kết Quả Tối Ưu |
|---|---|---|---|
| JavaScript Payload tải lần đầu | ~215 KB | 12 KB (Modules React Island) | Đã lược bỏ tối đa các khối JS thừa |
| First Contentful Paint (FCP) | 1.2s | 0.1s | Tăng cường khả năng vẽ UI nhanh nhất |
| Largest Contentful Paint (LCP) | 1.8s (Mất time Fetch Bundle) | 0.4s (HTML và Images Load Tức Khắc) | Cải thiện mạnh chuẩn Web Vitals |
| Total Blocking Time (TBT) | ~300ms (Trình duyệt thực thi Hook) | 0 ms (Main-Thread thông rảnh) | Thiết bị vận hành tác vụ On-Click Tức thì |
| Lighthouse Performance Score | 85-92 (Ảnh hưởng do Network) | 100 / 100 / 100 / 100 | Điểm đạt đỉnh ở 4 mảng Audit |
Lợi ích thực tế của điểm 100/100:
- Googlebot Crawl Budget: Dung lượng trang nhỏ giúp Googlebot index bài viết mới nhanh hơn đáng kể.
- Page Experience Signals: Core Web Vitals cao cải thiện trực tiếp thứ hạng Google cho các trang nội dung.
- Chi phí băng thông: Request đến Cloudflare CDN được tối giản. Traffic tăng không làm tăng chi phí hosting theo tỷ lệ tương ứng.
Phần 8: Giới Hạn Của Kiến Trúc Tĩnh Astro Component
Mọi framework đều có ranh giới.
- Quản lý Global SPA State: Các ứng dụng cần state liên tục khi chuyển trang (dashboard tài chính, media player) sẽ gặp rào cản với mô hình MPA isolated của Astro. React Context và Redux hoạt động tốt hơn trong kiến trúc SPA đúng nghĩa như Next.js hay React Router.
- Learning Curve với file
.astro: Format.astrotách biệt server-side script scope khỏi client-side DOM. Kỹ sư từ nền JSX thuần cần thời gian để quen với cách variable scope, event handler, và component lifecycle khác với React chuẩn.
Lời Kết
Migration từ Next.js sang Astro phản ánh xu hướng kiến trúc chung: delivery tinh gọn hơn, HTML tĩnh theo mặc định, và JavaScript chỉ xuất hiện khi thực sự cần.
Nguyên tắc lựa chọn công nghệ:
- Admin Dashboard tương tác cao, real-time data feed, hoặc phát media liên tục khi chuyển trang → SPA frameworks (Next.js, React Router/Remix) là lựa chọn đúng.
- Blog kỹ thuật, portfolio, documentation, và các trang nội dung cần Lighthouse cao và LCP tốt → Astro hiện là framework phù hợp nhất cho use case này.
Ràng buộc của Astro chính là điểm mạnh của nó: buộc phải quyết định tường minh cái gì thực sự cần JavaScript, thay vì hydrate mọi thứ theo mặc định.