Next.js là gì? Tại sao React SPA không còn đủ nữa
Từ empty div SEO-unfriendly đến full-stack framework với SSR, file-based routing và Server Components hành trình chuyển từ React SPA sang Next.js.
Nguyễn Nhật Long
@nguyennhatlong1303
Nếu bạn đã làm việc với React một thời gian, chắc chắn đã từng gặp cái cảm giác này: app chạy ngon trên local, UX mượt mà, nhưng khi SEO team ping lên hỏi "sao Google không index được trang sản phẩm?", bạn bắt đầu nhận ra React SPA có những giới hạn mà không thể bỏ qua.
Đây là lúc Next.js xuất hiện. Và mình không nói theo kiểu marketing mình đã dùng cả hai trong production, và sự khác biệt là rất thật.
Vấn đề thực sự của React SPA
Khi Googlebot crawl một React SPA, nó nhận được cái này:
1<!DOCTYPE html>2<html>3 <body>4 <div id="root"></div>5 <script src="/static/js/main.chunk.js"></script>6 </body>7</html>
Một cái div trống. Content của bạn nằm trong JS bundle, và dù Googlebot ngày nay có thể render JS, nó vẫn không đáng tin cậy bằng HTML thuần. Với các trang e-commerce hay blog cần SEO nghiêm túc, đây là vấn đề tử huyệt.
Ngoài SEO, còn có vấn đề initial load performance. Flow của React SPA là:
- Browser download toàn bộ JS bundle (có thể vài MB)
- Parse và execute JS
- React render UI
- Fetch data từ API
- Re-render với data thật
User nhìn thấy màn hình trắng hoặc skeleton loader trong suốt quá trình đó. First Contentful Paint (FCP) chậm, Largest Contentful Paint (LCP) càng chậm hơn. Core Web Vitals của bạn sẽ không đẹp.
Routing cũng là một điểm đau. React không có built-in routing bạn phải chọn giữa React Router, TanStack Router, hay tự implement. Không có gì sai với điều này, nhưng nó là thêm một thứ phải config và maintain.
Data fetching thì còn thủ công hơn nữa. Mỗi component tự fetch data theo cách riêng, không có caching strategy nhất quán, dễ dẫn đến waterfall requests.

Next.js giải quyết đúng chỗ đau
Next.js về cơ bản là React, nhưng với một layer infrastructure được build sẵn cho production. Thay vì bạn phải tự lắp ghép routing, SSR, optimization Next.js làm hết.
File-based routing là thứ mình thích nhất khi mới bắt đầu dùng. Bạn tạo file app/about/page.tsx → route /about tự động tồn tại. Không cần config gì thêm. Muốn dynamic route? app/blog/[slug]/page.tsx là xong. Cái này nghe đơn giản nhưng khi project lớn lên, việc routing được enforce bởi file structure giúp codebase dễ navigate hơn rất nhiều.
API Routes cho phép bạn viết backend endpoints ngay trong cùng project. File app/api/users/route.ts trở thành endpoint /api/users. Với nhiều project vừa và nhỏ, điều này loại bỏ hẳn nhu cầu có một backend server riêng.
Image optimization với next/image là một trong những thứ ít được nói đến nhưng cực kỳ có giá trị trong thực tế. Nó tự động lazy load, convert sang WebP/AVIF, resize theo viewport, và prevent layout shift. Mình đã từng optimize một trang từ 8MB images xuống còn 800KB chỉ bằng cách swap <img> thành <Image> từ Next.js.
App Router cách Next.js đang đi về tương lai
Next.js hiện có hai routing system tồn tại song song, và đây là điểm nhiều người mới bị confuse:
Pages Router không phải là xấu nó đã serve rất tốt trong nhiều năm và vẫn hoạt động hoàn hảo. Nhưng App Router là hướng đi của Next.js, và nếu bạn bắt đầu project mới, nên dùng App Router.
| Pages Router | App Router | |
|---|---|---|
| Thư mục | `pages/` | `app/` |
| Default rendering | Client-side | Server Component |
| Data fetching | `getServerSideProps`, `getStaticProps` | `async/await` trong component |
| Streaming | Không có | Có (Suspense) |
| Server Actions | Không có | Có |
| Status | Legacy, vẫn được support | Hiện tại và tương lai |
Cái hay của App Router là Server Components by default. Trong Pages Router, mọi component đều run trên client. Trong App Router, component mặc định chạy trên server nghĩa là bạn có thể query database, đọc file system, access secrets trực tiếp trong component mà không cần tạo API endpoint:
1// app/dashboard/page.tsx2// Component này chạy trên SERVER không có JS nào được ship xuống browser3async function DashboardPage() {4 // Query thẳng database, không cần API call5 const users = await db.query('SELECT * FROM users LIMIT 10');67 return (8 <div>9 {users.map(user => (10 <UserCard key={user.id} user={user} />11 ))}12 </div>13 );14}
Khi cần interactivity (event handlers, useState, useEffect), bạn opt-in bằng 'use client' ở đầu file:
1'use client';23import { useState } from 'react';45export function SearchBar() {6 const [query, setQuery] = useState('');78 return (9 <input10 value={query}11 onChange={e => setQuery(e.target.value)}12 placeholder="Tìm kiếm..."13 />14 );15}
Theo kinh nghiệm của mình, pattern tốt nhất là giữ Server Components cho data fetching và layout, Client Components cho interactive UI elements. Đừng vội 'use client' toàn bộ app bạn sẽ mất đi phần lớn lợi ích của App Router.
Các rendering strategy và khi nào dùng cái gì
Đây là phần nhiều người bị overwhelm vì Next.js có quá nhiều lựa chọn. Mình sẽ breakdown thực tế:
Static (SSG) render tại build time, output là HTML file thuần. Dùng cho blog posts, landing pages, documentation những thứ không thay đổi theo từng request. Nhanh nhất, có thể serve qua CDN.
Dynamic (SSR) render mỗi request trên server. Dùng cho dashboard, trang cần user-specific data, real-time content. Chậm hơn static nhưng luôn fresh.
ISR (Incremental Static Regeneration) đây là cái mình thấy hay nhất của Next.js. Trang được generate static, nhưng sau một khoảng thời gian (ví dụ 60 giây), Next.js sẽ regenerate lại nền. User luôn nhận HTML nhanh, nhưng content vẫn được update định kỳ.
1// ISR: revalidate mỗi 60 giây2export const revalidate = 60;34async function ProductPage({ params }) {5 const product = await fetchProduct(params.id);6 return <ProductDetail product={product} />;7}
Streaming render progressive với Suspense. Phần nào ready trước thì ship xuống browser trước, không cần chờ toàn bộ page.
1import { Suspense } from 'react';23export default function Page() {4 return (5 <div>6 <Header /> {/* Render ngay */}7 <Suspense fallback={<Skeleton />}>8 <SlowDataComponent /> {/* Stream sau khi ready */}9 </Suspense>10 </div>11 );12}
Setup project và chạy thử
Đủ lý thuyết rồi, làm thực tế thôi:
1npx create-next-app@latest my-app
CLI sẽ hỏi một loạt câu hỏi. Mình thường chọn: TypeScript ✓, ESLint ✓, Tailwind ✓, App Router ✓, Turbopack ✓.
Structure sau khi tạo xong:
1my-app/2├── app/3│ ├── layout.tsx # Root layout, wrap toàn bộ app4│ ├── page.tsx # Route "/"5│ └── globals.css6├── components/ # Shared UI components7├── lib/ # Utilities, helpers, db client8├── types/ # TypeScript type definitions9└── public/ # Static files (images, fonts)
Thêm một page mới đơn giản như sau:
1// app/about/page.tsx2export default function AboutPage() {3 return (4 <main>5 <h1>Về chúng tôi</h1>6 <p>Next.js app đầu tiên của mình!</p>7 </main>8 );9}
File này tự động trở thành route /about. Không cần thêm gì vào router config.
Navigate giữa các pages dùng Link component thay vì <a> tag để có client-side navigation:
1import Link from 'next/link';23export default function Header() {4 return (5 <nav>6 <Link href="/">Home</Link>7 <Link href="/about">About</Link>8 <Link href="/blog">Blog</Link>9 </nav>10 );11}
Chạy dev server:
1npm run dev
Next.js 15 dùng Turbopack làm default bundler thay Webpack. HMR gần như instant thay đổi code, browser update trong vòng chưa đến 1 giây. Cái này so với Webpack của React CRA ngày xưa là một trời một vực.

Một vài điểm anh em lưu ý khi mới bắt đầu
Thứ nhất, đừng 'use client' tất cả mọi thứ vì quen tay từ React. Mình thấy nhiều người mới chuyển sang Next.js vẫn viết code như React SPA thuần, bỏ qua hoàn toàn Server Components. Như vậy bạn đang dùng Next.js nhưng không tận dụng được gì.
Thứ hai, app/ và pages/ có thể coexist trong cùng project. Nếu bạn đang migrate một project Pages Router cũ, bạn không cần migrate tất cả một lúc làm dần từng route một hoàn toàn ổn.
Thứ ba, hiểu rõ rendering strategy trước khi chọn. Nhiều người mặc định dùng SSR cho tất cả mọi thứ trong khi 80% trang của họ hoàn toàn có thể là static. Static nhanh hơn, rẻ hơn, scale tốt hơn.
Next.js về bản chất là React bạn không cần học lại từ đầu. Nhưng nó thêm vào đúng những thứ mà React cố tình không bao gồm: rendering strategy, routing, optimization. Với mình, đây là lý do Next.js trở thành default choice cho hầu hết project mới thay vì phải tự assemble từng mảnh.
Nguyễn Nhật Long
@nguyennhatlong1303Nguyễn Nhật Long is a Senior Frontend Engineer and Frontend Team Leader with 7 years of experience building real-time fintech platforms. Specializing in React, Next.js, TypeScript, and React Native, shipping 10+ products across Web, Mobile, Telegram Mini-Apps, and Web3.
Thấy hay? Chia sẻ cho bạn bè!
