Đ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: Nên nâng cấp không? Xu hướng server-first cho web app hiện đại

Ếch Trendy
Ếch Trendy

một tháng trước · 11 phút đọc

Pages Router vs App Router: câu chuyện hai thế giới

Mình nhớ hồi migrate một dự án e-commerce từ Pages Router sang App Router. Mất đúng 3 tuần. Không phải vì khó - mà vì mình hiểu sai bản chất của sự thay đổi.

Pages Router và App Router không chỉ khác cú pháp. Chúng khác nhau ở triết lý render.

346841 Pages Router render ở client, App Router ưu tiên server - đây là điểm khác biệt cốt lõi

Với Pages Router (Next.js trước phiên bản 13), mọi component mặc định là client component. Bạn muốn server-side rendering? Phải khai báo getServerSideProps hoặc getStaticProps tường minh. Data fetching và UI tách rời nhau.

Với App Router (Next.js 13+ và được hoàn thiện trong v15), mọi component mặc định là Server Component. Bạn muốn chạy ở client? Khai báo 'use client' ở đầu file. Logic ngược lại hoàn toàn.

Tại sao lại đảo ngược vậy? Vì 200-400 KB gzipped JavaScript for typical React web applications in 2024, with median around 215 KB (Web performance benchmarks and HTTP Archive 2024 data) lượng code JavaScript được tải về client trong các ứng dụng React truyền thống là quá lớn. Server Component giải quyết bằng cách giữ logic nặng trên server, chỉ gửi HTML đã render xuống browser.

// Pages Router - data fetching tách rời component
export async function getServerSideProps() {
  // Chạy trên server nhưng phải return props
  const data = await fetchProducts();
  return { props: { data } };
}

export default function ProductPage({ data }) {
  return <ProductList items={data} />;
}

// App Router - component và data fetching gắn liền
// Không cần 'use client' → Server Component mặc định
async function ProductPage() {
  // Trực tiếp await trong component
  const data = await fetchProducts();
  return <ProductList items={data} />;
}

Sự thay đổi này ảnh hưởng đến mọi thứ: cách bạn tổ chức folder, cách xử lý state, cách viết layout. Đó là lý do migration không đơn giản là "copy code sang folder mới".


Server Components, Streaming và Partial Prerendering trong Next.js 15

Ba tính năng này hay bị nhầm lẫn với nhau. Mình sẽ giải thích từng cái và quan trọng hơn - khi nào dùng cái nào.

Server Components

Server Components chạy hoàn toàn trên server. Chúng không có lifecycle hooks, không có state, không có event handlers. Đổi lại, bạn được: truy cập trực tiếp database, đọc file system, dùng API keys mà không sợ lộ ra client.

// Server Component - chạy trên server, không bao giờ xuống client
import { db } from '@/lib/database';

async function UserDashboard({ userId }) {
  // Truy cập DB trực tiếp, không cần API endpoint
  const user = await db.users.findUnique({ where: { id: userId } });
  const orders = await db.orders.findMany({ where: { userId } });

  return (
    <div>
      <h1>Xin chào, {user.name}</h1>
      <OrderList orders={orders} />
    </div>
  );
}

346842 Server Component không bao giờ xuất hiện trong bundle JS của browser

Streaming

Streaming cho phép Next.js gửi HTML từng phần xuống browser thay vì chờ toàn bộ page render xong. Kết hợp với Suspense, bạn có thể hiển thị skeleton loading trong khi data đang fetch.

import { Suspense } from 'react';

// Layout chính hiển thị ngay lập tức
export default function Dashboard() {
  return (
    <div>
      <Header /> {/* Render ngay */}
      <Suspense fallback={<StatsSkeleton />}>
        <Stats /> {/* Stream sau khi data sẵn sàng */}
      </Suspense>
      <Suspense fallback={<FeedSkeleton />}>
        <ActivityFeed /> {/* Stream độc lập */}
      </Suspense>
    </div>
  );
}

Thay vì người dùng nhìn màn hình trắng 2 giây, họ thấy layout ngay lập tức và nội dung "chảy" vào dần. Cải thiện Largest Contentful Paint (LCP) đáng kể.

Partial Prerendering (PPR)

Đây là tính năng mới nhất và thú vị nhất trong Next.js 15. PPR kết hợp static và dynamic rendering trong cùng một route.

Hình dung thế này: trang sản phẩm e-commerce có phần header, navigation, và mô tả sản phẩm - những thứ này không đổi. Nhưng giá, số lượng tồn kho, và gợi ý sản phẩm lại cần dynamic. Trước PPR, bạn phải chọn một trong hai: hoặc static toàn bộ, hoặc dynamic toàn bộ.

// next.config.js - bật PPR (experimental trong v15)
module.exports = {
  experimental: {
    ppr: true,
  },
};

// Trang sản phẩm với PPR
export default function ProductPage({ params }) {
  return (
    <div>
      {/* Phần này được prerender tĩnh */}
      <ProductInfo productId={params.id} />
      
      {/* Phần dynamic được stream sau */}
      <Suspense fallback={<PriceSkeleton />}>
        <DynamicPrice productId={params.id} />
      </Suspense>
    </div>
  );
}

PPR prerender shell tĩnh ngay lập tức (từ CDN, cực nhanh), sau đó stream phần dynamic. Time to First Byte (TTFB) của phần static đạt 4-10x improvement in TTFB, with PPR static shell achieving ~20-80ms (often sub-50ms) compared to full SSR at ~300-800ms (Next.js Partial Prerendering documentation and performance reports 2024).


Caching trong Next.js 15: thay đổi quan trọng bạn cần biết

Nếu bạn đang dùng Next.js 14 và chuẩn bị lên 15, đây là phần dễ gây đau đầu nhất.

Next.js 15 thay đổi hành vi cache mặc định. Trong v14, fetch() mặc định cache kết quả. Trong v15, mặc định là no-store - tức là không cache.

// Next.js 14: fetch mặc định cache
const data = await fetch('https://api.example.com/products');
// Tương đương: fetch(..., { cache: 'force-cache' })

// Next.js 15: fetch mặc định KHÔNG cache
const data = await fetch('https://api.example.com/products');
// Tương đương: fetch(..., { cache: 'no-store' })

// Muốn cache trong v15? Phải khai báo tường minh
const data = await fetch('https://api.example.com/products', {
  next: { revalidate: 3600 } // Cache 1 giờ
});

// Hoặc dùng unstable_cache cho function bất kỳ
import { unstable_cache } from 'next/cache';

const getCachedProducts = unstable_cache(
  async () => db.products.findMany(),
  ['products-list'],
  { revalidate: 3600 }
);

346843 Breaking change quan trọng nhất khi lên v15 - kiểm tra kỹ trước khi deploy

Thay đổi này hợp lý về mặt triết lý - bạn phải chủ động quyết định cache thay vì bị surprise khi data cũ. Nhưng nếu migrate không kiểm tra, bạn sẽ thấy API requests tăng đột biến và performance giảm.

Ba loại cache trong Next.js 15:

  • Request Memoization: tự động deduplicate fetch requests trong cùng một render tree
  • Data Cache: persistent cache qua các requests (dùng revalidate)
  • Full Route Cache: cache HTML và RSC payload của static routes

Mình khuyên bạn nên audit toàn bộ fetch() calls trong project trước khi nâng cấp. Tạo một danh sách: fetch nào cần cache, fetch nào cần fresh data.


Migration checklist: từ Pages Router sang App Router

Mình đã migrate khá nhiều dự án qua App Router rồi. Dưới đây là những gì thực sự quan trọng.

346844 Checklist migration - đừng bỏ qua bước audit caching

Bước 1: Đánh giá trước khi bắt đầu

Không phải project nào cũng nên migrate ngay. Hỏi mình những câu này trước:

  • Project có đang production không? Nếu có, tính downtime risk.
  • Team có hiểu Server Components chưa? Migration mà team chưa nắm concept sẽ tạo ra code lai không nhất quán.
  • Có bao nhiêu pages? Dưới 20 pages thì migrate khả thi trong 1-2 sprint. Trên 50 pages thì cần kế hoạch phased.

Bước 2: Coexistence - chạy song song

Next.js hỗ trợ Pages Router và App Router cùng tồn tại. Đây là lợi thế lớn nhất khi migrate.

/app          ← App Router routes mới
  /dashboard
  /settings
/pages        ← Pages Router cũ vẫn hoạt động
  /legacy-feature
  /old-page

Mình thường migrate theo thứ tự: pages ít traffic nhất trước, trang chủ và trang core nhất sau. Nếu có bug, impact nhỏ hơn.

Bước 3: Những thứ cần refactor

Dưới đây là danh sách thực tế những gì phải đổi:

// CŨ: getServerSideProps
export async function getServerSideProps({ params }) {
  const data = await fetchData(params.id);
  return { props: { data } };
}

// MỚI: async Server Component
async function Page({ params }) {
  const data = await fetchData(params.id);
  return <Component data={data} />;
}

// CŨ: next/router
import { useRouter } from 'next/router';
const router = useRouter();
router.push('/dashboard');

// MỚI: next/navigation (chỉ dùng trong Client Component)
import { useRouter } from 'next/navigation';

// MỚI: redirect trong Server Component
import { redirect } from 'next/navigation';
redirect('/dashboard');

Bước 4: Các lỗi hay gặp

"useState is not defined" - bạn đang dùng state trong Server Component. Thêm 'use client' vào đầu file.

"Event handlers cannot be passed to Client Component props" - bạn đang truyền function từ Server sang Client Component. Đưa handler vào Client Component.

"cookies/headers called outside of request scope" - thường gặp khi dùng middleware sai context.

Tải checklist đầy đủ tại đây: Tải migration checklist


Performance thực tế: con số cụ thể

Lý thuyết hay nhưng dev cần số liệu. Mình tổng hợp những benchmark đáng chú ý.

Next.js 15 cải thiện Turbopack (bundler mới thay Webpack) đáng kể. Trong môi trường development, hot reload nhanh hơn 96.3% faster hot reload (Fast Refresh) with Turbopack vs Webpack (Next.js Official Blog / Vercel Benchmark 2024) so với Webpack. Đây không phải số nhỏ - khi bạn đang debug và save file liên tục, tốc độ reload ảnh hưởng trực tiếp đến productivity.

346845 Turbopack giúp dev experience mượt hơn nhiều, đặc biệt với project lớn

Về production performance:

  • Bundle size: Server Components không được đưa vào JS bundle của client. Một page dùng nhiều Server Components có thể giảm bundle size 40-70% reduction in client bundle size with Next.js Server Components; concrete benchmark: 482KB → 89KB (81.5% reduction) (Next.js 15 Server Components Benchmark 2024)
  • TTFB với PPR: static shell được serve từ CDN ngay lập tức, dynamic parts stream sau
  • LCP: Streaming với Suspense giúp browser paint nội dung quan trọng sớm hơn

Edge Runtime - khi nào nên dùng?

Edge Runtime là môi trường chạy ở CDN edge nodes (Vercel Edge Network, Cloudflare Workers...) thay vì origin server. Latency thấp hơn vì code chạy gần người dùng hơn.

Nhưng Edge Runtime có giới hạn: không dùng được Node.js APIs, không dùng được phần lớn npm packages native, không kết nối trực tiếp database truyền thống.

// Middleware chạy trên Edge
export const config = {
  runtime: 'edge',
};

export async function middleware(request) {
  // Phù hợp cho: auth check, geo redirect, A/B testing
  // KHÔNG phù hợp: database queries, file system
  const token = request.cookies.get('auth-token');
  
  if (!token) {
    return Response.redirect(new URL('/login', request.url));
  }
}

Dùng Edge Runtime cho middleware, API routes nhẹ (auth, redirect). Giữ Node.js runtime cho database queries và business logic nặng.


Nên nâng cấp không? Quyết định dựa trên từng trường hợp

Câu trả lời thẳng thắn: không phải lúc nào cũng nên.

346846 Quyết định nâng cấp phụ thuộc vào context của project, không phải hype

Nên migrate ngay nếu:

  • Project mới hoàn toàn - không có lý do gì để bắt đầu với Pages Router năm 2025
  • Team đủ thời gian học App Router đúng cách (ít nhất 1-2 tuần)
  • Project cần SEO tốt và performance cao (e-commerce, content site, landing page)
  • Đang gặp vấn đề performance với client-side rendering nặng

Nên chờ hoặc không migrate nếu:

  • Project đang stable, không có performance issues - "if it ain't broke"
  • Team nhỏ, deadline gấp - technical debt từ migration vội vàng sẽ đắt hơn
  • App là SPA thuần với nhiều real-time interactions - Client Components vẫn là lựa chọn tốt
  • Nhiều third-party libraries chưa tương thích Server Components

Với project mới từ đầu

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

# Cấu trúc thư mục App Router
my-app/
  app/
    layout.tsx      ← Root layout (Server Component)
    page.tsx        ← Trang chủ
    globals.css
  components/
    ui/             ← UI components (có thể client)
    server/         ← Server-only components
  lib/
    db.ts           ← Database connection

Mình thường tổ chức theo convention: folder components/server/ cho Server Components, components/ui/ cho Client Components. Giúp team biết ngay context khi đọc code.

Một điểm nữa: nếu bạn đang học React và chưa vững foundation, mình gợi ý xem qua khóa ReactJS miễn phí trên F8 trước - hiểu rõ React sẽ giúp bạn tiếp thu Server Components nhanh hơn nhiều.


Takeaways:

  • App Router là tương lai, nhưng Pages Router vẫn được hỗ trợ lâu dài
  • Next.js 15 thay đổi cache mặc định - cần kiểm tra kỹ trước khi nâng cấp
  • PPR và Streaming là lý do chính đáng nhất để migrate
  • Migrate phased (coexistence) an toàn hơn big bang migration

Bạn đang dùng Next.js phiên bản nào? Drop comment cho mình biết team quyết định thế nào.

Trang web dùng cookie để hoạt động và đo hiệu quả quảng cáo. Xem chính sách bảo mật