Đ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

Next.js 15 và React Server Components: Khi nào dùng Server vs Client Components cho web app 2026

Ếch Trendy
Ếch Trendy

3 ngày trước · 7 phút đọc

Server-first là gì và tại sao Next.js 15 đẩy mạnh hướng này

Mình nhớ lần đầu đọc docs React Server Components - cảm giác như bị reset toàn bộ mental model về React. Cái mình tưởng là "React" suốt mấy năm qua thực ra chỉ là một nửa câu chuyện.

Trước Next.js 13, mọi thứ đều chạy ở client. Bạn fetch data trong useEffect, hiển thị loading spinner, rồi render UI sau khi data về. Vấn đề? JavaScript bundle ngày càng phình to, SEO phụ thuộc vào SSR/SSG workaround, và người dùng phải chờ hai lần: tải JS xong rồi mới fetch data.

347229 Kiến trúc cũ: mọi thứ đều chạy ở client, bundle JS phình to theo thời gian

Next.js 15 với React Server Components (RSC) đảo ngược logic này. Mặc định, mọi component đều là Server Component - nghĩa là render ở server, gửi HTML thuần về browser, không kèm JavaScript. Client Components chỉ được dùng khi thực sự cần interactivity.

Kết quả thực tế? Bundle JavaScript phía client giảm đáng kể, initial page load nhanh hơn, và SEO cải thiện tự nhiên không cần trick gì thêm.


Server Components: dùng khi nào, làm được gì

Rule of thumb đơn giản: nếu component không cần onClick, useState, hoặc browser API - hãy để nó là Server Component.

347230 Server Component phù hợp nhất khi chỉ cần đọc data và render HTML

Server Components có thể:

  • Fetch data trực tiếp (kể cả query database) mà không cần API route
  • Đọc file system, environment variables bí mật
  • Import các thư viện nặng mà không ảnh hưởng bundle client
  • Render Markdown, highlight code, xử lý dữ liệu phức tạp
// app/posts/[slug]/page.tsx - Server Component mặc định
import { getPostBySlug } from '@/lib/db';
import { MDXRemote } from 'next-mdx-remote/rsc';

// Không cần 'use client' - đây là Server Component
export default async function PostPage({ params }: { params: { slug: string } }) {
  // Fetch thẳng từ DB, không qua API
  const post = await getPostBySlug(params.slug);

  return (
    <article>
      <h1>{post.title}</h1>
      {/* MDXRemote nặng ~200KB nhưng không tốn gì ở client */}
      <MDXRemote source={post.content} />
    </article>
  );
}

Lưu ý quan trọng: Server Components không thể nhận event handlers, không dùng hooks như useState hay useEffect, và không truy cập browser APIs (window, document, localStorage). Nếu cần những thứ này, bạn phải chuyển sang Client Component.

Một điểm hay của RSC là composability - bạn có thể lồng Client Component bên trong Server Component, nhưng không ngược lại. Pattern này cho phép giữ phần lớn app ở server, chỉ "island" nhỏ nào cần interactivity mới là client.


Client Components: đừng sợ dùng, nhưng biết giới hạn

Có một hiểu nhầm khá phổ biến sau khi Next.js 13 ra mắt RSC: nhiều người nghĩ Client Components là "xấu" và cần tránh tối đa. Thực ra không phải vậy.

Client Components vẫn render phía server ở lần đầu (SSR), nhưng sau đó hydrate ở browser để có interactivity. Bạn cần chúng cho:

  • State management (useState, useReducer)
  • Side effects (useEffect, useLayoutEffect)
  • Event handlers (onClick, onChange, onSubmit)
  • Browser APIs (localStorage, window, geolocation)
  • Các thư viện animation như Framer Motion
  • Real-time updates (WebSocket, Server-Sent Events)
// components/SearchBar.tsx
'use client'; // Khai báo Client Component

import { useState, useTransition } from 'react';
import { useRouter } from 'next/navigation';

export function SearchBar() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();
  const router = useRouter();

  const handleSearch = (value: string) => {
    setQuery(value);
    // startTransition giữ UI responsive trong khi navigate
    startTransition(() => {
      router.push(`/search?q=${value}`);
    });
  };

  return (
    <input
      value={query}
      onChange={(e) => handleSearch(e.target.value)}
      placeholder={isPending ? 'Đang tìm...' : 'Tìm kiếm...'}
      className="search-input"
    />
  );
}

347231 Client Component phù hợp nhất cho interactivity - nhưng hãy giữ nó nhỏ và tập trung

Một pattern mình hay dùng: push 'use client' xuống thấp nhất có thể trong component tree. Ví dụ, trang blog có Server Component lớn fetch và render bài viết, chỉ phần comment box mới là Client Component. Không cần thiết phải chuyển cả trang thành client chỉ vì một cái button.


Data fetching trong Next.js 15: cách tiếp cận mới

Đây là phần thay đổi nhiều nhất so với Next.js cũ. Bỏ hết getServerSideProps, getStaticProps đi - trong App Router, bạn fetch data trực tiếp trong component.

Next.js 15 mở rộng fetch của browser với cơ chế caching riêng. Tuy nhiên, có một thay đổi quan trọng so với Next.js 14: mặc định fetch không cache nữa (opt-out thay vì opt-in).

// app/products/page.tsx
async function getProducts() {
  // Mặc định Next.js 15: không cache (tương đương cache: 'no-store')
  const res = await fetch('https://api.example.com/products');
  return res.json();
}

async function getCategories() {
  // Cache 1 tiếng, revalidate sau
  const res = await fetch('https://api.example.com/categories', {
    next: { revalidate: 3600 }
  });
  return res.json();
}

async function getConfig() {
  // Cache vĩnh viễn (static)
  const res = await fetch('https://api.example.com/config', {
    cache: 'force-cache'
  });
  return res.json();
}

export default async function ProductsPage() {
  // Hai fetch chạy song song - không cần Promise.all vì RSC tự optimize
  const [products, categories] = await Promise.all([
    getProducts(),
    getCategories()
  ]);

  return (
    <main>
      <CategoryFilter categories={categories} />
      <ProductGrid products={products} />
    </main>
  );
}

347232 Dùng Promise.all để fetch song song - tránh waterfall request làm chậm trang

Một điểm mạnh khác: Request Memoization. Nếu nhiều component trong cùng request gọi cùng một URL, Next.js chỉ thực sự fetch một lần. Bạn không cần prop drilling data từ trên xuống - cứ fetch ngay tại component cần dùng.


Streaming và Suspense: load từng phần thay vì chờ tất cả

Bạn có bao giờ vào một trang và nhìn màn hình trắng trong 2-3 giây chờ tất cả data load xong?

Streaming giải quyết đúng vấn đề này. Thay vì block toàn bộ page cho đến khi mọi data ready, Next.js 15 cho phép stream từng phần HTML về browser ngay khi phần đó render xong.

// app/dashboard/page.tsx
import { Suspense } from 'react';
import { UserProfile } from './UserProfile'; // Load nhanh
import { RecentOrders } from './RecentOrders'; // Load chậm
import { Analytics } from './Analytics'; // Load rất chậm

export default function DashboardPage() {
  return (
    <div className="dashboard">
      {/* Render ngay lập tức - không cần data */}
      <h1>Dashboard</h1>

      {/* Load nhanh - hiển thị trước */}
      <Suspense fallback={<ProfileSkeleton />}>
        <UserProfile />
      </Suspense>

      {/* Load chậm hơn - hiển thị sau khi ready */}
      <Suspense fallback={<OrdersSkeleton />}>
        <RecentOrders />
      </Suspense>

      {/* Load chậm nhất - user thấy skeleton trong lúc chờ */}
      <Suspense fallback={<AnalyticsSkeleton />}>
        <Analytics />
      </Suspense>
    </div>
  );
}

347233 Streaming cho phép hiển thị UI ngay lập tức, thay vì chờ tất cả data về mới render

User thấy gì? Header và profile xuất hiện ngay, orders skeleton rồi thay bằng real data, analytics xuất hiện sau cùng. Tổng thời gian load không thay đổi, nhưng perceived performance cải thiện rõ rệt.

Kết hợp với loading.tsx (file đặc biệt trong App Router) thì bạn có streaming built-in mà không cần code thêm gì:


Partial Prerendering: best of both worlds

Partial Prerendering (PPR) là tính năng thực nghiệm trong Next.js 15, và nó là thứ mình hào hứng nhất trong bản release này.

Vấn đề truyền thống: bạn phải chọn hoặc là static (nhanh, cache được, SEO tốt) hoặc là dynamic (có data mới nhất). PPR xóa bỏ trade-off này.

Với PPR, một trang được prerender static shell ngay lúc build time - bao gồm layout, navigation, bất kỳ thứ gì không phụ thuộc data động. Phần dynamic (personalized content, real-time data) được stream vào sau khi user request.

// next.config.js - Bật PPR (experimental)
module.exports = {
  experimental: {
    ppr: true, // Hoặc 'incremental' để bật từng route
  },
};

// app/product/[id]/page.tsx
import { Suspense } from 'react';
import { StaticProductInfo } from './StaticProductInfo';
import { DynamicInventory } from './DynamicInventory';
import { PersonalizedRecommendations } from './PersonalizedRecommendations';

export default function ProductPage({ params }: { params: { id: string } }) {
  return (
    <>
      {/* Phần này prerender ở build time - static, nhanh */}
      <StaticProductInfo id={params.id} />

      {/* Phần này stream khi có request - dynamic */}
      <Suspense fallback={<InventorySkeleton />}>
        <DynamicInventory id={params.id} />
      </Suspense>

      {/* Personalized - chỉ render sau khi biết user là ai */}
      <Suspense fallback={<RecommendationsSkeleton />}>
        <PersonalizedRecommendations userId={getUserId()} />
      </Suspense>
    </>
  );
}

347234 PPR: shell tĩnh load ngay từ cache, phần động stream vào theo request - không phải chọn một trong hai

Thực tế, trang product trên sẽ: hiển thị tên, mô tả, ảnh sản phẩm từ cache CDN ngay lập tức (TTFB cực thấp), sau đó stream số lượng tồn kho và gợi ý cá nhân hóa vào trong khi user đang đọc.