Trong Phần 1, mình đã xây dựng nền tảng cơ bản cho hệ thống comment: database, API và UI. Hệ thống hiện tại đã hoạt động tốt, nhưng chưa thực sự tối ưu ở phần hiệu năng hoạt động.
Trong Phần 2 hôm nay, mình sẽ chia sẻ cách tối ưu hệ thống comment bằng hai kỹ thuật:
- Cloudflare KV — cache lại kết quả truy vấn D1 để giảm số lần truy vấn vào database
- D1 Composite Index — tăng tốc truy vấn khi cache bị miss
I. Vấn đề hiệu năng hiện tại
Theo thiết lập hiện tại, mỗi lần bạn đọc truy cập vào một bài viết, và kéo đến phần bình luận dưới cuối bài, hệ thống sẽ thực hiện một API request:
GET /api/comments/{slug}Request này sẽ truy vấn trực tiếp vào D1 để lấy toàn bộ comment của bài viết hiện tại
SELECT id, author_name, author_url, content, createdAt, parent_idFROM commentsWHERE post_slug = 'ten-bai-viet' AND status = 1ORDER BY created_at DESCGiả sử bài viết có 100 comment và một ngày có 10000 lượt xem, thì D1 sẽ phải xử lý 10000 query, mỗi query trả về 100 dòng dữ liệu. Tổng cộng là 1 triệu row reads/ngày (chiếm ~20% quota miễn phí của D1).
Điều quan trọng là: 10000 query này đều trả về cùng một kết quả — danh sách comment không thay đổi giữa các lần truy vấn.
Chúng ta không cần truy vấn database lặp đi lặp lại như vậy. Thay vào đó có thể tối ưu bằng cách cache kết quả ở lần truy vấn đầu tiên, các lần sau chỉ cần đọc từ cache. Vừa nhanh hơn vì không phải truy vấn vào database vừa không tốn quote row read của D1.
Cache chỉ cần làm mới khi có thay đổi (ví dụ: admin duyệt, xóa, trả lời comment). Nhờ đó, giảm tải cho database và tăng tốc độ phản hồi cho người dùng.
Giới hạn miễn phí của D1: Cloudflare D1 cung cấp 5 triệu row reads miễn phí mỗi ngày. Với đa số blog cá nhân, con số này là hoàn toàn dư dả. Nhưng nếu blog phát triển nhanh với nhiều bài viết và nhiều comment, quota miễn phí này sẽ nhanh chóng bị cạn kiệt nếu không được tối ưu.
II. Giải pháp: sư dụng Cloudflare KV Cache
Cloudflare KV là một hệ thống lưu trữ key-value toàn cầu, cực kỳ nhanh, được phân tán tới hàng trăm edge server trên thế giới. Đọc dữ liệu từ KV chỉ mất khoảng 1-2ms, nhanh hơn rất nhiều so với truy vấn D1 (10-20ms) và không tốn quota database.
Quy trình hoạt động sau khi áp dụng cache:
Bạn đọc truy cập bài viết → Trình duyệt gửi request GET /api/comments/ten-bai-viet → Kiểm tra KV có cache không? → Nếu CÓ: trả về ngay, không cần query D1 → Nếu KHÔNG: query D1, lưu kết quả vào KV, trả về lại trình duyệtCache sẽ tồn tại cho đến khi có thay đổi liên quan đến comment (ví dụ: admin duyệt, xóa, trả lời comment). Khi đó, cache sẽ bị xóa và lần truy cập tiếp theo sẽ tự động tạo lại cache mới.
Giới hạn miễn phí của Cloudflare KV
| Loại thao tác | Giới hạn miễn phí mỗi ngày |
|---|---|
| Đọc (Reads) | 100,000 |
| Ghi (Writes) | 1,000 |
| Xóa (Deletes) | 1,000 |
| Dung lượng | 1 GB |
Với blog cá nhân, các giới hạn này là quá dư dả. Việc ghi/xóa KV chỉ diễn ra khi có comment mới được duyệt, xóa hoặc trả lời.
III. Tối ưu thêm: Tạo index cho bảng comments
Bên cạnh việc cache, mình tạo thêm index tổng hợp (composite index) cho bảng comments để tăng tốc độ truy vấn khi cache bị miss.
-- Index for API public: filter by post_slugCREATE INDEX idx_comments_slug_status_created ON comments (post_slug, status, created_at);IV. Cài đặt Cloudflare KV
1. Tạo KV Namespace trên Cloudflare
Chạy lệnh sau để tạo namespace KV mới:
pnpm wrangler kv namespace create blog-thuanbui-comment-cacheSau khi chạy, bạn sẽ nhận được ID của namespace. Hãy lưu lại để dùng ở bước tiếp theo.
2. Thêm KV namespace vào file cấu hình wrangler.jsonc
{ "d1_databases": [ { "binding": "COMMENTS_DB", "database_name": "blog-thuanbui-comments", "database_id": "7ffe7eee-9d2b-4958-befd-2404d710d903", }, ], "kv_namespaces": [ { "binding": "COMMENTS_CACHE", "id": "ae53fae9a47a4054811ffb98f6f34656", // ID từ bước 1 }, ],}Lưu ý:
bindinglà tên dùng trong code (env.COMMENTS_CACHE).idlà ID thực trên Cloudflare. Tên namespace (blog-thuanbui-comment-cache) chỉ để dễ nhận diện trên dashboard.
3. Generate types từ wrangler config
pnpm wrangler typesLệnh này sẽ cập nhật file worker-configuration.d.ts ở root project — cần thiết để Cloudflare Workers build thành công. Nếu file chưa có COMMENTS_CACHE trong Env, hãy thêm thủ công:
declare namespace Cloudflare { interface Env { COMMENTS_DB: D1Database; COMMENTS_CACHE: KVNamespace; // thêm dòng này }}4. Thêm index vào schema
Cập nhật src/db/schema.ts — thêm composite index cho bảng comments:
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
export const comments = sqliteTable( "comments", { // ... existing columns ... }, table => [ index("idx_comments_slug_status_created").on( table.postSlug, table.status, table.createdAt ), ]);Chạy lệnh generate để tạo file migration mới:
pnpm db:generateLệnh này sẽ tạo ra file migration mới trong thư mục migrations/ với index tương ứng.
CREATE INDEX `idx_comments_slug_status_created` ON `comments` (`post_slug`, `status`, `created_at`);V. Cập nhật Comment API
1. Thêm logic cache vào API GET comment
Mở file src/pages/api/comments/[slug].ts và cập nhật như sau:
// ─── GET: Fetch comments for a post ───────────────────────────────────────export async function GET({ params }: APIContext) { const { slug } = params; if (!slug) { return new Response(JSON.stringify({ error: "Missing slug" }), { status: 400, headers: { "Content-Type": "application/json" }, }); }
const cacheKey = `comments:${slug}`;
// Check KV cache first — permanent until invalidated on approve/delete try { const cached = await env.COMMENTS_CACHE.get(cacheKey); if (cached) { return new Response(cached, { status: 200, headers: { "Content-Type": "application/json", "X-Cache": "HIT", }, }); } } catch { // KV unavailable (e.g. local dev without binding) — fall through to D1 }
try { const db = drizzle(env.COMMENTS_DB, { schema: { comments } }); const result = await db .select({ id, authorName, authorUrl, content, createdAt, parentId }) .from(comments) .where(and(eq(comments.postSlug, slug), eq(comments.status, 1))) .orderBy(desc(comments.createdAt));
return new Response(JSON.stringify({ comments: result }), { status: 200, }); const body = JSON.stringify({ comments: result });
await env.COMMENTS_CACHE.put(cacheKey, body).catch(() => {});
return new Response(body, { status: 200, headers: { "Content-Type": "application/json", "X-Cache": "MISS", }, }); } catch (error) { return new Response(JSON.stringify({ error: "Database error" }), { status: 500, }); }}Giải thích nhanh:
- Dòng 11–27: Kiểm tra KV cache → nếu cache tồn tại, trả về kết quả ngay, không truy vấn vào database.
- Dòng 40–50: Nếu cache khôgn tồn tại → truy vấn vào database → lưu vào KV → trả về kết quả.
- Bổ sung thêm header
X-Cachetrong cả hai trường hợp dùng để debug.
2. Xóa cache khi có thay đổi comment
Trong endpoint POST — khi email nằm trong danh sách đã được duyệt trước đó (trusted emails), comment sẽ được tự động được duyệt, cần xóa cache ngay để comment mới hiển thị ngay lập tức mà không cần refresh:
Trong src/pages/api/comments/[slug].ts, thêm vào sau phần insert comment:
// ── Insert comment ───────────────────────────────────────────────────await db .insert(comments) .values({ postSlug: slug, // ... }) .returning();
if (isTrusted) { await env.COMMENTS_CACHE.delete(`comments:${slug}`).catch(() => {});}Ngoài ra, bất kỳ API endpoint nào làm thay đổi danh sách comment hiển thị (duyệt, xóa, trả lời) đều cần xóa cache tương ứng:
// Sau khi thay đổi comment của một bài viếtawait env.COMMENTS_CACHE.delete(`comments:${postSlug}`);Phần này sẽ được áp dụng khi thiết lập trang dashboard để duyệt / xoá / trả lời comment.
VI. Cập nhật database và kiểm tra trên local
1. Cập nhật database
pnpm db:migrate:local2. Kiểm tra index đã tạo thành công
Chạy lẹện sau dể kiểm tra index
pnpm wrangler d1 execute blog-thuanbui-comments \ --local \ --command="SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='comments'"Kết quả hiện ra như bên dưới là ổn
┌──────────────────────────────────────┐│ name │├──────────────────────────────────────┤│ idx_comments_slug_status_created │└──────────────────────────────────────┘3. Khởi động dev server
pnpm run dev4. Kiểm tra trong trình duyệt
- Mở một bài viết có comment, ví dụ:
http://localhost:4321/cai-dat-wyze-bridge/ - Mở DevTools → tab Network
- Filter theo
api/comments - Click vào request, trong phần Header, kéo xuống phần Response Headers
- Kiểm tra header
x-cache:
| Lần truy cập | x-cache | Ý nghĩa |
|---|---|---|
| Lần đầu tiên | MISS | Truy vấn D1, lưu vào KV |
| Từ lần 2 (F5) | HIT | Đọc từ KV, không truy vấn D1 |
5. Kiểm tra KV bằng dòng lệnh
# Xem danh sách keyspnpm wrangler kv key list --local --binding=COMMENTS_CACHE
# Xem nội dung một keypnpm wrangler kv key get "comments:cai-dat-wyze-bridge" \ --local --binding=COMMENTS_CACHEVII. Áp dụng lên production
Sau khi thử nghiệm thành công trên local, mình sẽ áp dụng lên production (Cloudflare KV, D1)
1. Apply migration cho database
pnpm db:migrate:prodTiêp theo, commit code và push lên GitHub, Cloudflare Worker sẽ tự động build và deploy.
2. Kiểm tra cache trên production
Tương tự như local — mở DevTools trên production site, kiểm tra x-cache header trên request /api/comments/....
Ngoài ra có thể xem KV keys trên Cloudflare Dashboard:
Storage & databases → Workers KV → Chọn blog-thuanbui-comment-cache → KV Pairs
VIII. Tổng kết
Trong phần này, mình đã chia sẻ cách tối ưu hệ thống comment bằng hai kỹ thuật:
- Cloudflare KV — cache kết quả truy vấn D1, giảm số lần truy vấn database xuống gần như bằng 0
- D1 Composite Index — tăng tốc truy vấn khi cache bị miss
Nhờ áp dụng cache, hiệu năng hoạt động của blog đã đuợc cải thiện đáng kể
- Giảm số lần truy vấn database xuống gần như bằng 0 (chỉ query khi cache miss hoặc có thay đổi comment)
- Tăng tốc độ phản hồi cho người đọc (latency chỉ ~1ms từ KV)
- Tiết kiệm tài nguyên, không lo vượt quota miễn phí của D1
Cache sẽ tồn tại cho đến khi có thay đổi comment, đảm bảo danh sách luôn được cập nhật mới mà vẫn tối ưu hiệu năng.
Mình áp dụng comment cache cho blog từ ngày 10/04, có thể thấy ngay sau đó số lượng query read đã giảm hẳn so với trước đó

Trong Phần 3, mình sẽ chia sẻ cách xây dựng Admin Dashboard để quản lý comment (duyệt / xoá / trả lời). Hẹn gặp lại bạn trong phần tiếp theo!