Facebook Pixel

NextJs 15 có gì mới? So sánh NextJs 14 và NextJs 15

29 Oct, 2024

Tran Thuy Vy

Frontend Developer

Next.js 15 mang đến một loạt các cải tiến, nâng cấp hiệu suất với Turbopack, cập nhật API bất đồng bộ giúp tối ưu hóa dữ liệu và tốc độ phản hồi

NextJs 15 có gì mới? So sánh NextJs 14 và NextJs 15

Mục Lục

Đến hẹn lại lên, cứ sau khoảng 365 ngày thì NextJs lại có bản update version lớn một lần. Chúng ta lại có dịp so sánh phiên bản NextJs 15 và NextJs 14 với nhau.

Đương nhiên rồi, so sánh thì phải có bằng chứng, bằng chứng ở đâu thì cùng mình create-next-app@14create-next-app@latest và quan sát 2 file package.json của 2 phiên bản sau nha:

So sánh next 14 và next 15

Bạn có thể thấy phần scripts, câu lệnh dev bên version 15 có thêm --turbopack

  • next dev: khởi chạy server của Next.js. Đây là lệnh để build ứng dụng trong môi trường dev, nhưng với các ứng dụng lớn, thời gian khởi động có thể lâu hơn.
  • next dev –turbopack: đây là version ổn định mới của Next 15, tối ưu tốc độ bằng cách cải tiến hiệu suất với Turbopack. Khởi động server và cập nhật code nhanh hơn đáng kể so với next dev thông thường.

Tiếp theo, phần dependencies bạn có thể thấy được bản next 15 sử dụng react và react-dom version 19rc (19rc là version 19 thử nghiệm) còn với bản next 14 sử dụng react và react-dom version 18 chính thức.

Nhưng bạn có thể thấy phần type thì bên team Next vẫn giữ version 18 để đảm bảo hơn trong quá trình dev. Đấy là tổng quan về file package.json của next 14 và next 15, tổng quan rồi thì cùng mình vào chi tiết nha.

1. React 19

  • useFormState đã được thay thế bởi useActionState. useFormState bạn vẫn có thể sử dụng được nhưng sẽ bị loại bỏ trong tương lai.
  • useFormStatus có thêm các key như: data, method, và action, bình thường bạn sử dụng thì chỉ có key pending mà thôi.

2. Async Request APIs

Trong Next.js 15, các API mình nói bên dưới đã chuyển từ đồng bộ sang bất đồng bộ, giúp tối ưu hiệu suất và khả năng xử lý:

  • cookies
  • headers
  • draftMode
  • params trong layout.js, page.js, route.js, default.js,...
  • searchParams trong page.js

Về mặt hỗ trợ chuyển đổi, Next.js cung cấp công cụ codemod để tự động cập nhật code. Các API này vẫn có thể truy cập tạm thời theo cách đồng bộ, nhưng sẽ warning.

Theo chân mình đi xem các API này thay đổi cụ thể như thế nào nhé!

2.1 Cookies

TypeScript
import { cookies } from 'next/headers'
 
// Trước: Đồng bộ
const cookieStore = cookies()
const token = cookieStore.get('token')
 
// Sau: Bất đồng bộ
const cookieStore = await cookies()
const token = cookieStore.get('token')

Bên trên là code minh họa cách chuyển từ việc truy cập đồng bộ API cookies sang bất đồng bộ trong Next 15.

Bạn vẫn có thể call API cookies cách đồng bộ (không cần await), nhưng nó chỉ là giải pháp tạm thời để đỡ khó khăn trong quá trình chuyển đổi. Tuy nhiên, khi sử dụng cách này, bạn sẽ thấy warning trong môi trường dev.

Typescript
import { cookies, type UnsafeUnwrappedCookies } from 'next/headers'
 
// Trước
const cookieStore = cookies()
const token = cookieStore.get('token')
 
// Sau
const cookieStore = cookies() as unknown as UnsafeUnwrappedCookies
// Sẽ log warning trong môi trường dev
const token = cookieStore.get('token')

Lý do warning, là vì Next khuyến nghị bạn nên chuyển sang gọi bất đồng bộ await cookies() để đảm bảo sự tương thích tốt hơn.

2.2 Headers

Đoạn code bên dưới là cách thay đổi từ việc gọi đồng bộ sang bất đồng bộ với API headers trong Next.js 15.

Typescript
import { headers } from 'next/headers'
 
// Trước: Đồng bộ
const headersList = headers()
const userAgent = headersList.get('user-agent')
 
// Sau: Bất đồng bộ
const headersList = await headers()
const userAgent = headersList.get('user-agent')

Tương tự như với cookies thì bạn vẫn có thể call API headers cách đồng bộ (không cần await). Vẫn sẽ warning trong môi trường dev nhé.

Typescript
import { headers, type UnsafeUnwrappedHeaders } from 'next/headers'
 
// Trước
const headersList = headers()
const userAgent = headersList.get('user-agent')
 
// Sau: sẽ warning trong môi trường dev
const headersList = headers() as unknown as UnsafeUnwrappedHeaders
const userAgent = headersList.get('user-agent')

2.3 draftMode

Dưới đây là đoạn code thay đổi từ việc call API đồng bộ sang bất đồng bộ với draftMode

Typescript
import { draftMode } from 'next/headers'
 
// Trước: đồng bộ
const { isEnabled } = draftMode()
 
// Sau: bất đồng bộ
const { isEnabled } = await draftMode()

Nếu bạn vẫn muốn sử dụng đồng bộ theo cách cũ, thì vẫn có thể nhé. Nhưng sẽ warning trong môi trường dev

Typescript
import { draftMode, type UnsafeUnwrappedDraftMode } from 'next/headers'
 
// Trước
const { isEnabled } = draftMode()
 
// Sau: sẽ log warning in dev
const { isEnabled } = draftMode() as unknown as UnsafeUnwrappedDraftMode

2.4 params & searchParams

  • Asynchronous Layout

Các tham số params và searchParams cũng được chuyển sang bất đồng bộ khi sử dụng trong generateMetadataLayout

Typescript
// Trước: đồng bộ
type Params = { slug: string }
 
export function generateMetadata({ params }: { params: Params }) {
  const { slug } = params
}
 
export default async function Layout({
  children,
  params,
}: {
  children: React.ReactNode
  params: Params
}) {
  const { slug } = params
}
 
// Sau: bất đồng bộ
type Params = Promise<{ slug: string }>
 
export async function generateMetadata({ params }: { params: Params }) {
  const { slug } = await params
}
 
export default async function Layout({
  children,
  params,
}: {
  children: React.ReactNode
  params: Params
}) {
  const { slug } = await params
}

Bạn có thể thấy trong version next 15, bạn cần sử dụng await với params để truy cập dữ liệu, nhằm tối ưu hóa hiệu suất.

  • Synchronous Layout

Khi bạn cần xử lý params bất đồng bộ trong thành phần đồng bộ, bạn có thể dùng hook use từ React. Dùng use giúp đơn giản hóa việc xử lý bất đồng bộ dễ dàng hơn.

Typescript
// Trước
type Params = { slug: string }
 
export default function Layout({
  children,
  params,
}: {
  children: React.ReactNode
  params: Params
}) {
  const { slug } = params
}
 
// Sau
import { use } from 'react'
 
type Params = Promise<{ slug: string }>
 
export default function Layout(props: {
  children: React.ReactNode
  params: Params
}) {
  const params = use(props.params)
  const slug = params.slug
}
  • Asynchronous Page

Khi làm việc với các tham số params và searchParams dưới dạng bất đồng bộ trong một page

Typescript
// Trước: params và searchParams được truyền đồng bộ vào hàm generateMetadata
type Params = { slug: string }
type SearchParams = { [key: string]: string | string[] | undefined }
 
export function generateMetadata({
  params,
  searchParams,
}: {
  params: Params
  searchParams: SearchParams
}) {
  const { slug } = params
  const { query } = searchParams
}
 
export default async function Page({
  params,
  searchParams,
}: {
  params: Params
  searchParams: SearchParams
}) {
  const { slug } = params
  const { query } = searchParams
}
 
// Sau: params và searchParams là Promise, cần await để truy cập dữ liệu.
type Params = Promise<{ slug: string }>
type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>
 
export async function generateMetadata(props: {
  params: Params
  searchParams: SearchParams
}) {
  const params = await props.params
  const searchParams = await props.searchParams
  const slug = params.slug
  const query = searchParams.query
}
 
export default async function Page(props: {
  params: Params
  searchParams: SearchParams
}) {
  const params = await props.params
  const searchParams = await props.searchParams
  const slug = params.slug
  const query = searchParams.query
}
  • Synchronous Page
Typescript
'use client'
 
// Trước: params và searchParams được truyền trực tiếp vào thành phần mà không cần xử lý thêm
type Params = { slug: string }
type SearchParams = { [key: string]: string | string[] | undefined }
 
export default function Page({
  params,
  searchParams,
}: {
  params: Params
  searchParams: SearchParams
}) {
  const { slug } = params
  const { query } = searchParams
}
 
// Sau: params và searchParams có thể là bất đồng bộ, vì vậy sử dụng hook `use` từ React để xử lý ngay.
import { use } from 'react'
 
type Params = Promise<{ slug: string }>
type SearchParams = { [key: string]: string | string[] | undefined }
 
export default function Page(props: {
  params: Params
  searchParams: SearchParams
}) {
  const params = use(props.params)
  const searchParams = use(props.searchParams)
  const slug = params.slug
  const query = searchParams.query
}

Bạn có thể không cần định nghĩa type Params cho params và searchParams, giúp code dễ đọc trong TypeScript khi các giá trị cần xử lý bất đồng bộ.

Bạn hoàn toàn có thể viết ngắn gọn hơn không cần xác định kiểu, đơn giản hóa việc xử lý bằng cách truyền props trực tiếp vào use, phù hợp khi không cần kiểm soát kiểu dữ liệu chặt chẽ.

Typescript
// Trước
export default function Page({ params, searchParams }) {
  const { slug } = params
  const { query } = searchParams
}
 
// Sau
import { use } from "react"
 
export default function Page(props) {
  const params = use(props.params)
  const searchParams = use(props.searchParams)
  const slug = params.slug
  const query = searchParams.query
}
 
  • Route Handlers

Trước đây, params là đối tượng đồng bộ, có thể truy cập trực tiếp từ segmentData.params. Ở version 15, params trở thành Promise, vì vậy cần sử dụng await để lấy giá trị từ segmentData.params.

Typescript
// Trước
type Params = { slug: string }
 
export async function GET(request: Request, segmentData: { params: Params }) {
  const params = segmentData.params
  const slug = params.slug
}
 
// Sau
type Params = Promise<{ slug: string }>
 
export async function GET(request: Request, segmentData: { params: Params }) {
  const params = await segmentData.params
  const slug = params.slug
}

Bạn có thể ngắn gọn hơn, không cần phải định nghĩa kiểu dữ liệu:

Typescript
// Trước
export async function GET(request, segmentData) {
  const params = segmentData.params
  const slug = params.slug
}
 
// Sau
export async function GET(request, segmentData) {
  const params = await segmentData.params
  const slug = params.slug
}

3. fetch requests

Trong Next.js 15, mặc định các fetch request không còn được cache để giảm thiểu lưu trữ không cần thiết. Nếu bạn muốn cache thì đơn giản chỉ cần thêm option cache: 'force-cache' vào lệnh fetch. Ví dụ:

Typescript
const lab = await fetch('https://...', { cache: 'force-cache' }) // Cached

Còn nếu như bạn muốn cache mọi yêu cầu trong layout hoặc page, thêm export const fetchCache = 'default-cache' ở trên cùng layout. Ví dụ:

Typescript
export const fetchCache = 'default-cache'
 
export default async function RootLayout() {
  const a = await fetch('https://...') // Cached
  const b = await fetch('https://...', { cache: 'no-store' }) // Not cached
 
  // ...
}

Đây là những thay đổi quan trọng trong Next.js 15 mà cá nhân mình đánh giá cao! Nếu bạn thấy hứng thú, hãy khám phá các cải tiến trong version này.

4. Kết luận

Next.js 15 mang đến một loạt các cải tiến, từ việc nâng cấp hiệu suất với Turbopack cho đến các cập nhật API bất đồng bộ giúp tối ưu hóa dữ liệu và tốc độ phản hồi.

Việc hỗ trợ React 19 cùng các hooks mới là bước tiến nhằm tận dụng tối đa các tính năng mới của React, tối ưu hiệu năng cho các ứng dụng lớn. Next 15 sẽ không làm bạn thất vọng, hãy tự mình trải nghiệm để hiểu rõ hơn các tính năng phù hợp cho dự án của riêng bạn.

Các bài viết liên quan:

Bài viết liên quan

Lập trình backend expressjs

xây dựng hệ thống microservices
  • Kiến trúc Hexagonal và ứng dụngal font-
  • TypeScript: OOP và nguyên lý SOLIDal font-
  • Event-Driven Architecture, Queue & PubSubal font-
  • Basic scalable System Designal font-

Đăng ký nhận thông báo

Đừng bỏ lỡ những bài viết thú vị từ 200Lab