Đang tải...

Ếch Trendy

Cùng chú ếch nhỏ khám phá thế giới Web đầy biến động. Cập nhật trending nhanh như cách ếch đớp mồi! ⌨️🌿


0

React 19 Server Components: Bí Quyết Tăng Tốc Web App Cho Dev Việt 2026

Ếch Trendy
Ếch Trendy

6 tháng trước · 10 phút đọc

React 19 Server Components là gì và tại sao dev Việt cần biết ngay?

Mình nhớ hồi build dashboard cho một dự án SaaS cuối năm ngoái, bundle JS sau khi build lên đến hơn 2MB. Trang load chậm, Lighthouse đỏ lòm, khách hàng phàn nàn. Sau khi chuyển sang React 19 với Server Components, bundle giảm còn chưa đến 400KB. Không phải nhờ trick gì cao siêu - đơn giản là mình ngừng ship JavaScript xuống browser cho những thứ không cần tương tác.

Server Components (RSC) là thay đổi kiến trúc lớn nhất của React kể từ khi hooks ra đời. Ý tưởng cốt lõi rất đơn giản: những component không cần state hay event handlers thì chạy hoàn toàn trên server, render thành HTML rồi stream xuống client - không một dòng JS nào được ship kèm.

342814 Server render xong mới stream HTML xuống - client nhẹ hơn đáng kể

Với dev Việt đang build SaaS, dashboard, hay e-commerce: đây là thứ đáng để đầu tư thời gian năm 2026. Lý do?

  • Header, sidebar, data table, product list - tất cả có thể là Server Components
  • Fetch data trực tiếp trong component, không cần useEffect hay API routes riêng
  • SEO cải thiện vì server trả về HTML thật, không phải empty shell
  • LCP nhanh hơn nhờ Partial Prerendering trong Next.js 15

React 19 đã stable, nhưng để dùng RSC trong production thì Next.js 15 (App Router) vẫn là lựa chọn duy nhất đủ trưởng thành.


Server Components vs Client Components: Chọn cái nào?

Câu hỏi mình nhận nhiều nhất từ anh em sau khi giới thiệu RSC: "Vậy tôi dùng cái nào?" Thật ra quy tắc rất đơn giản - nếu component không cần useState, useEffect, hay xử lý event từ người dùng, hãy để nó là Server Component.

342815 Hai loại component, hai nhiệm vụ khác nhau - không phải cạnh tranh mà bổ trợ

Dưới đây là bảng so sánh để bạn tham khảo:

Tiêu chí Server Components Client Components
Chạy ở đâu Server only Server (initial) + Client hydrate
JS bundle Zero Có (state, effects)
Data fetching async/await trực tiếp useEffect + fetch
State/Events ❌ Không dùng được ✅ Full support
Browser APIs ❌ Không dùng được ✅ Full support
Use case điển hình Header, sidebar, data table, product list Form, modal, dropdown, chart tương tác

Một điểm dễ nhầm: Client Components không phải chỉ chạy trên browser. React vẫn render chúng trên server cho lần đầu (initial render), sau đó hydrate trên client để interactive. Sự khác biệt nằm ở chỗ Client Components ship kèm JavaScript, còn Server Components thì không.

Thêm 'use client' vào đầu file là cách khai báo Client Component:

// components/SearchBar.tsx
'use client';
import { useState } from 'react';

export default function SearchBar() {
  const [query, setQuery] = useState('');

  return (
    <input
      value={query}
      onChange={e => setQuery(e.target.value)}
      placeholder="Tìm kiếm sản phẩm..."
      className="border px-3 py-2 rounded"
    />
  );
}

Nếu không có directive này, Next.js 15 mặc định coi tất cả là Server Component. Đây là thay đổi tư duy lớn nhất so với React 18 - mặc định là server, opt-in sang client.


Ví dụ thực tế: Build dashboard với RSC patterns

Lý thuyết đủ rồi. Mình sẽ show code thật cho một màn hình dashboard kiểu SaaS - loại app mình thấy anh em hay build nhất.

Architecture: trang /dashboard fetch data danh sách users từ database, hiển thị trong bảng. Phần search/filter interactive thì để Client Component. Đây là pattern chuẩn - server lo data, client lo interaction.

// app/dashboard/page.tsx (Server Component - mặc định)
async function DashboardPage() {
  // Fetch trực tiếp, không cần useEffect hay API route
  const users = await fetch('https://api.example.com/users', {
    next: { revalidate: 60 } // Cache 60 giây
  }).then(res => res.json());

  return (
    <div className="p-6">
      <h1 className="text-2xl font-bold mb-4">Quản lý người dùng</h1>

      {/* Client Component - chỉ phần này ship JS */}
      <Suspense fallback={<p>Đang tải bộ lọc...</p>}>
        <UserSearchFilter />
      </Suspense>

      {/* Server Component - zero JS */}
      <UserTable users={users} />
    </div>
  );
}
// components/UserTable.tsx (Server Component)
function UserTable({ users }: { users: User[] }) {
  return (
    <table className="w-full border-collapse">
      <thead>
        <tr>
          <th className="border px-4 py-2">Tên</th>
          <th className="border px-4 py-2">Email</th>
          <th className="border px-4 py-2">Vai trò</th>
        </tr>
      </thead>
      <tbody>
        {users.map(user => (
          <tr key={user.id}>
            <td className="border px-4 py-2">{user.name}</td>
            <td className="border px-4 py-2">{user.email}</td>
            <td className="border px-4 py-2">{user.role}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}
// components/UserSearchFilter.tsx (Client Component)
'use client';
import { useState } from 'react';

export default function UserSearchFilter() {
  const [search, setSearch] = useState('');

  return (
    <div className="mb-4 flex gap-2">
      <input
        value={search}
        onChange={e => setSearch(e.target.value)}
        placeholder="Tìm theo tên hoặc email..."
        className="border px-3 py-2 rounded flex-1"
      />
      <select className="border px-3 py-2 rounded">
        <option value="">Tất cả vai trò</option>
        <option value="admin">Admin</option>
        <option value="user">User</option>
      </select>
    </div>
  );
}

342816 Server lo data, Client lo interaction - phân chia trách nhiệm rõ ràng

Kết quả khi build: UserTable (component lớn nhất) không đóng góp một byte nào vào JS bundle. Toàn bộ rendering logic ở server. Chỉ UserSearchFilter nhỏ mới ship JS xuống browser.

Một lưu ý quan trọng: Server Component có thể chứa Client Component, nhưng không ngược lại. DashboardPage (server) import UserSearchFilter (client) là hợp lệ. Ngược lại thì không - không thể import Server Component vào trong Client Component.


Server Actions: Xóa sổ API routes cho CRUD thông thường

Bạn có bao giờ nghĩ: tại sao mình cần viết cả API route chỉ để handle một form submit? Server Actions trong React 19/Next.js 15 giải quyết đúng vấn đề đó.

Server Actions là các function chạy trên server, được gọi trực tiếp từ component (kể cả Client Component). Không cần fetch('/api/something'), không cần route handler riêng.

// app/actions.ts
'use server'; // Directive này đánh dấu cả file là server-only

import { revalidatePath } from 'next/cache';

export async function createUser(formData: FormData) {
  const name = formData.get('name') as string;
  const email = formData.get('email') as string;

  // Gọi thẳng database - không qua API
  await db.users.create({ name, email });

  // Xóa cache, trang sẽ tự refresh data
  revalidatePath('/dashboard');
}

export async function deleteUser(userId: string) {
  await db.users.delete({ where: { id: userId } });
  revalidatePath('/dashboard');
}
// app/dashboard/CreateUserForm.tsx (Client Component)
'use client';
import { createUser } from '../actions';

export default function CreateUserForm() {
  return (
    <form action={createUser} className="flex gap-2 mb-4">
      <input
        name="name"
        placeholder="Tên người dùng"
        className="border px-3 py-2 rounded"
        required
      />
      <input
        name="email"
        type="email"
        placeholder="Email"
        className="border px-3 py-2 rounded"
        required
      />
      <button
        type="submit"
        className="bg-blue-600 text-white px-4 py-2 rounded"
      >
        Tạo mới
      </button>
    </form>
  );
}

342817 Server Actions thay thế API routes cho mutation - ít code hơn, an toàn hơn

Một ưu điểm ít người để ý: form với action={serverAction} hoạt động ngay cả khi JavaScript bị tắt trên browser (progressive enhancement). Đây là thứ rất khó đạt được với approach fetch API thông thường.

Thật ra trade-off cũng có: Server Actions không phù hợp cho những endpoint cần expose ra ngoài (third-party integrations, mobile app gọi). Trong những trường hợp đó vẫn nên dùng Route Handlers. Nguyên tắc đơn giản: mutation nội bộ trong app → Server Actions, endpoint public → Route Handlers.


Migrate từ React 18 lên React 19 + Next.js 15: Từng bước không đau

Mình đã migrate 3 project trong 6 tháng qua. Project đơn giản nhất mất khoảng 2 buổi tối. Phức tạp nhất (pages router → app router) mất khoảng 1 tuần. Dưới đây là con đường ít đau nhất.

Bước 1: Upgrade dependencies

# Tạo project mới với Next.js 15
npx create-next-app@15 my-app --typescript --app

# Hoặc upgrade project hiện tại
npm install react@19 react-dom@19 next@15
npm install --save-dev @types/react@19 @types/react-dom@19

Bước 2: Chuyển từ Pages Router sang App Router

pages/ không biến mất ngay - Next.js 15 hỗ trợ cả hai song song. Bạn có thể migrate từng page một:

# Cấu trúc cũ (Pages Router)
pages/
  index.tsx          → app/page.tsx
  dashboard.tsx      → app/dashboard/page.tsx
  _app.tsx           → app/layout.tsx

Bước 3: Chuyển data fetching

// TRƯỚC - getServerSideProps (React 18/Pages Router)
export async function getServerSideProps() {
  const data = await fetchUsers();
  return { props: { users: data } };
}

export default function UsersPage({ users }) {
  return <UserTable users={users} />;
}

// SAU - Async Server Component (React 19/App Router)
export default async function UsersPage() {
  const users = await fetchUsers(); // Fetch thẳng trong component
  return <UserTable users={users} />;
}

Bước 4: Thêm 'use client' đúng chỗ

Sau khi chuyển sang App Router, lỗi hay gặp nhất là dùng useState/useEffect mà không có 'use client'. Next.js sẽ báo lỗi rõ ràng - cứ follow lỗi mà fix.

Bước 5: Chuyển API routes sang Server Actions (optional)

Không cần làm ngay. API routes vẫn hoạt động bình thường trong Next.js 15. Chuyển dần khi có thời gian.

342818 Migration từng bước - không cần làm tất cả cùng lúc

Lưu ý bảo mật quan trọng: RSC có lỗ hổng bảo mật CVE-2025-55182 ảnh hưởng React 19.0 đến 19.2.0. Nếu bạn đang dùng phiên bản này, patch ngay lên phiên bản mới nhất trước khi deploy production. Kiểm tra bằng npm audit để xác nhận.

Nếu bạn muốn có nền tảng React vững trước khi nhảy vào Server Components, khóa học Xây Dựng Website với ReactJS cover đầy đủ từ cơ bản. Còn nếu muốn đi thẳng vào Next.js thực chiến, khóa Frontend NextJS của thầy Hoàng An là lựa chọn phù hợp.


Performance thực tế và deployment với Vercel

Số liệu mình hay thấy trong các benchmark: bundle JS giảm 60% reduction in client bundle sizes (CoderTrove - React Server Components + Next.js 15 in 2025) khi chuyển sang RSC cho các trang data-heavy. LCP (Largest Contentful Paint) cải thiện nhờ server-rendered HTML được gửi ngay, không chờ JS parse và execute.

Next.js 15 có thêm Partial Prerendering (PPR) - tính năng kết hợp static và dynamic rendering trong cùng một route:

// app/dashboard/page.tsx
import { Suspense } from 'react';

// Shell này được prerender tĩnh
export default function Dashboard() {
  return (
    <div>
      {/* Static - prerender, serve từ CDN */}
      <StaticHeader />
      <StaticSidebar />

      {/* Dynamic - stream sau khi static shell load xong */}
      <Suspense fallback={<TableSkeleton />}>
        <DynamicDataTable /> {/* Fetch real-time data */}
      </Suspense>
    </div>
  );
}

Kết quả: người dùng thấy UI ngay (static shell từ CDN), data load sau mà không bị blank screen.

342819 Partial Prerendering: static shell load ngay, dynamic data stream sau

Deployment lên Vercel là straightforward nhất cho Next.js 15:

# Deploy với Vercel CLI
npm i -g vercel
vercel --prod

Vercel tự detect Next.js, enable Turbopack cho build, và tự cấu hình Edge Network. Turbopack giúp build production nhanh hơn so với Webpack truyền thống - dev server startup tính bằng milliseconds thay vì seconds.

Nếu không dùng Vercel, Next.js 15 hỗ trợ self-hosting với next start hoặc Docker:

# Dockerfile tối ưu cho Next.js 15
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]

Tùy chọn output: 'standalone' trong next.config.js giúp tạo bundle chỉ chứa những gì cần thiết - image Docker nhỏ hơn đáng kể.

Giờ bạn đã có đủ công cụ để bắt đầu:

  1. Tạo project Next.js 15 mới và thử viết 1 Server Component fetch data
  2. Thêm 1 Client Component nhỏ (search input) và bọc bằng Suspense
  3. Viết 1 Server Action cho form submit
  4. Chạy npm run build và xem bundle analyzer - so sánh với project React 18 cũ

Gặp vướng mắc, drop comment. Mình đọc hết.