Đi đến nội dung
·18 phút đọc

Edge Functions: Là Gì, Khi Nào Dùng, và Khi Nào Không Nên

Edge runtime, V8 isolates, huyền thoại cold start, geo-routing, A/B testing, auth tại edge, và tại sao tôi chuyển một số thứ về Node.js. Cái nhìn cân bằng về edge computing.

Chia sẻ:X / TwitterLinkedIn

Từ "edge" được ném ra rất nhiều. Vercel nói vậy. Cloudflare nói vậy. Deno nói vậy. Mỗi bài nói chuyện về hiệu năng web đều tất yếu nhắc đến "chạy tại edge" như thể đó là câu thần chú làm ứng dụng nhanh lên.

Tôi đã tin vào điều đó. Tôi chuyển middleware, API route, thậm chí một số logic render sang edge runtime. Một số bước đi đó rất tuyệt. Số khác tôi lặng lẽ chuyển về Node.js ba tuần sau khi debug lỗi connection pool lúc 2 giờ sáng.

Bài viết này là phiên bản cân bằng của câu chuyện đó — edge thực sự là gì, nó thực sự tỏa sáng ở đâu, nó hoàn toàn không phù hợp ở đâu, và cách tôi quyết định dùng runtime nào cho mỗi phần của ứng dụng.

Edge Là Gì?#

Hãy bắt đầu với địa lý. Khi ai đó truy cập website của bạn, request di chuyển từ thiết bị, qua ISP, xuyên internet đến server, được xử lý, và response quay ngược lại. Nếu server ở us-east-1 (Virginia) và người dùng ở Tokyo, chuyến đi khứ hồi đó khoảng 14.000 km. Ở tốc độ ánh sáng qua cáp quang, đó khoảng 70ms chỉ riêng vật lý — một chiều. Thêm DNS resolution, TLS handshake, và thời gian xử lý, bạn dễ dàng nhìn vào 200-400ms trước khi người dùng thấy một byte duy nhất.

"Edge" nghĩa là chạy code trên server phân tán toàn cầu — cùng những node CDN vốn đã phục vụ static asset, nhưng giờ chúng cũng có thể thực thi logic. Thay vì một origin server ở Virginia, code chạy tại 300+ vị trí trên thế giới. Người dùng ở Tokyo truy cập server ở Tokyo. Người dùng ở Paris truy cập server ở Paris.

Phép tính độ trễ đơn giản và thuyết phục:

Truyền thống (single origin):
  Tokyo → Virginia: ~140ms khứ hồi (chỉ riêng vật lý)
  + TLS handshake: ~140ms nữa (thêm một chuyến khứ hồi)
  + Xử lý: 20-50ms
  Tổng: ~300-330ms

Edge (PoP local):
  Tokyo → Tokyo edge node: ~5ms khứ hồi
  + TLS handshake: ~5ms nữa
  + Xử lý: 5-20ms
  Tổng: ~15-30ms

Đó là cải thiện 10-20 lần cho response ban đầu. Nó có thật, đo được, và cho một số operation nó mang tính biến đổi.

Nhưng đây là điều marketing lướt qua: edge không phải là môi trường server đầy đủ. Nó là thứ hoàn toàn khác.

V8 Isolates vs Node.js#

Node.js truyền thống chạy trong một tiến trình hệ điều hành đầy đủ. Nó truy cập được filesystem, mở kết nối TCP, spawn child process, đọc biến môi trường dạng stream, về cơ bản làm được mọi thứ một tiến trình Linux có thể làm.

Edge function không chạy trên Node.js. Chúng chạy trên V8 isolate — cùng engine JavaScript chạy Chrome, nhưng được tước xuống lõi. Hãy nghĩ V8 isolate như sandbox nhẹ:

typescript
// Hoạt động trong Node.js nhưng KHÔNG ở edge
import fs from "fs";
import { createConnection } from "net";
import { execSync } from "child_process";
 
const file = fs.readFileSync("/etc/hosts");        // ❌ Không có filesystem
const conn = createConnection({ port: 5432 });     // ❌ Không có raw TCP
const result = execSync("ls -la");                  // ❌ Không có child process
process.env.DATABASE_URL;                           // ⚠️  Có sẵn nhưng tĩnh, được đặt lúc deploy

Những gì bạn CÓ ở edge là bề mặt Web API — cùng API có sẵn trong trình duyệt:

typescript
// Tất cả đều hoạt động ở edge
const response = await fetch("https://api.example.com/data");
const url = new URL(request.url);
const headers = new Headers({ "Content-Type": "application/json" });
const encoder = new TextEncoder();
const encoded = encoder.encode("hello");
const hash = await crypto.subtle.digest("SHA-256", encoded);
const id = crypto.randomUUID();
 
// Web Streams API
const stream = new ReadableStream({
  start(controller) {
    controller.enqueue("chunk 1");
    controller.enqueue("chunk 2");
    controller.close();
  },
});
 
// Cache API
const cache = caches.default;
await cache.put(request, response.clone());

Các ràng buộc là thực và khắc nghiệt:

  • Bộ nhớ: 128MB mỗi isolate (Cloudflare Workers), 256MB trên một số nền tảng
  • Thời gian CPU: 10-50ms thời gian CPU thực (không phải wall clock time — await fetch() không tính, nhưng JSON.parse() trên payload 5MB thì có)
  • Không có native module: Bất cứ gì cần C++ binding (bcrypt, sharp, canvas) đều không được
  • Không có kết nối bền vững: Bạn không thể giữ kết nối database mở giữa các request
  • Giới hạn kích thước bundle: Thường 1-5MB cho toàn bộ worker script

Đây không phải Node.js trên CDN. Đây là runtime khác với mô hình tư duy khác.

Cold Start: Huyền Thoại và Thực Tế#

Bạn có lẽ đã nghe rằng edge function có "zero cold start." Điều này... gần như đúng, và sự so sánh thực sự ấn tượng.

Hàm serverless truyền thống dựa trên container (AWS Lambda, Google Cloud Functions) hoạt động thế này:

  1. Request đến
  2. Nền tảng cấp phát container (nếu không có sẵn)
  3. Container boot OS
  4. Runtime khởi tạo (Node.js, Python, v.v.)
  5. Code tải và khởi tạo
  6. Request được xử lý

Bước 2-5 là cold start. Cho Node.js Lambda, thường 200-500ms. Cho Java Lambda, có thể 2-5 giây. Cho .NET Lambda, 500ms-1.5s.

V8 isolate hoạt động khác:

  1. Request đến
  2. Nền tảng tạo V8 isolate mới (hoặc dùng lại cái đang warm)
  3. Code tải (đã được biên dịch thành bytecode lúc deploy)
  4. Request được xử lý

Bước 2-3 mất dưới 5ms. Thường dưới 1ms. Isolate không phải container — không có OS để boot, không có runtime để khởi tạo. V8 tạo isolate mới trong vài microsecond. Cụm từ "zero cold start" là ngôn ngữ marketing, nhưng thực tế (khởi động dưới 5ms) đủ gần zero để không quan trọng cho hầu hết use case.

Nhưng đây là khi cold start vẫn cắn bạn ở edge:

Bundle lớn. Nếu edge function kéo 2MB dependencies, code đó vẫn cần được tải và parse. Tôi học bài này khó khăn khi bundle thư viện validation và thư viện format ngày vào edge middleware. Cold start từ 2ms lên 40ms. Vẫn nhanh, nhưng không phải "zero."

Vị trí hiếm. Các nhà cung cấp edge có hàng trăm PoP, nhưng không phải tất cả PoP đều giữ code warm. Nếu bạn nhận một request mỗi giờ từ Nairobi, isolate đó bị tái chế giữa các request. Request tiếp theo trả chi phí khởi động lại.

Nhiều isolate mỗi request. Nếu edge function gọi edge function khác (hoặc middleware và API route đều ở edge), bạn có thể spin up nhiều isolate cho một request người dùng.

Lời khuyên thực tế: giữ bundle edge function nhỏ. Chỉ import những gì cần. Tree-shake mạnh tay. Bundle càng nhỏ, cold start càng nhanh, lời hứa "zero cold start" càng đúng.

typescript
// ❌ Đừng làm thế này ở edge
import dayjs from "dayjs";
import * as yup from "yup";
import lodash from "lodash";
 
// ✅ Thay vào đó — dùng API tích hợp
const date = new Date().toISOString();
const isValid = typeof input === "string" && input.length < 200;
const unique = [...new Set(items)];

Use Case Hoàn Hảo Cho Edge Function#

Sau khi thử nghiệm rộng rãi, tôi tìm thấy pattern rõ ràng: edge function xuất sắc khi bạn cần đưa ra quyết định nhanh về request trước khi nó đến origin server. Chúng là người gác cổng, bộ định tuyến, và bộ biến đổi — không phải application server.

1. Chuyển Hướng Dựa Trên Vị Trí Địa Lý#

Đây là use case sát thủ. Request đến edge node gần nhất, đã biết người dùng ở đâu. Không cần gọi API, không cần tra cứu IP — nền tảng cung cấp dữ liệu geo:

typescript
// middleware.ts — chạy ở edge trên mọi request
import { NextRequest, NextResponse } from "next/server";
 
export const config = {
  matcher: ["/", "/shop/:path*"],
};
 
export function middleware(request: NextRequest) {
  const country = request.geo?.country ?? "US";
  const city = request.geo?.city ?? "Unknown";
  const region = request.geo?.region ?? "Unknown";
 
  // Chuyển hướng đến cửa hàng theo quốc gia
  if (request.nextUrl.pathname === "/shop") {
    const storeMap: Record<string, string> = {
      DE: "/shop/eu",
      FR: "/shop/eu",
      GB: "/shop/uk",
      JP: "/shop/jp",
      TR: "/shop/tr",
    };
 
    const storePath = storeMap[country] ?? "/shop/us";
    if (request.nextUrl.pathname !== storePath) {
      return NextResponse.redirect(new URL(storePath, request.url));
    }
  }
 
  // Thêm geo header cho downstream sử dụng
  const response = NextResponse.next();
  response.headers.set("x-user-country", country);
  response.headers.set("x-user-city", city);
  response.headers.set("x-user-region", region);
  return response;
}

Chạy dưới 5ms, ngay bên cạnh người dùng. Phương án thay thế — gửi request suốt đến origin server chỉ để tra cứu IP rồi redirect ngược lại — tốn 100-300ms cho người dùng xa origin.

2. A/B Testing Không Nhấp Nháy#

A/B testing phía client gây ra "flash of original content" đáng sợ — người dùng thấy phiên bản A trong tích tắc trước khi JavaScript hoán đổi phiên bản B. Ở edge, bạn có thể gán variant trước khi trang bắt đầu render:

typescript
import { NextRequest, NextResponse } from "next/server";
 
export function middleware(request: NextRequest) {
  // Kiểm tra xem người dùng đã có variant chưa
  const existingVariant = request.cookies.get("ab-variant")?.value;
 
  if (existingVariant) {
    // Rewrite đến trang variant đúng
    const url = request.nextUrl.clone();
    url.pathname = `/variants/${existingVariant}${url.pathname}`;
    return NextResponse.rewrite(url);
  }
 
  // Gán variant mới (chia 50/50)
  const variant = Math.random() < 0.5 ? "control" : "treatment";
 
  const url = request.nextUrl.clone();
  url.pathname = `/variants/${variant}${url.pathname}`;
 
  const response = NextResponse.rewrite(url);
  response.cookies.set("ab-variant", variant, {
    maxAge: 60 * 60 * 24 * 30, // 30 ngày
    httpOnly: true,
    sameSite: "lax",
  });
 
  return response;
}

Người dùng không bao giờ thấy nhấp nháy vì rewrite xảy ra ở tầng mạng. Trình duyệt không biết đó là A/B test — nó chỉ nhận trang variant trực tiếp.

3. Xác Minh Auth Token#

Nếu auth dùng JWT (và bạn không tra cứu session từ database), edge là hoàn hảo. Xác minh JWT là crypto thuần túy — không cần database:

typescript
import { jwtVerify, importSPKI } from "jose";
import { NextRequest, NextResponse } from "next/server";
 
const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----`;
 
export async function middleware(request: NextRequest) {
  const token = request.cookies.get("session-token")?.value;
 
  if (!token) {
    return NextResponse.redirect(new URL("/login", request.url));
  }
 
  try {
    const key = await importSPKI(PUBLIC_KEY, "RS256");
    const { payload } = await jwtVerify(token, key, {
      algorithms: ["RS256"],
      issuer: "https://auth.example.com",
    });
 
    // Truyền thông tin người dùng downstream qua header
    const response = NextResponse.next();
    response.headers.set("x-user-id", payload.sub as string);
    response.headers.set("x-user-role", payload.role as string);
    return response;
  } catch {
    // Token hết hạn hoặc không hợp lệ
    const response = NextResponse.redirect(new URL("/login", request.url));
    response.cookies.delete("session-token");
    return response;
  }
}

Pattern này mạnh mẽ: edge middleware xác minh token và truyền thông tin người dùng đến origin dưới dạng header đáng tin cậy. API route không cần xác minh token lại — chỉ cần đọc request.headers.get("x-user-id").

4. Phát Hiện Bot và Rate Limiting#

Edge function có thể chặn traffic không mong muốn trước khi nó đến origin:

typescript
import { NextRequest, NextResponse } from "next/server";
 
// Rate limiter in-memory đơn giản (theo edge location)
const rateLimitMap = new Map<string, { count: number; timestamp: number }>();
 
export function middleware(request: NextRequest) {
  const ip = request.headers.get("x-forwarded-for")?.split(",").pop()?.trim()
    ?? "unknown";
  const ua = request.headers.get("user-agent") ?? "";
 
  // Chặn bot xấu đã biết
  const badBots = ["AhrefsBot", "SemrushBot", "MJ12bot", "DotBot"];
  if (badBots.some((bot) => ua.includes(bot))) {
    return new NextResponse("Forbidden", { status: 403 });
  }
 
  // Rate limiting đơn giản
  const now = Date.now();
  const windowMs = 60_000; // 1 phút
  const maxRequests = 100;
 
  const entry = rateLimitMap.get(ip);
  if (entry && now - entry.timestamp < windowMs) {
    entry.count++;
    if (entry.count > maxRequests) {
      return new NextResponse("Too Many Requests", {
        status: 429,
        headers: { "Retry-After": "60" },
      });
    }
  } else {
    rateLimitMap.set(ip, { count: 1, timestamp: now });
  }
 
  // Dọn dẹp định kỳ để ngăn memory leak
  if (rateLimitMap.size > 10_000) {
    const cutoff = now - windowMs;
    for (const [key, val] of rateLimitMap) {
      if (val.timestamp < cutoff) rateLimitMap.delete(key);
    }
  }
 
  return NextResponse.next();
}

Lưu ý: rate limit map ở trên là theo isolate, theo location. Nếu bạn có 300 edge location, mỗi cái có map riêng. Cho rate limiting nghiêm ngặt, bạn cần distributed store như Upstash Redis hoặc Cloudflare Durable Objects. Nhưng cho ngăn chặn lạm dụng sơ bộ, giới hạn theo location hoạt động tốt đáng ngạc nhiên.

5. Rewrite Request và Header Cá Nhân Hóa#

Edge function xuất sắc trong việc biến đổi request trước khi đến origin:

typescript
import { NextRequest, NextResponse } from "next/server";
 
export function middleware(request: NextRequest) {
  const response = NextResponse.next();
  const url = request.nextUrl;
 
  // Content negotiation dựa trên thiết bị
  const ua = request.headers.get("user-agent") ?? "";
  const isMobile = /mobile|android|iphone/i.test(ua);
  response.headers.set("x-device-type", isMobile ? "mobile" : "desktop");
 
  // Feature flag từ cookie
  const flags = request.cookies.get("feature-flags")?.value;
  if (flags) {
    response.headers.set("x-feature-flags", flags);
  }
 
  // Phát hiện locale cho i18n
  const acceptLanguage = request.headers.get("accept-language") ?? "en";
  const preferredLocale = acceptLanguage.split(",")[0]?.split("-")[0] ?? "en";
  const supportedLocales = [
    "en", "tr", "de", "fr", "es", "pt", "ja", "ko", "it",
    "nl", "ru", "pl", "uk", "sv", "cs", "ar", "hi", "zh",
  ];
  const locale = supportedLocales.includes(preferredLocale)
    ? preferredLocale
    : "en";
 
  if (!url.pathname.startsWith(`/${locale}`) && !url.pathname.startsWith("/api")) {
    return NextResponse.redirect(new URL(`/${locale}${url.pathname}`, request.url));
  }
 
  return response;
}

Nơi Edge Thất Bại#

Đây là phần mà các trang marketing bỏ qua. Tôi đã đụng mọi bức tường này.

1. Kết Nối Database#

Đây là vấn đề lớn nhất. Database truyền thống (PostgreSQL, MySQL) dùng kết nối TCP bền vững. Server Node.js mở connection pool lúc khởi động và tái sử dụng các kết nối đó qua các request. Hiệu quả, đã chứng minh, được hiểu rõ.

Edge function không làm được điều này. Mỗi isolate là tạm thời. Không có giai đoạn "khởi động" để mở kết nối. Ngay cả nếu bạn có thể mở kết nối, isolate có thể bị tái chế sau một request, lãng phí thời gian thiết lập kết nối.

typescript
// ❌ Pattern này về cơ bản không hoạt động ở edge
import { Pool } from "pg";
 
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10, // Connection pool 10
});
 
// Mỗi lần gọi edge sẽ:
// 1. Tạo Pool mới (không thể tái sử dụng giữa các lần gọi một cách đáng tin cậy)
// 2. Mở kết nối TCP đến database (ở us-east-1, không phải ở edge)
// 3. Thực hiện TLS handshake với database
// 4. Chạy query
// 5. Hủy kết nối khi isolate bị tái chế
 
// Ngay cả với service connection pooling như PgBouncer,
// bạn vẫn trả chi phí network latency từ edge → origin database

Vấn đề khứ hồi database là nền tảng. Database ở một region. Edge function ở 300 region. Mỗi database query từ edge phải đi từ edge location đến database region và quay lại. Cho người dùng ở Tokyo truy cập Tokyo edge node, nhưng database ở Virginia:

Edge function ở Tokyo
  → Query đến PostgreSQL ở Virginia: ~140ms khứ hồi
  → Query thứ hai: ~140ms nữa
  → Tổng: 280ms chỉ cho hai query

Node.js function ở Virginia (cùng region với DB)
  → Query đến PostgreSQL: ~1ms khứ hồi
  → Query thứ hai: ~1ms nữa
  → Tổng: 2ms cho hai query

Edge function chậm hơn 140 lần cho database operation trong kịch bản này. Không quan trọng edge function khởi động nhanh hơn — database khứ hồi chi phối mọi thứ.

Đó là lý do HTTP-based database proxy tồn tại (serverless driver của Neon, fetch-based driver của PlanetScale, REST API của Supabase). Chúng hoạt động, nhưng vẫn gửi HTTP request đến database ở một region duy nhất. Chúng giải quyết vấn đề "không dùng được TCP" nhưng không phải vấn đề "database ở xa".

typescript
// ✅ Hoạt động ở edge (truy cập database qua HTTP)
// Nhưng vẫn chậm nếu database xa edge node
import { neon } from "@neondatabase/serverless";
 
export const runtime = "edge";
 
export async function GET(request: Request) {
  const sql = neon(process.env.DATABASE_URL!);
  // Gửi HTTP request đến Neon database
  // Hoạt động, nhưng latency phụ thuộc khoảng cách đến database region
  const posts = await sql`SELECT * FROM posts WHERE published = true LIMIT 10`;
  return Response.json(posts);
}

2. Task Chạy Lâu#

Edge function có giới hạn CPU time, thường 10-50ms thời gian compute thực. Wall clock time rộng rãi hơn (thường 30 giây), nhưng operation nặng CPU sẽ chạm giới hạn nhanh:

typescript
// ❌ Những thứ này sẽ vượt giới hạn CPU time ở edge
export const runtime = "edge";
 
export async function POST(request: Request) {
  const data = await request.json();
 
  // Xử lý hình ảnh — nặng CPU
  // (Cũng không dùng được sharp vì là native module)
  const processed = heavyImageProcessing(data.image);
 
  // Tạo PDF — nặng CPU + cần Node.js API
  const pdf = generatePDF(data.content);
 
  // Biến đổi dữ liệu lớn
  const result = data.items // 100.000 items
    .map(transform)
    .filter(validate)
    .sort(compare)
    .reduce(aggregate, {});
 
  return Response.json(result);
}

Nếu function cần hơn vài millisecond CPU time, nó thuộc về server Node.js regional. Chấm hết.

3. Dependencies Chỉ Dành Cho Node.js#

Điều này bắt nhiều người bất ngờ. Số lượng đáng ngạc nhiên các npm package phụ thuộc vào module tích hợp Node.js:

typescript
// ❌ Các package này không hoạt động ở edge
import bcrypt from "bcrypt";            // Native C++ binding
import sharp from "sharp";              // Native C++ binding
import puppeteer from "puppeteer";      // Cần filesystem + child_process
import nodemailer from "nodemailer";    // Cần net module
import { readFile } from "fs/promises"; // Node.js filesystem API
import mongoose from "mongoose";         // Kết nối TCP + Node.js API
 
// ✅ Thay thế tương thích edge
import { hashSync } from "bcryptjs";    // Triển khai JS thuần (chậm hơn)
// Cho hình ảnh: dùng service hoặc API riêng
// Cho email: dùng HTTP-based email API (Resend, SendGrid REST)
// Cho database: dùng HTTP-based client

Trước khi chuyển bất cứ gì sang edge, kiểm tra mọi dependency. Một require("fs") bị chôn ba tầng sâu trong cây dependency sẽ crash edge function lúc runtime — không phải lúc build. Bạn deploy, mọi thứ trông ổn, rồi request đầu tiên chạm code path đó và bạn nhận lỗi khó hiểu.

4. Kích Thước Bundle Lớn#

Nền tảng edge có giới hạn bundle size nghiêm ngặt:

  • Cloudflare Workers: 1MB (miễn phí), 5MB (trả phí)
  • Vercel Edge Functions: 4MB (compressed)
  • Deno Deploy: 20MB

Nghe có vẻ đủ cho đến khi bạn import thư viện UI component, thư viện validation, và thư viện date. Tôi từng có edge middleware phình lên 3.5MB vì import từ barrel file kéo theo toàn bộ thư mục @/components.

typescript
// ❌ Import barrel file có thể kéo quá nhiều
import { validateEmail } from "@/lib/utils";
// Nếu utils.ts re-export từ 20 module khác, tất cả đều bị bundle
 
// ✅ Import trực tiếp từ nguồn
import { validateEmail } from "@/lib/validators/email";

5. Streaming và WebSocket#

Edge function có thể streaming response (Web Streams API), nhưng kết nối WebSocket lâu dài là câu chuyện khác. Trong khi một số nền tảng hỗ trợ WebSocket ở edge (Cloudflare Workers, Deno Deploy), bản chất tạm thời của edge function làm chúng không phù hợp cho kết nối stateful, lâu dài.

Next.js Edge Runtime#

Next.js giúp việc chuyển sang edge runtime theo từng route trở nên đơn giản. Bạn không phải all-in — bạn chọn chính xác route nào chạy ở edge.

Middleware (Luôn Edge)#

Next.js middleware luôn chạy ở edge. Điều này là by design — middleware chặn mọi request khớp, nên nó cần nhanh và phân tán toàn cầu:

typescript
// middleware.ts — luôn chạy ở edge, không cần opt-in
import { NextRequest, NextResponse } from "next/server";
 
export function middleware(request: NextRequest) {
  // Chạy trước mọi request khớp
  // Giữ nhanh — không gọi database, không tính toán nặng
  return NextResponse.next();
}
 
export const config = {
  // Chỉ chạy trên path cụ thể
  matcher: [
    "/((?!_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml).*)",
  ],
};

API Route Ở Edge#

Bất kỳ route handler nào cũng có thể opt-in edge runtime:

typescript
// app/api/hello/route.ts
export const runtime = "edge"; // Dòng này thay đổi runtime
 
export async function GET(request: Request) {
  return Response.json({
    message: "Hello from the edge",
    region: process.env.VERCEL_REGION ?? "unknown",
    timestamp: Date.now(),
  });
}

Page Route Ở Edge#

Ngay cả toàn bộ trang cũng có thể render ở edge, dù tôi sẽ cân nhắc kỹ trước khi làm điều này:

typescript
// app/dashboard/page.tsx
export const runtime = "edge";
 
export default async function DashboardPage() {
  // Nhớ: không có Node.js API ở đây
  // Mọi data fetching phải dùng fetch() hoặc client tương thích edge
  const data = await fetch("https://api.example.com/dashboard", {
    headers: { Authorization: `Bearer ${process.env.API_KEY}` },
    next: { revalidate: 60 },
  }).then((r) => r.json());
 
  return (
    <main>
      <h1>Dashboard</h1>
      {/* render data */}
    </main>
  );
}

Những Gì Có Sẵn Trong Edge Runtime#

Đây là tham chiếu thực tế về những gì bạn có và không có:

typescript
// ✅ Có sẵn ở edge
fetch()                          // HTTP request
Request / Response               // Web standard request/response
Headers                          // HTTP header
URL / URLSearchParams            // URL parsing
TextEncoder / TextDecoder        // String encoding
crypto.subtle                    // Crypto operation (signing, hashing)
crypto.randomUUID()              // Tạo UUID
crypto.getRandomValues()         // Số ngẫu nhiên mật mã
structuredClone()                // Deep cloning
atob() / btoa()                  // Base64 encoding/decoding
setTimeout() / setInterval()     // Timer (nhưng nhớ giới hạn CPU)
console.log()                    // Logging
ReadableStream / WritableStream  // Streaming
AbortController / AbortSignal    // Hủy request
URLPattern                       // URL pattern matching
 
// ❌ KHÔNG có sẵn ở edge
require()                        // CommonJS (dùng import)
fs / path / os                   // Module tích hợp Node.js
process.exit()                   // Điều khiển process
Buffer                           // Dùng Uint8Array thay thế
__dirname / __filename           // Dùng import.meta.url
setImmediate()                   // Không phải web standard

Auth Ở Edge: Pattern Đầy Đủ#

Tôi muốn đi sâu hơn về authentication vì đây là một trong những use case edge có tác động lớn nhất, nhưng cũng dễ làm sai.

Pattern hoạt động là: xác minh token ở edge, truyền claim đáng tin cậy downstream, không bao giờ chạm database trong middleware.

typescript
// lib/edge-auth.ts — Tiện ích auth tương thích edge
import { jwtVerify, SignJWT, importSPKI, importPKCS8 } from "jose";
 
const PUBLIC_KEY_PEM = process.env.JWT_PUBLIC_KEY!;
const ISSUER = "https://auth.myapp.com";
const AUDIENCE = "https://myapp.com";
 
export interface TokenPayload {
  sub: string;
  email: string;
  role: "user" | "admin" | "moderator";
  iat: number;
  exp: number;
}
 
export async function verifyToken(token: string): Promise<TokenPayload | null> {
  try {
    const publicKey = await importSPKI(PUBLIC_KEY_PEM, "RS256");
    const { payload } = await jwtVerify(token, publicKey, {
      algorithms: ["RS256"],
      issuer: ISSUER,
      audience: AUDIENCE,
      clockTolerance: 30, // Dung sai lệch đồng hồ 30 giây
    });
 
    return payload as unknown as TokenPayload;
  } catch {
    return null;
  }
}
 
export function isTokenExpiringSoon(payload: TokenPayload): boolean {
  const now = Math.floor(Date.now() / 1000);
  const fiveMinutes = 5 * 60;
  return payload.exp - now < fiveMinutes;
}
typescript
// middleware.ts — Auth middleware
import { NextRequest, NextResponse } from "next/server";
import { verifyToken, isTokenExpiringSoon } from "./lib/edge-auth";
 
const PUBLIC_PATHS = ["/", "/login", "/register", "/api/auth/login"];
const ADMIN_PATHS = ["/admin"];
 
export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
 
  // Bỏ qua auth cho path công khai
  if (PUBLIC_PATHS.some((p) => pathname === p || pathname.startsWith("/api/public"))) {
    return NextResponse.next();
  }
 
  // Trích xuất token
  const token = request.cookies.get("auth-token")?.value;
  if (!token) {
    return NextResponse.redirect(new URL("/login", request.url));
  }
 
  // Xác minh token (crypto thuần — không gọi database)
  const payload = await verifyToken(token);
  if (!payload) {
    const response = NextResponse.redirect(new URL("/login", request.url));
    response.cookies.delete("auth-token");
    return response;
  }
 
  // Kiểm soát truy cập dựa trên role
  if (ADMIN_PATHS.some((p) => pathname.startsWith(p)) && payload.role !== "admin") {
    return NextResponse.redirect(new URL("/unauthorized", request.url));
  }
 
  // Truyền thông tin người dùng đã xác minh đến origin qua header đáng tin cậy
  const response = NextResponse.next();
  response.headers.set("x-user-id", payload.sub);
  response.headers.set("x-user-email", payload.email);
  response.headers.set("x-user-role", payload.role);
 
  // Báo hiệu nếu token cần refresh
  if (isTokenExpiringSoon(payload)) {
    response.headers.set("x-token-refresh", "true");
  }
 
  return response;
}
typescript
// app/api/profile/route.ts — Origin server đọc header đáng tin cậy
export async function GET(request: Request) {
  // Các header này được đặt bởi edge middleware sau khi xác minh JWT
  // Chúng đáng tin cậy vì đến từ hạ tầng của chúng ta
  const userId = request.headers.get("x-user-id");
  const userRole = request.headers.get("x-user-role");
 
  if (!userId) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }
 
  // Giờ chúng ta có thể truy vấn database — ở origin server,
  // ngay bên cạnh database, với connection pool
  const user = await db.user.findUnique({ where: { id: userId } });
 
  return Response.json(user);
}

Insight quan trọng: edge làm phần nhanh (xác minh crypto), và origin làm phần chậm (database query). Mỗi cái chạy nơi nó hiệu quả nhất.

Lưu ý quan trọng: điều này chỉ hoạt động với JWT. Nếu hệ thống auth cần tra cứu database mỗi request (như session-based auth với session ID cookie), edge không giúp được — bạn vẫn cần gọi database, nghĩa là khứ hồi đến origin region.

Edge Caching#

Caching ở edge là nơi mọi thứ trở nên thú vị. Edge node có thể cache response, nghĩa là request tiếp theo đến cùng URL được phục vụ trực tiếp từ edge mà không cần đến origin.

Cache-Control Đúng Cách#

typescript
// app/api/products/route.ts
export const runtime = "edge";
 
export async function GET(request: Request) {
  const url = new URL(request.url);
  const category = url.searchParams.get("category") ?? "all";
 
  const products = await fetch(
    `${process.env.ORIGIN_API}/products?category=${category}`,
  ).then((r) => r.json());
 
  return Response.json(products, {
    headers: {
      // Cache trên CDN 60 giây
      // Phục vụ stale trong khi revalidate đến 5 phút
      // Client cache 10 giây
      "Cache-Control": "public, s-maxage=60, stale-while-revalidate=300, max-age=10",
 
      // Vary theo header này để variant khác nhau có cache entry khác nhau
      Vary: "Accept-Language, Accept-Encoding",
 
      // Cache tag dành cho CDN cho invalidation nhắm mục tiêu
      "Cache-Tag": `products,category-${category}`,
    },
  });
}

Pattern stale-while-revalidate đặc biệt mạnh ở edge. Đây là những gì xảy ra:

  1. Request đầu tiên: Edge fetch từ origin, cache response, trả về
  2. Request trong 60 giây: Edge phục vụ từ cache (0ms origin latency)
  3. Request ở giây 61-360: Edge phục vụ bản cached cũ ngay lập tức, nhưng fetch bản mới từ origin ở background
  4. Sau 360 giây: Cache hết hạn hoàn toàn, request tiếp theo đi đến origin

Người dùng gần như luôn nhận cached response. Sự đánh đổi độ tươi là rõ ràng và có thể điều chỉnh.

Edge Config Cho Cấu Hình Động#

Edge Config của Vercel (và service tương tự từ nền tảng khác) cho phép lưu trữ cấu hình key-value được nhân bản đến mọi edge location. Cực kỳ hữu ích cho feature flag, quy tắc redirect, và cấu hình A/B test mà bạn muốn cập nhật mà không cần redeploy:

typescript
import { get } from "@vercel/edge-config";
import { NextRequest, NextResponse } from "next/server";
 
export async function middleware(request: NextRequest) {
  // Đọc Edge Config cực nhanh (~1ms) vì
  // dữ liệu được nhân bản đến mọi edge location
  const maintenanceMode = await get<boolean>("maintenance_mode");
 
  if (maintenanceMode) {
    return NextResponse.rewrite(new URL("/maintenance", request.url));
  }
 
  // Feature flag
  const features = await get<Record<string, boolean>>("feature_flags");
  if (features?.["new_pricing_page"] && request.nextUrl.pathname === "/pricing") {
    return NextResponse.rewrite(new URL("/pricing-v2", request.url));
  }
 
  // Redirect động (cập nhật redirect không cần redeploy)
  const redirects = await get<Array<{ from: string; to: string; permanent: boolean }>>(
    "redirects",
  );
 
  if (redirects) {
    const match = redirects.find((r) => r.from === request.nextUrl.pathname);
    if (match) {
      return NextResponse.redirect(
        new URL(match.to, request.url),
        match.permanent ? 308 : 307,
      );
    }
  }
 
  return NextResponse.next();
}

Đây là game-changer thực sự. Trước Edge Config, thay đổi feature flag nghĩa là sửa code và redeploy. Giờ bạn cập nhật giá trị JSON trong dashboard và nó lan truyền toàn cầu trong vài giây.

Phép Tính Hiệu Năng Thực Tế#

Hãy làm phép tính thành thật thay vì phép tính marketing. Tôi sẽ so sánh ba kiến trúc cho endpoint API điển hình cần query database:

Kịch Bản: User Profile API (2 database query)#

Kiến trúc A: Node.js Regional Truyền Thống

Người dùng ở Tokyo → Origin ở Virginia: 140ms
  + DB query 1 (cùng region): 2ms
  + DB query 2 (cùng region): 2ms
  + Xử lý: 5ms
  = Tổng: ~149ms

Kiến trúc B: Edge Function Với HTTP Database

Người dùng ở Tokyo → Edge ở Tokyo: 5ms
  + DB query 1 (HTTP đến Virginia): 145ms
  + DB query 2 (HTTP đến Virginia): 145ms
  + Xử lý: 3ms
  = Tổng: ~298ms ← CHẬM HƠN regional

Kiến trúc C: Edge Function Với Regional Database (read replica)

Người dùng ở Tokyo → Edge ở Tokyo: 5ms
  + DB query 1 (HTTP đến Tokyo replica): 8ms
  + DB query 2 (HTTP đến Tokyo replica): 8ms
  + Xử lý: 3ms
  = Tổng: ~24ms ← Nhanh nhất, nhưng cần multi-region DB

Kiến trúc D: Edge Cho Auth + Regional Cho Data

Người dùng ở Tokyo → Edge middleware ở Tokyo: 5ms (xác minh JWT)
  → Origin ở Virginia: 140ms
  + DB query 1 (cùng region): 2ms
  + DB query 2 (cùng region): 2ms
  + Xử lý: 5ms
  = Tổng: ~154ms
  (Nhưng auth đã được xác minh — origin không cần xác minh lại)
  (Và request không được phép bị chặn ở edge — không bao giờ đến origin)

Các kết luận:

  1. Edge + origin database = thường chậm hơn so với chỉ dùng regional server
  2. Edge + multi-region database = nhanh nhất nhưng đắt nhất và phức tạp nhất
  3. Edge cho gác cổng + regional cho data = cân bằng thực tế tốt nhất
  4. Edge thuần (không database) = bất bại cho redirect và auth check

Kiến trúc D là thứ tôi dùng cho hầu hết dự án. Edge xử lý những gì nó giỏi (quyết định nhanh, auth, routing), và server Node.js regional xử lý những gì nó giỏi (database query, tính toán nặng).

Khi Edge Thực Sự Thắng: Operation Không Database#

Phép tính hoàn toàn đảo ngược khi không có database:

Redirect (edge):
  Người dùng ở Tokyo → Edge ở Tokyo → redirect response: ~5ms

Redirect (regional):
  Người dùng ở Tokyo → Origin ở Virginia → redirect response: ~280ms
API response tĩnh (edge + cache):
  Người dùng ở Tokyo → Edge ở Tokyo → cached response: ~5ms

API response tĩnh (regional):
  Người dùng ở Tokyo → Origin ở Virginia → response: ~280ms
Chặn bot (edge):
  Bot xấu ở bất kỳ đâu → Edge (gần nhất) → 403 response: ~5ms
  (Bot không bao giờ đến origin server)

Chặn bot (regional):
  Bot xấu ở bất kỳ đâu → Origin ở Virginia → 403 response: ~280ms
  (Bot vẫn tiêu thụ tài nguyên origin)

Cho operation không cần database, edge nhanh hơn 20-50 lần. Đây không phải marketing — đây là vật lý.

Framework Quyết Định Của Tôi#

Sau một năm làm việc với edge function trong production, đây là flowchart tôi dùng cho mỗi endpoint hoặc logic mới:

Bước 1: Nó có cần Node.js API không?#

Nếu import fs, net, child_process, hoặc native module — Node.js regional. Không tranh luận.

Bước 2: Nó có cần database query không?#

Nếu có, và bạn không có read replica gần người dùng — Node.js regional (cùng region với database). Database khứ hồi sẽ chi phối.

Nếu có, và bạn có read replica phân tán toàn cầu — Edge có thể hoạt động, dùng HTTP-based database client.

Bước 3: Nó là quyết định về request (routing, auth, redirect)?#

Nếu có — Edge. Đây là điểm ngọt. Bạn đang đưa ra quyết định nhanh quyết định điều gì xảy ra với request trước khi nó đến origin.

Bước 4: Response có cacheable không?#

Nếu có — Edge với header Cache-Control đúng. Ngay cả nếu request đầu tiên đến origin, request tiếp theo phục vụ từ edge cache.

Bước 5: Nó có nặng CPU không?#

Nếu liên quan đến tính toán đáng kể (xử lý hình ảnh, tạo PDF, biến đổi dữ liệu lớn) — Node.js regional.

Bước 6: Nó nhạy cảm với latency đến mức nào?#

Nếu là background job hoặc webhook — Node.js regional. Không ai đợi nó. Nếu là request người dùng nơi mỗi ms đều quan trọng — Edge, nếu đáp ứng tiêu chí khác.

Bảng Tóm Tắt#

typescript
// ✅ HOÀN HẢO cho edge
// - Middleware (auth, redirect, rewrite, header)
// - Logic vị trí địa lý
// - Gán A/B test
// - Phát hiện bot / quy tắc WAF
// - API response thân thiện cache
// - Kiểm tra feature flag
// - CORS preflight response
// - Biến đổi dữ liệu tĩnh (không DB)
// - Xác minh chữ ký webhook
 
// ❌ GIỮ trên Node.js regional
// - Database CRUD operation
// - Upload / xử lý file
// - Chỉnh sửa hình ảnh
// - Tạo PDF
// - Gửi email (dùng HTTP API, nhưng vẫn regional)
// - WebSocket server
// - Background job / hàng đợi
// - Bất cứ gì dùng native npm package
// - SSR page với database query
// - GraphQL resolver truy vấn database
 
// 🤔 TÙY TRƯỜNG HỢP
// - Authentication (edge cho JWT, regional cho session-DB)
// - API route (edge nếu không DB, regional nếu DB)
// - Server-rendered page (edge nếu data từ cache/fetch, regional nếu DB)
// - Tính năng real-time (edge cho auth ban đầu, regional cho kết nối bền vững)

Những Gì Tôi Thực Sự Chạy Ở Edge#

Cho trang web này, đây là phân tích:

Edge (middleware):

  • Phát hiện locale và redirect
  • Lọc bot
  • Security header (CSP, HSTS, v.v.)
  • Access logging
  • Rate limiting (cơ bản)

Node.js regional:

  • Render nội dung blog (xử lý MDX cần Node.js API qua Velite)
  • API route truy cập Redis
  • Tạo OG image (cần nhiều CPU time hơn)
  • Tạo RSS feed

Static (không runtime nào cả):

  • Trang tool (pre-render lúc build)
  • Trang bài blog (pre-render lúc build)
  • Tất cả hình ảnh và asset (CDN phục vụ)

Runtime tốt nhất thường là không có runtime. Nếu bạn có thể pre-render thứ gì đó lúc build và phục vụ như static asset, điều đó sẽ luôn nhanh hơn bất kỳ edge function nào. Edge dành cho những thứ thực sự cần dynamic trên mỗi request.

Tóm Tắt Thành Thật#

Edge function không phải là thay thế cho server truyền thống. Chúng là bổ sung. Chúng là công cụ bổ sung trong hộp công cụ kiến trúc — cực kỳ mạnh cho use case đúng và gây hại cho use case sai.

Heuristic tôi luôn quay lại: nếu function cần truy cập database ở một region duy nhất, đặt function ở edge không giúp — nó hại. Bạn vừa thêm một hop. Function chạy nhanh hơn, nhưng rồi tốn 100ms+ để quay lại database. Kết quả ròng: chậm hơn so với chạy mọi thứ ở một region.

Nhưng cho quyết định có thể đưa ra chỉ với thông tin trong request — vị trí địa lý, cookie, header, JWT — edge là bất bại. Những response 5ms ở edge không phải benchmark tổng hợp. Chúng có thật, và người dùng cảm nhận được sự khác biệt.

Đừng chuyển mọi thứ sang edge. Đừng giữ mọi thứ khỏi edge. Đặt mỗi phần logic nơi vật lý ưu tiên nó.

Bài viết liên quan