Hub

Next.js

Bài này nói về Next.js 15 App Router cho phỏng vấn mức senior, map từ Nuxt: Next thêm gì cho React, khi nào Vite React vẫn thắng, bản đồ tư duy Nuxt và chỗ nó gãy, RSC khác SSR, file App Router, fetch, navigation, và middleware Edge. Dành cho engineer đã ship Vue 3 / Nuxt, giờ cần một quyết định dưới ràng buộc thật, không phải tour tính năng.

Đọc 21.1–21.5 trước (nó là gì, khác React, map Nuxt, RSC, App Router). Người phỏng vấn đào 'use client' và “RSC có phải SSR?” nhiều hơn trivia thư mục. Rồi 21.6–21.8 cho fetch (Next 15 mặc định không cache), Link / loading.tsx, và middleware không phải authorization. Nêu rõ ranh giới process và tương đương Nuxt trong một câu.


Tóm tắt: Next.js 15 là framework React cho production: file routing của App Router, Server Component mặc định, fetch trên server, Route Handler, và một đích deploy. React là thư viện UI. Nuxt là tương đương gần nhất: framework Vue cho production trên Nitro.

Vì sao quan trọng: “SSR cho React” là cách junior hay chốt. Người phỏng vấn muốn ranh giới process: cái gì chạy trên server, JS nào được gửi, authz nằm ở đâu, và bạn còn chọn Pages Router năm 2026 không (không: App Router là mặc định).

  1. Cách hoạt động: app/page.tsx là Server Component trừ khi bạn thêm 'use client'. Nó có thể await fetch (Next 15: không cache trừ khi opt-in). app/layout.tsx bọc segment. app/api/**/route.ts là Route Handler.
  2. React không cho bạn: file router, layout lồng nhau, data phía server mà không cần BFF riêng, loading.tsx / error.tsx, helper ảnh/font/metadata, và middleware.ts Edge. Đó mới là framework.
  3. Tương đương Nuxt: pages/ + layouts/ + server/api + useAsyncData. Next tách client/server bằng directive, không dùng tên file .client / .server. Xem bản đồ tư duy.
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return <html lang="en"><body>{children}</body></html>
}
// app/page.tsx: Server Component mặc định (không use client)
export default async function Page() {
  const res = await fetch('https://api.example.com/hello') // Next 15: không cache
  const data: { message: string } = await res.json()
  return <h1>{data.message}</h1>
}
// Nuxt: pages/index.vue + useAsyncData / $fetch trên server

Điều gì xảy ra: Page chạy trên server, fetch data, rồi gửi HTML kèm một payload RSC nhỏ. Data lần đầu không cần useEffect. Next 15 sẽ gọi API mỗi request trừ khi bạn truyền { cache: 'force-cache' } hoặc option revalidate. Nuxt sẽ fetch tương tự trong useAsyncData.

PhầnApp Router (15)Pages Router
Component mặc địnhServer ComponentTrang client
Dataawait fetch trong pagegetServerSideProps / getStaticProps
Mặc định 2026Có: học cái nàyLegacy; migrate

Góc nhìn senior

  • App Router là câu trả lời năm 2026. Pages Router chỉ để migrate, không phải greenfield mới.
  • Next không phải “React kèm SSR”. RSC có thể bỏ JS của component; SSR vẫn gửi HTML. Xem 21.4.
  • Authz nằm ở server, không phải middleware.ts một mình (21.8) và không phải guard layout phía client.
  • Nuxt 3/4 trên Nitro là chỗ tư duy quen thuộc. Đừng học Next như chưa từng ship một meta-framework.
  • Kiểm chứng: HTML trang đã có data ngay response đầu; Network không thấy thêm /hello từ trình duyệt cho first paint đó.

Lỗi hay gặp

  • Triệu chứng: “Next là SSR”. Nguyên nhân: trộn RSC với SSR. Cách xử lý: 21.4, RSC là ai chạy component.
  • Triệu chứng: fetch đầu thiếu trên prod. Nguyên nhân: tưởng cache mặc định như Next 14. Cách xử lý: Next 15 fetch không cache.
  • Triệu chứng: chọn Pages Router cho app mới. Nguyên nhân: blog cũ. Cách xử lý: App Router.
  • Triệu chứng: useEffect load trên app/page.tsx. Nguyên nhân: coi như Vite CRA. Cách xử lý: await trong Server Component.

Người phỏng vấn hỏi gì

Next cho bạn gì mà React không cho, và App Router hay Pages Router mới là mặc định 2026? Họ muốn một framework production, không phải “SSR cho React”, kèm tương đương Nuxt trong một câu.

Trả lời khi phỏng vấn

Next 15 là framework React cho production: App Router, Server Component mặc định, fetch trên server, Route Handler, và một đích deploy. React là thư viện. Mình không làm app mới trên Pages Router. Fetch trong page không cần useEffect; Next 15 không cache fetch đó trừ khi mình opt-in. Tương đương Nuxt là pages, layouts, server API, và useAsyncData. RSC không phải SSR. Đó là topic sau. Middleware không phải authorization. Mình map từng phần sang Nuxt trong một câu để người phỏng vấn nghe đây là transfer, không phải viết lại sự nghiệp.

Nhớ nhanh

  • Next 15 App Router là framework React cho production. React là thư viện.
  • Server Component mặc định. await fetch trong page. Next 15 fetch không cache.
  • Tương đương Nuxt: pages/ + layouts/ + server/api + useAsyncData.
  • Pages Router là legacy cho app mới. RSC không phải SSR, xem 21.4.

Câu hỏi nối tiếp thường gặp

  • App khác Pages năm 2026? App Router. Pages chỉ để migrate.
  • Next có full-stack? Tùy chọn. Route Handler / Action là BFF, không phải backend bắt buộc.
  • Nuxt useAsyncData? await fetch trong Server Component, hoặc Query client nếu data phải sống phía client.
  • Vì sao không useEffect trên page? Server đã có data ngay HTML đầu.

Tóm tắt: React là thư viện. Next.js là framework với ranh giới process: server chạy component và fetch mà trình duyệt không bao giờ thấy. Vite React trần thắng khi bạn không muốn ranh giới đó: dashboard đã login, không SEO, BFF sẵn.

Vì sao quan trọng: “Chỉ dùng Vite + React được không?” là câu senior thật. Next tốn cache, kỷ luật use client, và ràng buộc host. SPA với BFF đơn giản hơn khi cái giá đó không đáng trả. Nuxt vs Vite Vue là cùng ngã rẽ.

  1. Cách hoạt động: Vite React: server chỉ host file tĩnh. Mọi data là fetch phía client hoặc BFF. Next: app/page.tsx đọc cookie, secret, và DB (cẩn thận) trên server được. use client là ranh giới, không phải tối ưu.
  2. Chọn Next khi muốn HTML từ server, layout lồng nhau, hoặc giữ token khỏi client. Chọn Vite React khi SEO không quan trọng, API đã có, và team không muốn semantics cache của RSC. Tương đương Nuxt: Nuxt vs vite create vue, cùng ngã rẽ.
  3. Next có thể full-stack (Route Handler, Server Action). Nó không thay API thật cho client khác. Xem khi nào chọn Next ở 21.11.
// Vite React: data là chuyện của client
export function Dashboard() {
  const [rows, setRows] = useState<Row[] | null>(null)
  useEffect(() => { void api.rows().then(setRows) }, [])
  if (!rows) return <p>Loading…</p>
  return <Table rows={rows} />
}
// Next App Router: cùng UI, server đã có rows
// export default async function Page() {
//   const rows = await db.rows() // không gửi tới trình duyệt như một fetch
//   return <Table rows={rows} />
// }

Điều gì xảy ra: Vite vẽ loading state, rồi hydrate thành rows. Next gửi được HTML của table ngay response đầu và bỏ fetch phía client. Đó là ranh giới process. Nếu Table cần click, nó là Client Component con: use client trên Table, không phải trên page đã load data.

FeatureVite ReactNext App Router
RoutingBạn phụ trách react-routerFile-based app/
Render mặc địnhCSRRSC + SSR/SSG tùy chọn
DataQuery / effectServer fetch + Query client
Khi Vite thắngApp đã login, BFF sẵnSEO / giữ secret khỏi client

Góc nhìn senior

  • Nói “Next là full-stack” mà không nêu rõ API cho mobile là miss. Route Handler là tùy chọn.
  • use client khắp nơi xóa lý do bạn chọn Next. Đẩy ranh giới xuống.
  • Hosting: Node vs Edge vs static export. SPA Vite là một bucket tĩnh. Next thì không, trừ khi export.
  • Nuxt vs Vite Vue là cùng cuộc nói chuyện. Đừng giả Next là ma thuật riêng.
  • 21.11 là checklist quyết định. Topic này là đường chia thư viện vs framework.

Lỗi hay gặp

  • Triệu chứng: SPA Vite viết lại sang Next “cho SEO” với use client mọi page. Nguyên nhân: không có ranh giới process. Cách xử lý: fetch trên server.
  • Triệu chứng: secret trong NEXT_PUBLIC_. Nguyên nhân: coi Next như env Vite. Cách xử lý: 21.12 leaking-server-secrets.
  • Triệu chứng: “React 19 gồm framework”. Nguyên nhân: trộn React với Next. Cách xử lý: React vẫn là thư viện.
  • Triệu chứng: Next cho tool nội bộ chỉ Chrome. Nguyên nhân: mốt. Cách xử lý: Vite React kèm BFF sẵn.

Người phỏng vấn hỏi gì

Chỉ dùng Vite và React được không, và Next có full-stack? Họ muốn ranh giới process, khi nào SPA thắng, và tương đương Nuxt vs Vite Vue.

Trả lời khi phỏng vấn

React là thư viện. Next là framework với ranh giới process: server chạy component và fetch mà trình duyệt không bao giờ thấy. Mình chọn Vite React khi SEO không quan trọng, BFF đã có, và mình không muốn semantics cache RSC. Admin đã login là case thắng thường gặp. Mình chọn Next khi muốn HTML từ server hoặc giữ token khỏi client. use client là ranh giới mình đẩy xuống, không phải mặc định trên trang. Nuxt khác Vite Vue là cùng ngã rẽ. Next full-stack là tùy chọn; mobile vẫn cần API thật. Checklist quyết định là topic sau.

Nhớ nhanh

  • React là thư viện. Next là framework với ranh giới process phía server.
  • Vite React thắng cho app đã login, đã có BFF, không SEO.
  • use client là ranh giới. Gắn nó lên mọi trang thì mất lý do dùng Next.
  • Nuxt khác Vite Vue là cùng ngã rẽ. 21.11 là checklist.

Câu hỏi nối tiếp thường gặp

  • Next có full-stack? Tùy chọn. Client khác vẫn cần API.
  • Khi nào SPA thắng? Không SEO, BFF sẵn, team không muốn RSC.
  • Tương đương Nuxt? Nuxt khác vite create vue. Cùng ngã rẽ.
  • React 19 compiler? Vẫn là thư viện. Nó không thêm file router.

Tóm tắt: Dùng map này để chuyển thói quen Nuxt sang Next 15 App Router. Rồi học ba bẫy chỗ so sánh gãy: middleware Edge, cache fetch, và use client khác file .client. Chỉ map tên thư mục là chưa đủ.

Vì sao quan trọng: “Tương đương Nuxt của X là gì?” là kiểu phỏng vấn chuyển. Senior nêu runtime mặc định, không phải list thư mục. Middleware chạy Node trong Nuxt thì sang Next là Edge. useFetch không phải await fetch. .client.vue không phải use client.

  1. Cách hoạt động (bẫy 1, middleware): Nuxt middleware/ là Node, theo từng route, dùng full server được. Next middleware.ts là Edge, một file, API hạn chế, và không phải authorization. Xem 21.8.
  2. Bẫy 2 (data): Nuxt useFetch / useAsyncData là composable có refetch. Next await fetch trong Server Component là một request (Next 15: mặc định không cache). Data live trên client vẫn là TanStack Query. cookies() làm route dynamic.
  3. Bẫy 3 (ranh giới client): Nuxt .client / .server là tên file. Next use client là ranh giới module: cả file và import của nó thành client. Bạn không truyền hàm từ Server Component vào được. server-only thay .server.
// Bẫy 3: file này là module Client Component
'use client'
export function PayButton({ onPay }: { onPay: () => void }) {
  return <button onClick={onPay}>Pay</button>
}
// Trang server không truyền hàm inline vào PayButton được.
// Truyền id serialise được, hoặc đưa child xuống dưới ranh giới client.
// Bẫy 2: await fetch('...') trong Server Component không phải useFetch.
// Next 15: không cache trừ khi cache: 'force-cache' / revalidate.

Điều gì xảy ra: Đánh dấu file use client thì JS của nó được gửi đi. Trang server cố truyền () => pay() như prop sẽ fail serialise. Nuxt sẽ dùng island .client thay vì directive. Giữ bảng map; nhớ ba bẫy.

NuxtNext.js (App Router)
pages/app/**/page.tsx
layouts/layout.tsx lồng nhau
middleware/ (Node)middleware.ts (Edge, khác)
server/apiapp/api/**/route.ts
useFetch / useAsyncDataawait trong Server Component; Query phía client nếu live
refreshNuxtDatarevalidatePath / revalidateTag
definePageMetaconvention segment / dynamic / cache API
Nitro presetsRuntime Node/Edge + host adapter
.client / .serverranh giới use client; import server-only

Góc nhìn senior

  • Bảng là cheat sheet. Phỏng vấn chấm ba bẫy, nhất là middleware Edge.
  • Next 15 fetch không cache. Đừng mang chuyện “useFetch Nuxt đã cache” sang mà không nêu rõ chỗ opt-in.
  • definePageMeta({ middleware }) không có one-liner. Dùng matcher middleware.ts kèm redirect phía server.
  • Payload RSC không phải payload Nuxt. Đừng chờ window.__NUXT__ tồn tại.
  • Nói Vue/Nuxt trước, rồi mới mapping Next. Chuyển, đừng reboot.

Lỗi hay gặp

  • Triệu chứng: middleware kiểu Nuxt đụng API Node trên Edge. Nguyên nhân: analogy. Cách xử lý: 21.8; giữ Node trong Route Handler.
  • Triệu chứng: chờ refetch useFetch trong Server Component. Nguyên nhân: composable khác request. Cách xử lý: Query phía client.
  • Triệu chứng: use client trên page.tsx vì một button. Nguyên nhân: thói quen tên file. Cách xử lý: tách button.
  • Triệu chứng: truyền hàm qua prop xuyên ranh giới. Nguyên nhân: serialise. Cách xử lý: id, hoặc để cùng phía client.

Nhà tuyển dụng hay hỏi gì

Tương đương Nuxt của X là gì, và useFetch để đâu? Họ muốn runtime mặc định, ba bẫy, không phải list đồng nghĩa thư mục.

Trả lời khi phỏng vấn

Mình giữ một bảng map và mở bằng ba bẫy. Middleware Nuxt là Node theo từng route; middleware Next là Edge và không phải authorization. useFetch là composable; await fetch trong Server Component là một request, và Next 15 không cache trừ khi mình opt-in. Data live trên client vẫn là Query. use client là ranh giới module, không phải tên file .client. Mình không truyền hàm từ Server Component sang được. pages map sang app page, layouts sang layout lồng, server API sang route handler. Mình chuyển từ Nuxt; mình không reboot model trong đầu.

Nhớ nhanh

  • Giữ bảng map. Phỏng vấn chấm ba bẫy, không phải tên thư mục.
  • Middleware Nuxt là Node. middleware.ts của Next là Edge và không phải authz.
  • useFetch ≠ await fetch. Next 15 fetch mặc định không cache.
  • use client là ranh giới module. .client từng là tên file.

Câu hỏi nối tiếp thường gặp

  • useFetch để đâu? Server: await fetch. Live trên client: Query. Không phải drop-in.
  • Vì sao không truyền hàm vào Client Component? Payload RSC là giá trị serialise được.
  • refreshNuxtData? revalidatePath / revalidateTag trên server sau mutation.
  • Nitro presets? Runtime Next kèm host adapter. Không phải list preset 1:1.

Tóm tắt: SSR gửi HTML cho request này. SSG prerender lúc build. CSR/SPA render trên trình duyệt. RSC (React Server Components) chạy component trên server và không gửi JS của nó. Đó không phải SSR. Nuxt có SSR/SSG/SPA; không có RSC.

Vì sao quan trọng: “RSC có phải SSR?” là câu Next bị nhầm nhiều nhất. SSR là HTML cho paint đầu. RSC là ai chạy component và JS của nó có được tải không. Bạn SSR một Client Component được. Bạn dùng RSC mà không cần HTML nếu chỉ stream payload flight. Chọn theo data cần mới cỡ nào và ai được thấy data.

  1. Cách hoạt động: SSG: marketing, docs, ít đổi; stale đến lúc rebuild. ISR / revalidateTag: catalog; cửa sổ stale. SSR (dynamic): theo request, cookie, cá nhân hóa; đừng cache HTML của user dưới public key.
  2. CSR / SPA island: dashboard sau login, không SEO. Bạn chịu trách nhiệm chuyện loading. RSC: composition mặc định của App Router: data và UI trên server, không JS component cho node đó. Nó không interactive và không phải chiến lược cache.
  3. Tương đương Nuxt: ssr: true / routeRules ISR / mode SPA. Không có RSC. Thí nghiệm server component của Nuxt không phải mặc định Next. Partial prerender (shell tĩnh kèm lỗ dynamic) vẫn đang đổi. Nói bức tranh đó, rồi đối chiếu docs hiện tại.
// RSC: file này không use client, không gửi JS component
export default async function Product({ id }: { id: string }) {
  const product = await db.product(id) // ở lại trên server
  return (
    <section>
      <h1>{product.title}</h1>
      <AddToCart id={product.id} /> {/* island client */}
    </section>
  )
}
// SSR vẫn gửi HTML cho Product. RSC bỏ JS của Product.
// AddToCart.tsx bắt đầu bằng use client, JS đó thì được gửi.

Điều gì xảy ra: Server đọc DB, render HTML của Product (SSR), và không gửi JS component của Product (RSC). AddToCart là island client, hydrate sau. Nhầm hai câu đó là miss. Nuxt sẽ SSR trang và gửi hết JS component, trừ khi bạn tách island.

ModeKhiRàng buộc
SSGMarketing, docs, ít đổiStale đến lúc rebuild
ISR / revalidate theo tagCatalog, trang CMSCửa sổ stale / bug invalidation
SSR (dynamic)Theo request, cá nhân hóa, cookieChi phí server; đừng public-cache HTML của user
CSR / SPA islandDashboard tương tác, không SEOFirst content yếu hơn; bạn chịu trách nhiệm loading
RSCMặc định: data + UI trên server, không JS cho node đóKhông interactive; không phải chiến lược cache

Góc nhìn senior

  • RSC ≠ SSR. Nói đủ hai câu. Họ fail nếu bạn coi hai từ này là một.
  • cookies() / headers() biến trang lẽ ra static thành dynamic. Đó là độ tươi, không phải RSC.
  • Đừng cache HTML cá nhân hóa dưới public key của CDN. Đó là leak user.
  • Island SPA vẫn có chỗ trong Next cho dashboard nặng. Bạn không cần SSR hết mọi thứ.
  • Nuxt không có RSC mặc định. Đừng bịa 1:1. Nói “HTML server kèm island”.

Lỗi hay gặp

  • Triệu chứng: “RSC là SSR”. Nguyên nhân: cả hai chạy trên server. Cách xử lý: phân biệt HTML với JS được gửi.
  • Triệu chứng: user A thấy HTML của user B. Nguyên nhân: cache public của trang cookie. Cách xử lý: dynamic / private.
  • Triệu chứng: trang static gọi cookies(). Nguyên nhân: vô tình thành dynamic. Cách xử lý: truyền id như param.
  • Triệu chứng: map RSC sang server component Nuxt. Nguyên nhân: analogy. Cách xử lý: Nuxt không mặc định kiểu này.

Nhà tuyển dụng hay hỏi gì

ISR khác SSR khác SSG, khi nào dùng? Partial prerender là gì? RSC có phải SSR? Họ muốn độ tươi, ai được thấy data, chỗ cắt RSC/SSR, và tương đương Nuxt.

Trả lời khi phỏng vấn

SSR gửi HTML cho request này. SSG prerender lúc build. CSR render trên trình duyệt. RSC chạy component trên server và không gửi JavaScript của component đó. Đó không phải SSR. Mình SSR được một island client, và RSC một node data không bao giờ hydrate. Mình chọn theo độ tươi và ai được thấy data: SSG cho docs, revalidate theo tag cho catalog, SSR dynamic cho trang cookie, island SPA cho dashboard. Nuxt có SSR và ISR qua route rules; không có RSC làm mặc định. Partial prerender là shell tĩnh với lỗ dynamic. Mình nêu rõ bức tranh đó, rồi đối chiếu docs hiện tại.

Nhớ nhanh

  • RSC ≠ SSR. SSR là HTML. RSC là ai chạy component và JS của nó có gửi không.
  • Chọn theo độ tươi và ai được thấy data, không theo bốn acronym.
  • Đừng public-cache HTML cá nhân hóa. cookies() làm route dynamic.
  • Nuxt: SSR/SSG/SPA. Không có RSC mặc định. Nói HTML server kèm island.

Câu hỏi nối tiếp thường gặp

  • RSC có phải SSR? Không. SSR là HTML. RSC là không JS component cho node đó. Chúng thường đi cùng.
  • PPR là gì? Shell tĩnh kèm lỗ dynamic được stream. Tên hay đổi: mô tả bức tranh.
  • Tương đương Nuxt của RSC? Không có 1:1 mặc định. HTML server kèm island.
  • Khi nào island SPA đúng trong Next? Dashboard đã login, không SEO, tương tác nặng.

Tóm tắt: App Router lấy folder làm segment: page.tsx, layout.tsx lồng nhau, Server Component mặc định. use client là ranh giới module. Client Component không import được Server Component. Bạn không truyền được prop hàm xuyên ranh giới đó. Tương đương Nuxt: pages/ kèm layouts/ lồng nhau.

Vì sao quan trọng: “use client để đâu?” quyết định bạn còn server hay không. Gắn nó lên layout.tsx hoặc page.tsx vì một button thì cả cây bị gửi. Truyền onPay={() => ...} từ Server Component sẽ fail serialise. Island .client của Nuxt từng là tên file; đây là cắt graph.

  1. Cách hoạt động: app/dashboard/layout.tsx bọc app/dashboard/page.tsx và app/dashboard/settings/page.tsx. Layout persist khi chuyển giữa các trang đó. template.tsx thì remount. page.tsx là leaf.
  2. Default export không có use client là Server Component: await được, đọc secret, import module server-only. Gắn use client ở đầu file cần event hoặc hook. Tách button ra; đừng đánh dấu cả trang.
  3. Composition: Server Component import Client Component được (truyền prop serialise được). Client Component không import Server Component được. Tương đương Nuxt: child .client trong trang server, không phải trang client import component .server.
// app/dashboard/page.tsx: Server Component (mặc định)
import { PayButton } from './pay-button'
export default async function Page() {
  const invoice = await db.invoice() // chỉ chạy trên server
  return <PayButton invoiceId={invoice.id} /> // id serialise được
}
// pay-button.tsx
'use client'
export function PayButton({ invoiceId }: { invoiceId: string }) {
  return <button onClick={() => pay(invoiceId)}>Pay</button>
}
// Đừng: <PayButton onPay={() => pay(invoice.id)} /> từ trang server.

Điều gì xảy ra: Trang vẫn là Server Component. Chỉ PayButton gửi JS. Id đi xuyên ranh giới; hàm thì không. Gắn use client lên page.tsx sẽ đẩy cách dùng db sang client, hoặc fail build vì server-only.

FileVai tròNuxt
layout.tsxWrapper persist cho segmentlayouts/*.vue
page.tsxUI leaf cho URLpages/*.vue
template.tsxRemount lúc navigateKhông 1:1
File use clientHook, event, API trình duyệt.client.vue

Góc nhìn senior

  • Đẩy use client xuống. Trang load data nên vẫn là Server Component.
  • Client Component import Server Component là lỗi compile. Đảo lại: truyền children, hoặc truyền id.
  • Layout lồng nhau là lý do App Router tồn tại. Đừng dựng lại chrome dashboard ở mọi trang.
  • loading.tsx / error.tsx thuộc 21.7. Nền tảng ở đây là file và ranh giới.
  • Layout persist của Nuxt là cảm giác gần nhất. Directive khác tên file mới là bẫy (21.3).

Lỗi hay gặp

  • Triệu chứng: use client trên layout gốc. Nguyên nhân: một event handler. Cách xử lý: tách widget.
  • Triệu chứng: prop hàm từ server sang client. Nguyên nhân: serialise. Cách xử lý: id hoặc để cùng phía.
  • Triệu chứng: Client Component import db. Nguyên nhân: ranh giới quá cao. Cách xử lý: server-only trên db.
  • Triệu chứng: dựng lại shell mọi trang. Nguyên nhân: bỏ qua layout lồng. Cách xử lý: layout.tsx.

Nhà tuyển dụng hay hỏi gì

use client để đâu, vì sao không truyền được prop hàm từ Server Component, và Client Component có import Server Component được không? Họ cũng muốn nghe layout lồng và map Nuxt.

Trả lời khi phỏng vấn

App Router lấy folder làm segment: layout lồng persist, page là leaf, Server Component mặc định. Mình gắn use client trên file cần hook hoặc event, không phải trên trang load data. Client Component không import Server Component được. Mình không truyền hàm xuyên ranh giới. Mình truyền id serialise được. Tương đương Nuxt là pages kèm layouts lồng; .client từng là tên file, đây là cắt module. Tách button Pay ra. Gắn use client lên layout gốc là xóa server.

Nhớ nhanh

  • Server Component mặc định. use client là ranh giới module bạn đẩy xuống.
  • Client Component không import Server Component được. Truyền prop serialise được.
  • layout.tsx lồng persist chrome. page.tsx là leaf.
  • Nuxt: pages/ + layouts/. .client từng là tên file, không phải cắt graph.

Câu hỏi nối tiếp thường gặp

  • Client Component có import Server Component? Không. Đảo cây hoặc truyền children.
  • template.tsx khác layout.tsx? Template remount. Layout persist.
  • server-only để đâu? Trên module không được rò vào bundle client.
  • .client Nuxt trong trang server? Cảm giác gần nhất với file use client đã đẩy xuống.

Tóm tắt: Trong Server Component, await fetch (hoặc DB client). Song song Promise.all; nối await trong layout lồng là waterfall. Next 15 không cache fetch mặc định. Data live trên client là TanStack Query. Tương đương Nuxt: useAsyncData / $fetch trên server.

Vì sao quan trọng: Next 14 cache fetch mặc định; Next 15 thì không. Copy blog đời 14 sẽ bất ngờ trên prod. cookies() / headers() làm route dynamic. useEffect trên trang là thói quen Vite. await nối trong layout lồng là waterfall 21.12.

  1. Cách hoạt động: Server Component: const res = await fetch(url). Opt-in cache với { cache: 'force-cache' } hoặc { next: { revalidate: 60, tags: ['x'] } }. Mutate rồi revalidatePath / revalidateTag.
  2. Song song: bắt đầu cả hai promise, rồi await Promise.all. Nối: await a rồi await b cần a.id. Layout lồng, mỗi tầng await user rồi dashboard rồi widget, là waterfall tình cờ. Xem 21.12.
  3. Fetch phía client: TanStack Query (hoặc SWR) trong island use client cho search live, polling, UI optimistic. Đừng useEffect + useState cho data load đầu mà server đã có. Nuxt: useAsyncData trên server; plugin client cho live.
export default async function Page() {
  const userP = currentUser() // bắt đầu ngay
  const dashP = fetch('https://api.example.com/dash') // Next 15: không cache
  const [user, dashRes] = await Promise.all([userP, dashP])
  const dash = await dashRes.json()
  return <Dashboard user={user} dash={dash} />
}
// Nối chỉ khi b cần a.id. cookies() làm route dynamic.
// Search live trên client: Query trong island use client, không useEffect trên trang này.

Điều gì xảy ra: Cả hai request bắt đầu trước khi cái nào xong. await currentUser() rồi fetch nối sẽ dính waterfall. Next 15 đụng API mỗi lần trừ khi bạn opt-in cache. Nuxt useAsyncData kèm lazy / Promise.all là cùng ý.

CầnNext 15Nuxt
Data HTML đầuawait fetch / DB trong Server ComponentuseAsyncData / $fetch
Catalog đã cachecache: 'force-cache' / revalidate + tagsgetCachedData / routeRules
Client liveTanStack Query trong use clientQuery / useFetch trên client
Sau mutationrevalidatePath / revalidateTagrefreshNuxtData

Góc nhìn senior

  • fetch không cache của Next 15 là baseline. Nói luôn. Thói quen Next 14 là bug.
  • cookies() / headers() / searchParams trong layout làm cả cây dynamic.
  • Dedup: cùng URL fetch trong một render vẫn được dedup. Đó không phải TTL cache.
  • Authz trong fetch: server có session. Đừng chỉ giấu hàng ở Query phía client.
  • Waterfall await trong layout lồng: 21.12. Topic này là đường song song, happy path.

Lỗi hay gặp

  • Triệu chứng: catalog cũ sau CMS publish. Nguyên nhân: copy cache fetch kiểu Next 14. Cách xử lý: tags + revalidate, hoặc chấp nhận không cache.
  • Triệu chứng: useEffect load trên page.tsx. Nguyên nhân: thói quen Vite. Cách xử lý: await trên server.
  • Triệu chứng: layout chờ user, trang chờ dash, widget chờ items. Nguyên nhân: await nối tiếp. Cách xử lý: Promise.all / 21.12.
  • Triệu chứng: cookies() trên trang marketing. Nguyên nhân: dynamic tình cờ. Cách xử lý: đừng đọc cookie ở đó.

Người phỏng vấn hỏi gì

Fetch ở đâu trong App Router, useEffect khác fetch trên server khác Query, và cookies() làm gì với cache? Họ muốn Next 15 mặc định không cache và map Nuxt.

Trả lời khi phỏng vấn

Mình fetch trong Server Component bằng await fetch hoặc DB client. Next 15 không cache fetch đó trừ khi mình opt-in force-cache hoặc tag revalidate. Mình mở các promise độc lập rồi Promise.all chúng; await nối chỉ khi b cần a.id. cookies và headers làm route dynamic. Data load lần đầu không thuộc useEffect. Search live là TanStack Query trong island use client. Tương đương Nuxt là useAsyncData trên server và refreshNuxtData sau mutation. Waterfall layout lồng là topic lỗi production về sau.

Nhớ nhanh

  • Next 15: fetch mặc định không cache. Opt-in bằng force-cache / tag.
  • Promise.all song song. Nối chỉ khi cái sau cần id cái trước.
  • Data HTML đầu: await trên server. Client live: Query. Không useEffect trên trang.
  • Nuxt: useAsyncData / $fetch. cookies() làm Next dynamic.

Câu hỏi nối tiếp thường gặp

  • Next 15 đổi gì ở fetch? Nó không còn cache mặc định.
  • cookies() trong layout? Segment là dynamic. Đừng làm vậy trên shell marketing tĩnh.
  • Nuxt refreshNuxtData? revalidatePath / revalidateTag.
  • Khi nào Query trên client vẫn đúng? Search live, polling, UI optimistic.

Tóm tắt: Link navigate phía client và prefetch các route trong viewport. layout.tsx lồng thì persist. loading.tsx bọc segment trong Suspense. error.tsx là error boundary của segment đó. Tương đương Nuxt: NuxtLink, layout, và loading component bạn tự viết.

Vì sao quan trọng: <a href> thuần thì reload và vứt state layout. Prefetch cả admin trên landing marketing đốt băng thông. loading.tsx không chừa chiều cao thì dính CLS. error.tsx không reset lúc navigate thì kẹt shell gãy. Prefetch NuxtLink của Nuxt cũng cùng cơ chế.

  1. Cách hoạt động: <Link href="/dash"> prefetch khi hiện (mặc định). Dùng <a> cho external. scroll={false} khi phải giữ scroll. prefetch={false} trên link admin ít khi bấm.
  2. loading.tsx là fallback Suspense cho page.tsx của segment đó. Layout lồng vẫn hiện. Ghép với skeleton chừa chiều cao. error.tsx bắt lỗi render trong segment; cần reset bằng navigate hoặc retry.
  3. Navigate mềm giữ layout mounted. Reload đầy đủ remount mọi thứ, kể cả state client bạn tưởng là global. Tương đương Nuxt: NuxtLink + layouts/ persist; bạn vẫn phải thiết kế slot loading.
// app/dashboard/layout.tsx persist xuyên /dashboard/*
export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <section>
      <nav><Link href="/dashboard/settings">Settings</Link></nav>
      {children}
    </section>
  )
}
// app/dashboard/loading.tsx: fallback Suspense, chừa chiều cao
export default function Loading() {
  return <div className="dash-skel" aria-busy="true" />
}
// error.tsx: error boundary cho segment này, không phải cả app.

Điều gì xảy ra: Click Settings thì nav vẫn mounted. Trong lúc trang fetch, loading.tsx hiện skeleton đã chừa size để CLS không nhảy. Lỗi trong settings không unmount layout dashboard. <a> thuần sẽ reload shell.

File / APILàm gìNuxt
LinkNav client + prefetch viewportNuxtLink
layout.tsxPersist xuyên trang conlayouts/
loading.tsxFallback Suspense cho segmentBạn tự viết
error.tsxError boundary của segmenterror.vue / plugin

Góc nhìn senior

  • Prefetch vừa là đòn bẩy hiệu năng vừa tốn bandwidth. Tắt trên chunk admin quá nặng khi mạng 3G.
  • loading.tsx không phải spinner treo trong khoảng trống. Chừa chiều cao, không thì fail CLS (performance 9.2.5, nói bằng lời).
  • error.tsx khác global-error.tsx: cái sau thay HTML gốc. Đừng bắt mọi thứ ở root.
  • Parallel route / intercepting route là phần thêm. Nền tảng trước: Link, layout, loading, error.
  • Prefetch NuxtLink kèm layout là câu chuyển.

Lỗi hay gặp

  • Triệu chứng: state layout reset mỗi lần click. Nguyên nhân: <a href> thay vì Link. Cách xử lý: Link.
  • Triệu chứng: landing marketing tải JS admin. Nguyên nhân: prefetch mặc định của link admin đang hiện. Cách xử lý: prefetch={false} / chưa link nó.
  • Triệu chứng: spinner làm nhảy trang. Nguyên nhân: loading.tsx không có chiều cao. Cách xử lý: skeleton + aspect-ratio.
  • Triệu chứng: lỗi kẹt sau navigate. Nguyên nhân: không reset. Cách xử lý: reset() từ error boundary hoặc một Link.

Người phỏng vấn hỏi gì

Layout lồng hoạt động thế nào, loading.tsx khác <Suspense>, và navigation phía client khác reload đầy đủ? Họ muốn prefetch, CLS, error boundary, và tương đương NuxtLink.

Trả lời khi phỏng vấn

Link navigate phía client và prefetch route trong viewport. Layout lồng persist xuyên trang con; thẻ a thuần thì reload shell. loading.tsx là fallback Suspense của segment đó. Mình chừa chiều cao để CLS không nhảy. error.tsx là error boundary của segment, không phải cả app. Mình tắt prefetch trên link admin nặng, ít khi bấm. Tương đương Nuxt là NuxtLink kèm layout kèm slot loading mình vẫn phải thiết kế. Navigate mềm giữ state layout; reload đầy đủ thì không.

Nhớ nhanh

  • Link prefetch. layout.tsx lồng thì persist. <a> reload shell.
  • loading.tsx là Suspense cho segment. Không chừa chiều cao thì CLS nhảy.
  • error.tsx là boundary của segment. global-error ở root thay HTML.
  • Nuxt: NuxtLink + layouts/ + slot loading bạn vẫn thiết kế.

Câu hỏi nối tiếp thường gặp

  • loading.tsx khác <Suspense>? loading.tsx là Suspense mức file quanh trang. Bạn vẫn thêm được nữa.
  • Khi nào prefetch sai? JS admin quá nặng trên landing public, hoặc saveData / 2G.
  • Layout có remount lúc navigate con? Không. Đó chính là ý. template.tsx thì có.
  • Tương đương Nuxt của error.tsx? error.vue kèm plugin. Cùng ý: tách segment.

Tóm tắt: middleware.ts của Next chạy trên Edge, trước khi request vào Node. Nó redirect, rewrite, set header được. Nó không phải authorization: fetch tự chế vẫn tới Route Handler nếu bạn chỉ giấu trang. Nuxt middleware/ là Node, theo route, runtime khác hẳn.

Vì sao quan trọng: “Auth-gate App Router thế nào?” mà chỉ trả lời bằng middleware là fail. Edge không dùng hết API Node. Matcher mà cũng dính /_next hoặc /login là vòng redirect. Route middleware Nuxt import Pinia store thì không port được.

  1. Cách hoạt động: Một middleware.ts ở root (hoặc src/). NextRequest / NextResponse. config.matcher chọn path. Redirect user chưa auth tới /login. Bỏ file tĩnh và chính route login.
  2. Authz vẫn nằm ở Server Component, Route Handler, hoặc Server Action: đọc session, redirect / 403. Middleware là cổng rẻ: chỉ xem cookie có mặt, kèm header geolocation / A/B. Giới hạn Edge: không full Node, cẩn thận thư viện verify JWT.
  3. Tương đương Nuxt: middleware/auth.ts là Node và navigateTo được. Next Edge không useAsyncData hay đụng DB. Guard SPA React chỉ là UX, cùng một cảnh báo. Xem security 10.3 để biết session nằm ở đâu, bằng lời.
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(req: NextRequest) {
  const session = req.cookies.get('session')
  if (!session && req.nextUrl.pathname.startsWith('/dashboard')) {
    const login = new URL('/login', req.url)
    login.searchParams.set('from', req.nextUrl.pathname)
    return NextResponse.redirect(login)
  }
  return NextResponse.next()
}
export const config = { matcher: ['/dashboard/:path*'] }
// Vẫn check session trong Server Component. Đây không phải authz.

Điều gì xảy ra: Thiếu cookie trên /dashboard thì redirect sang /login. Matcher bỏ /login nên bạn không loop. Caller gọi /api/secret mà không có cookie vẫn cần Route Handler trả 401. Middleware Nuxt có thể query DB; file Edge này thì không được.

Nuxt middleware/Next middleware.ts
RuntimeNode, file theo routeEdge, một file + matcher
Đụng DB được?CóThường không: để trong Route Handler
Authz?Vẫn check trên serverChỉ cổng cookie; server vẫn quyết
Tương đương SPAGuard router: chỉ UXCùng ý: không phải tường

Góc nhìn senior

  • Matcher gồm /_next hoặc /login là vòng redirect hay gặp.
  • Verify JWT trên Edge: dùng thư viện chạy được trên Edge. jsonwebtoken (crypto Node) là bẫy.
  • Geolocation, cookie A/B, và rewrite locale mới là chỗ dùng đúng. Authz thì không.
  • CSP và header bảo mật set ở đây được, nhưng layer host/CDN đáng tin hơn.
  • Map sang Nuxt: nói “Edge, không phải Node, không Pinia, không authz” ngay câu nối đầu.

Lỗi hay gặp

  • Triệu chứng: vòng redirect. Nguyên nhân: matcher dính /login hoặc /_next. Cách xử lý: siết config.matcher.
  • Triệu chứng: “bọn mình auth trong middleware”. Nguyên nhân: chỉ giấu trang. Cách xử lý: check session trong Server Component / Route Handler.
  • Triệu chứng: lỗi runtime Edge trên fs / Prisma. Nguyên nhân: API Node. Cách xử lý: chuyển sang Route Handler.
  • Triệu chứng: store Nuxt import vào middleware Next. Nguyên nhân: suy tương đương. Cách xử lý: chỉ dùng bit cookie.

Nhà tuyển dụng hay hỏi gì

Auth-gate App Router thế nào, middleware Next có giống Nuxt không, và vì sao bị vòng redirect? Họ muốn giới hạn Edge, matcher sạch, và “đây không phải authz”.

Trả lời khi phỏng vấn

Middleware Next chạy trên Edge, trước Node. Nó redirect, rewrite, và set header được. Nó không phải authorization: mình vẫn check session trong Server Component hoặc Route Handler. Mình match đúng path dashboard để login và file tĩnh của Next không loop. Edge không dùng Prisma hay full API Node; verify JWT cần thư viện chạy được trên Edge. Middleware Nuxt là Node theo route và navigateTo được, cái đó không port. Guard SPA React chỉ là UX, cùng cảnh báo. Cookie có hay không thì check ở đây, authz nằm trên server.

Nhớ nhanh

  • middleware.ts là Edge: redirect, rewrite, header. Nó không phải authz.
  • Siết matcher. Dính /login hoặc /_next là vòng redirect.
  • Authz nằm ở Server Component / Route Handler / Action.
  • Middleware Nuxt là Node theo route. Next là một file Edge. Không port được.

Câu hỏi nối tiếp thường gặp

  • Middleware có query database? Không trên Edge. Dùng Route Handler hoặc Server Component.
  • Vì sao login loop? Matcher gồm /login hoặc cookie bạn cũng set lúc login.
  • definePageMeta({ middleware }) Nuxt? Next dùng config.matcher, không phải meta theo trang.
  • Guard router SPA có đủ? Không. Giống middleware: UX, không phải tường.

Tóm tắt: Server Action là POST công khai mà Next nối từ form hoặc button. use server đánh dấu hàm. Bạn vẫn authenticate, authorize, validate, mutate, rồi revalidateTag. Tương đương Nuxt: POST server/api kèm refreshNuxtData.

Vì sao quan trọng: “Action có an toàn?” là câu phỏng vấn. Chúng trông như RPC. Chúng là endpoint. Check Origin có, nhưng không thay authz trong action. Export helper từ file use server là công khai nó.

  1. Cách hoạt động: File hoặc hàm đánh dấu use server gọi được như một POST. <form action={createItem}> gửi FormData. Button phía client gọi cùng hàm được. Next check header Origin. Đó là giảm CSRF, không phải authorization.
  2. Trong action: session (cookies), quyền, schema (zod), ghi, rồi revalidateTag / revalidatePath. Return kết quả serialise được. Đừng return process.env. useActionState / useFormStatus là UI pending và lỗi. useOptimistic chỉ là một lần vẽ: server vẫn là nguồn sự thật.
  3. Giữ route.ts cho webhook, POST bên thứ ba, và upload streaming. Tương đương Nuxt là handler Nitro kèm refreshNuxtData / refreshCookie. Đừng coi Action thay cho API công khai mà client khác cần.
// app/actions.ts: directive của file là use server
'use server'
import { revalidateTag } from 'next/cache'
import { z } from 'zod'
const Input = z.object({ title: z.string().min(1).max(200) })
export async function createItem(formData: FormData) {
  const session = await getSession() // cookies()
  if (!session) redirect('/login')
  if (!can(session, 'item:create')) throw new Error('Forbidden')
  const parsed = Input.safeParse({ title: formData.get('title') })
  if (!parsed.success) return { ok: false as const, error: 'Invalid title' }
  await db.item.create({ data: { title: parsed.data.title, userId: session.id } })
  revalidateTag('items') // không phải revalidatePath('/blog/[slug]')
  return { ok: true as const }
}
// <form action={createItem}><input name="title" /><button>Save</button></form>

Điều gì xảy ra: Submit POST tới createItem. User chưa đăng nhập thì redirect. Forbidden thì throw. Title sai thì return lỗi serialise được cho form. Ghi thành công thì tag items để trang list làm mới. Hàm là public: ai POST được thì gọi được nếu thiếu authz.

PhầnDùng choKhông dùng cho
Server ActionForm và button first-partyWebhook, client khác
route.tsHTTP công khai, upload, cronMutation nằm cạnh UI
revalidateTagNgay sau khi ghiWildcard path làm tag

Góc nhìn senior

  • Coi mọi hàm export use server là endpoint công khai. Authz nằm bên trong nó.
  • Check Origin giảm CSRF cổ điển. Chúng không thay cookie same-site hay check quyền.
  • revalidatePath('/blog/[slug]') không phải tag. Tag data (items, product:id) rồi revalidate cái đó.
  • useOptimistic không có round-trip server là lời nói dối, lần refresh sau sẽ hủy.
  • Nuxt: Nitro server/api kèm refreshNuxtData. Cùng bốn bước: authn, authz, validate, invalidate.

Lỗi hay gặp

  • Triệu chứng: ai cũng tạo được. Nguyên nhân: tin Action vì “nội bộ”. Cách xử lý: session + can() bên trong.
  • Triệu chứng: list vẫn cũ. Nguyên nhân: revalidatePath với pattern token. Cách xử lý: revalidateTag('items').
  • Triệu chứng: helper cùng file gọi được từ trình duyệt. Nguyên nhân: mọi export là endpoint. Cách xử lý: tách file.
  • Triệu chứng: secret Stripe trong return Action. Nguyên nhân: return cả row. Cách xử lý: { ok: true } / một DTO.

Nhà tuyển dụng hay hỏi gì

use server là gì, Server Action có an toàn không, và làm mới sau POST thế nào? Họ muốn POST công khai, authz bên trong, invalidation bằng tag, và khi nào vẫn viết route.ts.

Trả lời khi phỏng vấn

Server Action là POST công khai mà Next nối từ form hoặc button. Mình coi nó như Route Handler mình không viết tay: authenticate, authorize, validate bằng Zod, mutate, rồi revalidateTag. Check Origin có, nhưng không thay cookie same-site hay authz trong action. Mình không export helper từ file use server vì mọi export thành endpoint. Mình vẫn viết route.ts cho webhook, POST bên thứ ba, và upload streaming. Tương đương Nuxt là POST server/api kèm refreshNuxtData. useOptimistic chỉ là một lần vẽ; server vẫn là nguồn sự thật.

Nhớ nhanh

  • Action là endpoint POST công khai. Authz nằm trong action.
  • Validate, mutate, rồi revalidateTag. Wildcard path không phải tag.
  • Đừng export helper từ file use server.
  • Tương đương Nuxt: POST server/api + refreshNuxtData. Giữ route.ts cho webhook.

Câu hỏi nối tiếp thường gặp

  • useActionState / useFormStatus? Pending và lỗi field trên client. Action vẫn return kết quả serialise được.
  • Khi nào vẫn route.ts? Webhook, POST bên thứ ba, upload streaming, client không phải Next.
  • Vì sao revalidatePath với wildcard sai? Nó không phải tag. Gắn tag cho data, rồi revalidateTag.
  • useOptimistic không có sự thật server? Lần refresh sau hủy lần vẽ. Giữ Action làm nguồn sự thật.

Tóm tắt: Nói theo bốn lớp cache, rồi nêu rõ major. Next 15 fetch không cache trừ khi bạn opt-in. cookies() / headers() làm route thành dynamic. Sheet này là bản đồ; kiểm cơ chế trên version bạn đang dùng.

Vì sao quan trọng: “Sao trang này cũ / sao luôn dynamic?” là câu on-call. Next 14 cache fetch mặc định; Next 15 đảo ngược. Copy blog viết cho 14 sang 15 thì prod bất ngờ. Tiền và tồn kho cần tag, không phải theo thời gian.

  1. Cách hoạt động: Bốn lớp: request memoization (dedupe fetch giống nhau trong một lần render phía server), data cache (giữ fetch xuyên request; Next 15 tắt trừ khi opt-in), full route cache (RSC/HTML đã cache cho route tĩnh hoặc ISR), client router cache (prefetch, tiến/lùi).
  2. Đừng đọc cả bảng. Chọn lớp giải thích được bug, rồi nêu rõ cơ chế (no-store, force-cache, revalidate, tags, cookies()). use cache là cache hàm tường minh mới hơn; kiểm trên major của bạn trước khi khẳng định.
// app/catalog/page.tsx: Next 15, fetch này không cache trừ khi opt-in
export default async function CatalogPage() {
  const res = await fetch('https://api.example.com/items', {
    next: { revalidate: 60, tags: ['items'] }, // data cache + tag
  })
  const items: Item[] = await res.json()
  return <ul>{items.map((it) => <li key={it.id}>{it.title}</li>)}</ul>
}
// Sau khi ghi: revalidateTag('items') từ Server Action / webhook.
// Đừng: cookies() trong layout gốc chỉ để đọc theme.
// Tiền / tồn kho: bỏ revalidate theo thời gian; chỉ tag + on-demand.

Điều gì xảy ra: Không có option next thì Next 15 gọi API mỗi request. Tag để một lần ghi làm mới HTML catalog. Request memoization vẫn dedupe cùng URL trong một lần render. cookies() trong layout gốc sẽ biến cả cây thành dynamic.

LớpNó cache gìMặc định Next 15
Request memoizationfetch giống nhau trong một renderBật
Data cachefetch xuyên request (thời gian / tag)Tắt đến khi opt-in
Full route cacheRSC/HTML của route tĩnh hoặc ISRChỉ route tĩnh
Client router cachePrefetch và tiến/lùiSoft navigation
cookies() / headers()Đưa route sang dynamicCả cây con
Tag khác thời gianGhi → revalidateTag; tiền bỏ thời gianƯu tiên tag cho catalog
PPR / use cacheKhung tĩnh, lỗ dynamic; cache hàm tường minhKiểm major của bạn

Góc nhìn senior

  • Nêu lớp trước, rồi mới tới cơ chế. “Vẫn cũ sau invalidation phía server đã đúng” thường là client router cache.
  • cookies() trong layout gốc cho theme biến trang marketing thành dynamic. Cookie trên <html> hoặc layout hẹp hơn.
  • Revalidate theo thời gian sai với tiền và tồn kho. Tag kèm webhook / Action.
  • Thói quen Next 14 là incident prod trên 15. Nói major ra miệng.
  • Tương đương Nuxt: routeRules swr / isr / prerender. Cùng ý, cơ chế khác. Kiểm major bên đó nữa.

Lỗi hay gặp

  • Triệu chứng: fetch luôn hit trên 15. Nguyên nhân: giả định cache mặc định Next 14. Cách xử lý: opt-in, hoặc chấp nhận dynamic.
  • Triệu chứng: cả app dynamic. Nguyên nhân: cookies() trong layout gốc. Cách xử lý: đưa lần đọc xuống.
  • Triệu chứng: product vẫn cũ sau revalidateTag. Nguyên nhân: fetch không có tag khớp. Cách xử lý: gắn tag lúc đọc và lúc ghi.
  • Triệu chứng: HTML đúng, UI cũ khi Back. Nguyên nhân: client router cache. Cách xử lý: router refresh sau khi ghi.

Người phỏng vấn hỏi gì

Giải thích các cache của Next, revalidate khác no-store khác cookies, và vì sao trang này cũ hoặc luôn dynamic. Họ muốn các lớp, major, và policy tường minh trên mọi fetch.

Trả lời khi phỏng vấn

Mình nói theo bốn lớp: request memoization, data cache, full route cache, và client router cache. Rồi mình nêu rõ major. Fetch Next 15 không cache trừ khi mình opt-in bằng force-cache, revalidate, hoặc tags. Đọc cookies là opt-in dynamic; mình không để cookies trong layout gốc chỉ vì theme. Revalidate theo thời gian sai với tiền; mình gắn tag lúc fetch rồi revalidateTag từ Action hoặc webhook. Cũ sau khi invalidation phía server đã đúng thường là client router cache. Tương đương Nuxt là routeRules swr, isr, prerender. Mình kiểm cơ chế trên version đang để trước mặt. use cache và PPR thì có; mình không bluff API.

Nhớ nhanh

  • Bốn lớp. Nêu rõ major. Ưu tiên policy tường minh trên mọi fetch.
  • Next 15 fetch không cache đến khi bạn opt-in. Next 14 thì ngược lại.
  • cookies() trong layout gốc có thể biến cả app thành dynamic.
  • Tiền và tồn kho: tag on-demand, không revalidate theo thời gian.

Câu hỏi nối tiếp thường gặp

  • Đi qua một trang product? SSG hoặc fetch gắn tag + webhook CMS revalidateTag. Không cookies trong layout đó.
  • Đi qua một trang account? cookies() + no-store. Đừng bao giờ CDN-cache HTML đó.
  • Thiếu tag trên fetch? revalidateTag sau đó là no-op với lần đọc đó.
  • Lớp nào sau invalidation phía server đã đúng? Thường là client router cache.

Tóm tắt: Chọn Next khi SEO, preview mạng xã hội, hoặc data server cạnh UI là quan trọng. Chọn Vite React SPA cho dashboard đã login với API chín. Next dùng như SPA host với use client khắp nơi là chịu dở cả hai bên. Nuxt khác Vite Vue là cùng một ngã rẽ.

Vì sao quan trọng: “Có dashboard đã auth: Next hay Vite?” là ràng buộc product, không phải chuyện résumé. Người phỏng vấn chấm bạn có chịu trả chi phí cache RSC khi chẳng có SEO. Xem thêm 21.2.

  1. Cách hoạt động (checklist): 1. URL có cần HTML cho crawler hoặc unfurl? Next (hoặc Nuxt). 2. Đã có BFF và login kín? SPA Vite. 3. Muốn cookies/session nằm cạnh cây? Next. 4. Team sống được với kỷ luật use client? Không thì SPA.
  2. 5. Mutation: Server Action colocate trên Next; API REST/GraphQL sẵn thì SPA thắng. 6. Hiring: skill RSC hẹp hơn skill React SPA. 7. Hosting: bucket tĩnh khác Node/Edge. 8. Trộn: marketing public trên Next, app là SPA thì được, miễn hai hũ cookie không rò sang nhau.
  3. Tương đương Nuxt: Nuxt khác vite create vue: cùng tám câu. Áp lực résumé không phải lý do chọn meta-framework. Xem 21.2 Next khác React.
// Phác quyết định: nói to ra, rồi chọn
type Pick = 'next' | 'vite-spa'
export function pickFramework(p: {
  seo: boolean
  existingBff: boolean
  closedLogin: boolean
  teamWantsRsc: boolean
}): Pick {
  if (p.seo) return 'next'
  if (p.closedLogin && p.existingBff && !p.teamWantsRsc) return 'vite-spa'
  if (!p.teamWantsRsc && p.existingBff) return 'vite-spa'
  return 'next'
}
// Admin đã login, API chín → vite-spa. Docs + app, cookies → next.

Điều gì xảy ra: SEO thì chọn Next. Login kín kèm BFF kèm team không muốn RSC thì trả Vite. Hàm chỉ là phác thảo, không phải bảng điểm: nói ràng buộc trước. Nuxt khác Vite Vue trả lời bằng cùng các nhánh.

Ràng buộcNextVite SPA
Nội dung đầu / SEOThắngThua trừ khi thêm renderer
DX dashboard authFootgun cache / RSCQuery + router đơn giản
MutationServer Action colocateAPI của bạn đã có
Hiring / hostingSkill RSC; Node hoặc EdgeSkill SPA rộng hơn; bucket tĩnh

Góc nhìn senior

  • Next dùng như SPA host (use client mọi trang) thì trả giá framework mà chẳng giữ được server. Đừng migrate kiểu đó.
  • Marketing + app trong một repo với cookie session phía server là Next hợp. Hai product với hai cookie auth là bẫy.
  • “Chỉ thêm middleware” vào app Vite là lúc Next bắt đầu đáng tiền, hoặc lúc nên giữ SPA và một BFF.
  • Cùng bài test Nuxt khác SPA Vue. Áp lực résumé không phải ràng buộc.
  • Sau khi chọn Next, các lớp state nằm ở doc state-management React. Đừng nhét Query vào Zustand.

Lỗi hay gặp

  • Triệu chứng: SPA Vite viết lại sang Next “cho SEO” với use client mọi trang. Nguyên nhân: résumé. Cách xử lý: giữ Vite, hoặc dùng server cho đúng.
  • Triệu chứng: tool nội bộ chỉ Chrome mà dựng trên Next. Nguyên nhân: theo mốt. Cách xử lý: Vite React kèm BFF sẵn có.
  • Triệu chứng: hai app, một cookie session bị rò. Nguyên nhân: trộn marketing + app không tách cookie. Cách xử lý: tách host / tên.
  • Triệu chứng: “phải học RSC cho admin CRUD”. Nguyên nhân: không SEO, API chín. Cách xử lý: SPA mới là lựa chọn senior.

Người phỏng vấn hỏi gì

Có một dashboard đã auth: chọn Next hay Vite? Và migrate từ Nuxt/Vue thì có cùng cách không? Họ muốn ràng buộc product, không phải framework trên CV.

Trả lời khi phỏng vấn

Nếu SEO và data server cạnh UI quan trọng, mình chọn Next. Nếu đây là dashboard auth sau login với API đã chín, Vite kèm React mới là lựa chọn senior, không phải đang né. Next dùng như SPA host với use client mọi trang là tệ nhất cả hai. Mình chạy cùng checklist tám câu như khi so Nuxt với SPA Vue: crawler, BFF sẵn, cookie cạnh cây, team có muốn RSC không, mutation, hiring, hosting, tách cookie. Áp lực résumé không phải ràng buộc. Sau Next, mình vẫn giữ Query cho list server. Đường chia library khác framework là 21.2; topic này là làm hay không làm.

Nhớ nhanh

  • Next cho SEO, HTML lai, và data server nằm cạnh UI.
  • SPA Vite cho dashboard auth kín với API đã có.
  • use client trên mọi trang không phải chiến lược migrate.
  • Nuxt khác Vite Vue là cùng ngã rẽ. Résumé không phải ràng buộc.

Câu hỏi nối tiếp thường gặp

  • State sau khi chọn Next? Local / URL / Query / một store client nhỏ. Xem doc state-management React.
  • Khi nào middleware Vite nghĩa là Next? Khi bạn muốn HTML server hoặc cookie session cạnh cây.
  • Marketing Next + app SPA? Được. Tách cookie và OAuth client. Đừng share một hũ session.
  • Bạn có dùng Nuxt cho product này không? Nếu không, đừng chọn Next cho bản port React.

Tóm tắt: On-call Next gom thành ba nhóm: lệch hydration, data server trong payload Flight, và waterfall await. Đừng gọi mọi thứ là “SSR”. Mỗi nhóm có H4 riêng bên dưới.

Vì sao quan trọng: Người phỏng vấn hỏi “bạn đã đưa bug hydration lên production chưa?”, “secret lộ trong RSC thế nào?”, và “sao TTFB 2s với ba fetch?” Senior có fix mặc định cho từng nhóm, rồi chứng minh.

  1. Cách hoạt động: Mở H4 khớp incident. Lệch hydration là HTML first-paint khác first render phía client. Lộ secret là mọi thứ bạn đưa xuyên ranh giới RSC. Waterfall là await nối tiếp trong layout lồng.
  2. Rule chung: nêu rõ ranh giới process. Nuxt có cùng ba nhóm (ClientOnly / payload / useAsyncData nối tiếp). Nguyên nhân riêng React nằm ở các H4. Vue 5.2.11 là topic hydration anh em.
// Ba nhóm: mở H4 khớp, đừng debug “SSR”
// 1. Hydration: HTML server phải khớp chữ với first render phía client
//    <span>{createdAt.toLocaleDateString()}</span>  // locale có thể khác
// 2. Secret: props tới Client Component nằm trong payload Flight
//    <Panel user={user} />  // user.emailHash có thể là leak
// 3. Waterfall: layout lồng await nối tiếp làm TTFB phình
//    await user; await dash; await items  // bắt đầu việc độc lập cùng lúc
// Chứng minh: build prod cho (1), tìm trong response RSC cho (2), flame server cho (3).

Điều gì xảy ra: Comment là bản đồ triage, không phải fix. Nhóm 1 là lệch chữ. Nhóm 2 là payload bạn Ctrl-F được. Nhóm 3 là timeline server. Mỗi H4 có example chạy được.

NhómFix mặc địnhChứng minh bằng
HydrationRender thuần / clock clientOverlay bản production
Payload lộserver-only + DTO tối thiểuTìm trong body RSC
Waterfall awaitPromise.all / lỗ SuspenseServer timing / TTFB

Góc nhìn senior

  • “Bug SSR” không phải chẩn đoán. Chọn nhóm, rồi mở H4.
  • Thứ xuyên ranh giới RSC là public. Kể cả giá trị return của Action.
  • Việc độc lập phải chạy song song, không thì bạn trả TTFB cho không.
  • Nuxt: cùng ba nhóm. ClientOnly, runtimeConfig.public, useAsyncData nối tiếp.
  • Topic cha này là triage. Độ sâu nằm ở ba H4.

Lỗi hay gặp

  • Triệu chứng: gọi mọi bug Next là “SSR”. Nguyên nhân: không phân nhóm. Cách xử lý: hydration / payload / waterfall.
  • Triệu chứng: suppressHydrationWarning trên layout. Nguyên nhân: giấu clock. Cách xử lý: H4 hydration.
  • Triệu chứng: row Prisma làm prop Client. Nguyên nhân: cho tiện. Cách xử lý: H4 secret.
  • Triệu chứng: mỗi layout lồng một await. Nguyên nhân: “một fetch một file”. Cách xử lý: H4 waterfall.

Người phỏng vấn hỏi gì

Bạn đã đưa bug hydration lên production chưa, secret lộ trong RSC thế nào, và sao TTFB 2s trên trang có ba fetch? Họ muốn ba nhóm và mỗi nhóm một fix mặc định, không phải tour tính năng.

Trả lời khi phỏng vấn

Ba nhóm mình đã đưa lên production là lệch hydration, secret trong payload RSC, và await nối tiếp. HTML server phải khớp first render phía client. Mọi thứ mình đưa vào Client Component đều thấy được trong payload Flight. Fetch độc lập phải start cùng lúc, hoặc mình stream một lỗ bằng Suspense. Mình chứng minh từng nhóm bằng overlay bản production, tìm trong body RSC, và track server timing. Nuxt có cùng ba nhóm dưới ClientOnly, payload, và useAsyncData nối tiếp. Mình không gọi mọi thứ là SSR. Các H4 bên dưới là độ sâu.

Nhớ nhanh

  • Ba nhóm: hydration, payload lộ, waterfall await.
  • Thứ xuyên ranh giới RSC là public.
  • Việc độc lập phải chạy song song, không thì bạn trả TTFB cho không.
  • Topic này là triage. Mở H4 khớp.

Câu hỏi nối tiếp thường gặp

  • Server khác Client: modal có useState sống ở đâu? Child Client. Trang vẫn có thể là Server Component.
  • Cache product + tag webhook? Đó là 21.10, không phải nhóm thứ tư.
  • useAsyncData Nuxt khác Server Component async? Cùng ý; Query vẫn thắng cho data live trên client.
  • Middleware khác redirect layout khác guard SPA? Middleware không phải authz. Xem 21.8.

Tóm tắt: Hydration đòi first render phía client khớp chữ với HTML server. toLocaleDateString(), Date.now(), Math.random(), và localStorage lúc render phá điều đó. Chung với Vue 5.2.11; nguyên nhân riêng React là locale, HTML không hợp lệ, và extension.

Vì sao quan trọng: Overlay “did not match” trên layout format new Date(), hoặc theme đọc localStorage lúc render, là incident production. suppressHydrationWarning trên cả cây thì giấu bug. ClientOnly của Nuxt là lối thoát anh em: dùng cho map, không dùng cho clock.

  1. Cách hoạt động: Server phát HTML. React đi HTML đó và first render phía client. Lệch chữ thì vứt node và render lại. Format ngày với locale tường minh (hoặc chuỗi ISO đã serialize). Theme: cookie trên <html> (server thấy được) hoặc script inline trước paint, không localStorage trong render.
  2. suppressHydrationWarning cho một text node clock, không phải layout. HTML không hợp lệ (<p><div>) bị browser viết lại; cây React thì không. Trông như bug của bạn. Extension browser inject DOM cùng một class. Chứng minh bằng bản production; HMR nói dối.
  3. Tương đương Nuxt: payload kèm ClientOnly / <client-only>. Vue 3.4+ data-allow-mismatch là phẫu thuật. Xem hydration Vue 5.2.11.
// Bẫy: HTML server "en-US 10/08" khác client "vi-VN 08/10"
export function CreatedAt({ iso }: { iso: string }) {
  // ❌ locale và timezone theo máy, không theo request
  // return <time>{new Date(iso).toLocaleDateString()}</time>
  // ✅ serialize trên server với locale tường minh
  const text = new Intl.DateTimeFormat('en-GB', {
    timeZone: 'UTC', dateStyle: 'medium',
  }).format(new Date(iso))
  return <time dateTime={iso}>{text}</time>
}
// Theme: gán class trên <html> từ cookie, không localStorage lúc render.
// suppressHydrationWarning: một text node (clock live), không bao giờ layout.

Điều gì xảy ra: Cả hai máy format iso bằng en-GB và UTC, nên chữ khớp. toLocaleDateString() cũ dùng locale của server khác laptop. Class theme từ cookie nằm trong HTML server; localStorage thì không.

Nguyên nhânTriệu chứngCách xử lý
Locale / timezoneChữ ngày khácLocale + TZ tường minh
localStorage lúc renderClass theme lệchCookie hoặc script inline
HTML không hợp lệBrowser viết lại câyLồng hợp lệ
Extension / script A/BThêm node trước hydrateHoãn injector

Góc nhìn senior

  • Sửa từ nguồn. ClientOnly / clock client cho map hoặc ticker live, không phải để giấu bug.
  • HTML không hợp lệ là ticket “framework hỏng” phổ biến nhất ở cả Vue và React.
  • useId() ổn định xuyên server và client. Counter module-scope thì không.
  • Chứng minh bằng bản production trên cùng locale bạn ship. Overlay dev kèm HMR không phải bằng chứng.
  • Chung với Vue 5.2.11. Nói phần riêng React: suppressHydrationWarning là phẫu thuật.

Lỗi hay gặp

  • Triệu chứng: overlay trên clock ở header. Nguyên nhân: toLocaleDateString() trong layout chung. Cách xử lý: locale tường minh.
  • Triệu chứng: theme nháy rồi lệch. Nguyên nhân: localStorage lúc render. Cách xử lý: cookie trên <html>.
  • Triệu chứng: warning chỉ trên prod. Nguyên nhân: tin HMR. Cách xử lý: next build && next start.
  • Triệu chứng: suppressHydrationWarning trên <body>. Nguyên nhân: giấu cả cây. Cách xử lý: một text node, hoặc sửa từ nguồn.

Người phỏng vấn hỏi gì

Overlay “did not match” trên layout format new Date(), hoặc theme đọc localStorage lúc render. Họ muốn render thuần, locale tường minh, và khi nào suppressHydrationWarning chấp nhận được.

Trả lời khi phỏng vấn

HTML server phải khớp first render phía client. Mình không format toLocaleDateString trong layout chung nếu không có locale tường minh, và không đọc localStorage lúc render. Theme nằm ở cookie hoặc script inline trước paint. suppressHydrationWarning chỉ cho một text node đồng hồ, không phải layout. HTML không hợp lệ thì browser viết lại, React thì không. Đó là bug framework giả hay gặp. Extension trình duyệt trông y như vậy. Mình chứng minh bằng production build. ClientOnly của Nuxt là lối thoát cùng kiểu cho map. Vue 5.2.11 cùng họ.

Nhớ nhanh

  • Render thuần. Locale tường minh. Cookie cho theme.
  • localStorage lúc render là bug hydration.
  • suppressHydrationWarning là một text node, không bao giờ layout.
  • Chứng minh bằng production build, không chỉ HMR.

Câu hỏi nối tiếp thường gặp

  • SSR theme toggle mà không lệch? Class trên html lấy từ cookie. Script inline trước paint nếu bắt buộc.
  • Khi nào suppressHydrationWarning chấp nhận được? Một text node đồng hồ đang chạy. Không phải layout. Không phải theme.
  • Extension trình duyệt trông như bug của bạn? Node thừa trước hydrate. Reproduce trên profile sạch.
  • HTML không hợp lệ? Browser “sửa” p>div; React thì không. Validate HTML SSR.

Tóm tắt: Mọi thứ bạn đưa vào Client Component hoặc return từ Server Action nằm trong payload user đọc được. NEXT_PUBLIC_ là công khai. import 'server-only' làm fail build nếu file client import module đó. Tương đương Nuxt: runtimeConfig.public khác private.

Vì sao quan trọng: “Secret lộ trong RSC thế nào?” muốn payload Flight, không phải “đừng dùng env vars”. Đưa row Prisma vào <Panel user={user} /> thì serialise mọi cột. Xem thêm secret trong Vite và Nuxt.

  1. Cách hoạt động: Server Component đọc được process.env.DATABASE_URL. Một khi giá trị đó thành prop, return, hoặc chuỗi NEXT_PUBLIC_ bị inline, nó là công khai. Đánh dấu module secret bằng import 'server-only'. Chỉ truyền { id, name }, không phải cả row ORM. Session nằm trong cookie httpOnly, không dump vào store phía client.
  2. Nguyên nhân: NEXT_PUBLIC_STRIPE_SECRET; return process.env từ Action; log row vào error boundary phía Client; nhét PII lên prop Client “cho tiện”. Tìm key trong response RSC (payload Flight) trước khi gọi trang là an toàn.
  3. Tương đương Nuxt: runtimeConfig private khác runtimeConfig.public / NUXT_PUBLIC_. Composable return cả config thì đổ hết vào window.__NUXT__. Cùng thói quen: grep payload.
import 'server-only'
export const dbUrl = process.env.DATABASE_URL! // không được vào module client

export type UserDto = { id: string; name: string }
export function toUserDto(row: { id: string; name: string; emailHash: string }): UserDto {
  return { id: row.id, name: row.name } // không emailHash, không row Prisma
}
// Client: <Panel user={toUserDto(row)} />
// Đừng: <Panel user={row} /> hoặc return process.env từ Action.
// NEXT_PUBLIC_* bị inline vào bundle trình duyệt: chỉ key publishable.
// Chứng minh: tìm DATABASE_URL và emailHash trong body RSC / Flight.

Điều gì xảy ra: server-only làm fail build nếu file use client import dbUrl. toUserDto bỏ emailHash trước khi vào payload Flight. Tìm trong body RSC mới là kiểm chứng, không phải hy vọng lúc review.

KênhCông khai?Thói quen
NEXT_PUBLIC_*Có, bị inlineChỉ key publishable
Prop Client ComponentCó, payload FlightDTO tối thiểu
Return Server ActionCó, body POST{ ok: true } / DTO
Module server-onlyKhông, nếu build còn đứngSecret, ORM, env

Góc nhìn senior

  • Nếu mình đưa nó vào Client Component, nó nằm trong payload Flight. User đã đọc được rồi.
  • NEXT_PUBLIC_ không phải prefix an toàn cho secret thật. Cùng rule với VITE_ và NUXT_PUBLIC_.
  • Session trong Zustand sau hydrate chỉ là bản sao. Cookie httpOnly mới là nguồn sự thật.
  • Error boundary phía client không được đổ row server. Scrub PII ngay ở DTO.
  • Sau khi lộ: rotate trước, rồi grep artifact, rồi thêm grep CI cho sk_ / SECRET.

Lỗi hay gặp

  • Triệu chứng: Stripe sk_ trong bundle. Nguyên nhân: NEXT_PUBLIC_STRIPE_SECRET. Cách xử lý: rotate, rồi server-only.
  • Triệu chứng: hash email trong body RSC. Nguyên nhân: đưa row Prisma. Cách xử lý: toUserDto.
  • Triệu chứng: Action return process.env. Nguyên nhân: debug. Cách xử lý: { ok: true }. Check Origin không giấu nó.
  • Triệu chứng: server-only sau đó nằm sau use client. Nguyên nhân: refactor. Cách xử lý: build phải fail; đừng bỏ qua.

Nhà tuyển dụng hay hỏi gì

Secret lộ trong RSC thế nào? Họ muốn payload Flight, server-only, DTO tối thiểu, và NEXT_PUBLIC_ như prefix công khai, không phải “đừng dùng env vars”.

Trả lời khi phỏng vấn

Nếu mình đưa nó vào Client Component, nó nằm trong payload Flight. Mình đánh dấu module secret là server-only, truyền { id, name }, và giữ session trong cookie httpOnly. NEXT_PUBLIC không phải prefix an toàn cho secret thật, cùng rule với VITE_ và NUXT_PUBLIC_. Return process.env từ Action vẫn là lộ dù có check Origin. Mình tìm key trong response RSC trước khi gọi trang an toàn. Sau khi lộ, mình rotate trước. Tương đương Nuxt là runtimeConfig private khác public. Anh em Vue/Vite là security 10.7.

Nhớ nhanh

  • server-only trên module secret. DTO tối thiểu khi qua ranh giới.
  • NEXT_PUBLIC_* không phải prefix an toàn cho secret thật.
  • Return Action và prop Client là payload công khai.
  • Tìm trong payload Flight. User đã đọc được rồi.

Câu hỏi nối tiếp thường gặp

  • server-only sau đó nằm sau use client? Build phải fail. Đó là lý do có package này.
  • Return process.env từ Action? Vẫn là lộ. Check Origin là CSRF, không phải bí mật.
  • Session trong Zustand khác cookie httpOnly? Cookie là nguồn sự thật. Store chỉ là bản sao.
  • PII trong error boundary phía client? Scrub ở DTO. Đừng đưa row ORM “cho overlay”.

Tóm tắt: Waterfall là việc nối tiếp không cần phải chờ. Layout lồng, mỗi cái một await, làm TTFB phình. Chỉ waterfall khi data phụ thuộc thật; việc độc lập thì Promise.all hoặc stream một lỗ bằng Suspense. Tương đương Nuxt: useAsyncData nối trong layout lồng.

Vì sao quan trọng: “Sao TTFB 2s trên trang với ba fetch?” muốn đồ thị phụ thuộc, không phải “dùng Promise.all khắp nơi”. Next 15 fetch không cache, nên layout nối không được cache hit miễn phí. Shell RSC rỗng kèm năm useEffect fetch là Next dùng như SPA.

  1. Cách hoạt động: await getUser() trước nếu orders và notifs cần user.id, rồi Promise.all phần còn lại. Bắt đầu việc con ở parent (Promise.all / preload) hoặc dựa vào request memoization để fetch cùng URL của child trong lần render này là miễn phí. Stream lỗ không critical bằng <Suspense> thay vì chặn cả trang.
  2. layout.tsx lồng, mỗi cái await widget dashboard riêng, là chỗ TTFB phình kinh điển. Giới hạn concurrency: đừng Promise.all 10k id. fetch không cache của Next 15 làm chuyện này lộ; cache mặc định Next 14 thì giấu.
  3. Tương đương Nuxt: useAsyncData / useFetch nối trong layout lồng, khác Promise.all kèm đảo lazy. Cùng đồ thị. Xem 21.6 cho các pattern fetch mà H4 này không lặp.
export default async function DashboardPage({ id }: { id: string }) {
  // Lỗi hay gặp: nối tiếp, getOrders không cần getNotifs xong
  // const user = await getUser(id)
  // const orders = await getOrders(user.id)
  // const notifs = await getNotifs(user.id)
  const user = await getUser(id) // cần nếu cái khác cần user.id
  const [orders, notifs] = await Promise.all([
    getOrders(user.id),
    getNotifs(user.id),
  ])
  return <Dash user={user} orders={orders} notifs={notifs} />
}
// Widget không critical: <Suspense fallback={<Skel />}><Widget /></Suspense>
// Fetch con cùng URL trong lần render này được memoize: vẫn bắt đầu ở parent.

Điều gì xảy ra: User là phụ thuộc thật. Orders và notification bắt đầu cùng lúc sau khi có user.id. Bản nối trong comment trả TTFB vì getNotifs phải chờ getOrders. Lỗ Suspense quanh widget thì shell vẽ trước.

PatternKhi nàoTTFB
await nốiB cần id của ATổng cả hai
Promise.allĐộc lập sau AMax của nhóm
Lỗ SuspenseWidget không criticalShell trước
fetch con cùng URLMột render, được memoizeMiễn phí nếu parent đã bắt đầu

Góc nhìn senior

  • Chỉ waterfall khi B cần id của A. “Một await một file” không phải phụ thuộc.
  • Bắt đầu fetch con ở parent, hoặc share dedupe fetch. Đừng hy vọng child miễn phí.
  • RSC rỗng kèm useEffect fetch phía client là SPA mặc áo Next. HTML đầu đã nên có data shell.
  • Giới hạn concurrency: Promise.all 10k id sẽ làm API sập. Hãy batch hoặc hàng đợi.
  • Nuxt: useAsyncData nối trong layout lồng là cùng chỗ TTFB phình.

Lỗi hay gặp

  • Triệu chứng: TTFB là tổng ba fetch. Nguyên nhân: layout lồng mỗi cái await. Cách xử lý: Promise.all ở parent.
  • Triệu chứng: “nhưng fetch được cache”. Nguyên nhân: thói quen Next 14 trên Next 15. Cách xử lý: mặc định không cache; hãy nhìn đồ thị.
  • Triệu chứng: Promise.all 10k. Nguyên nhân: không giới hạn. Cách xử lý: batch.
  • Triệu chứng: RSC rỗng, năm fetch client. Nguyên nhân: thói quen Vite. Cách xử lý: await shell trên server.

Nhà tuyển dụng hay hỏi gì

Vì sao TTFB 2s trên trang có ba fetch? Họ muốn dependency graph, khi nào Promise.all không đúng chỗ, và khi nào Suspense một lỗ thay vì await cả trang.

Trả lời khi phỏng vấn

Mình chỉ waterfall khi B cần id của A. Orders và notification bắn cùng lúc sau user. Nếu Server Component con fetch, mình start promise đó ở parent hoặc share fetch dedupe. Hit của child trong lần render này sẽ miễn phí. Shell RSC rỗng kèm năm fetch useEffect là Next dùng như SPA. Fetch Next 15 không cache, nên layout nối không được cache hit miễn phí. Mình stream widget không tối quan trọng bằng Suspense. Mình đọc timeline server, không phải output câu đố. useAsyncData nối trong layout lồng của Nuxt cũng phình TTFB y vậy.

Nhớ nhanh

  • Phụ thuộc thật mới waterfall. Việc độc lập thì Promise.all.
  • Bắt đầu fetch của child ở parent, hoặc share dedupe fetch.
  • RSC rỗng + fetch useEffect phía client là SPA mặc áo Next.
  • Suspense một lỗ khi widget không nằm trên critical path.

Câu hỏi nối tiếp thường gặp

  • Suspense một lỗ thay vì await trên trang? Khi widget không bắt buộc cho first meaningful paint.
  • Request memoization giấu fetch của child? Cùng URL trong lần render này thì miễn phí. Vẫn start ở parent.
  • Vì sao không Promise.all 10k id? Bạn sẽ làm API quá tải. Giới hạn batch.
  • Vẽ timeline server? getUser, rồi Promise.all(orders, notifs). Bản nối thì các thanh chồng.