React, Next.js và TypeScript đã trở thành bộ công nghệ mặc định cho ứng dụng web chuyên nghiệp. React mang đến mô hình component, Next.js bổ sung định tuyến, render và lấy dữ liệu, còn TypeScript bắt được cả loạt lỗi trước khi code của bạn chạy. Dùng đúng cách, chúng tạo ra những trang web nhanh với người truy cập và dễ chịu khi bảo trì với lập trình viên.
Dùng cẩu thả, cùng bộ công nghệ đó tạo ra những trang chậm với hàng megabyte JavaScript. Bài viết này trình bày những ý tưởng tạo nên khác biệt, kèm code bạn có thể áp dụng ngay vào dự án Next.js App Router.
Vì sao bộ công nghệ này trở thành mặc định
- React mô tả giao diện như một cây các component tái sử dụng được. Một nút bấm, một thẻ hay cả một trang đều là hàm của dữ liệu, giúp giao diện dễ đoán và dễ kiểm thử.
- Next.js biến React thành một framework hoàn chỉnh: định tuyến theo file, render phía server, bộ nhớ đệm, tối ưu hình ảnh và font, cùng quy trình build cho production.
- TypeScript thêm kiểu tĩnh cho JavaScript. Trình soạn thảo biết cấu trúc của mọi đối tượng, refactor an toàn, và nhiều lỗi lúc chạy trở thành lỗi lúc biên dịch.
Kết hợp lại, chúng đáp ứng mọi thứ từ trang marketing đến dashboard phức tạp, với một ngôn ngữ duy nhất cho cả frontend lẫn server.
Server Components thay đổi cách mặc định
Trong Next.js App Router, mọi component đều là React Server Component trừ khi bạn chọn khác. Server Components chỉ chạy trên server. Chúng có thể đọc trực tiếp từ cơ sở dữ liệu hoặc gọi API nội bộ, và JavaScript của chúng không bao giờ được gửi tới trình duyệt. Chỉ có kết quả đã render được gửi đi.
Bạn chỉ thêm chỉ thị 'use client' cho những component cần tương tác: state, effect, xử lý sự kiện hoặc API của trình duyệt. Cách làm tốt nhất là giữ các client component nhỏ và đẩy chúng xuống các nhánh lá của cây:
// app/blog/page.tsx: a Server Component (the default in the App Router)
import { getPosts } from '@/lib/posts'
import { LikeButton } from './like-button'
export default async function BlogPage() {
const posts = await getPosts() // runs on the server, so no extra API route is needed
return (
<ul>
{posts.map((post) => (
<li key={post.slug}>
<a href={`/blog/${post.slug}`}>{post.title}</a>
<LikeButton />
</li>
))}
</ul>
)
}// app/blog/like-button.tsx: only this small component ships JavaScript
'use client'
import { useState } from 'react'
export function LikeButton() {
const [liked, setLiked] = useState(false)
return (
<button type="button" aria-pressed={liked} onClick={() => setLiked((value) => !value)}>
{liked ? 'Liked' : 'Like'}
</button>
)
}Danh sách, việc truy cập dữ liệu và phần markup không tốn gì của trình duyệt. Chỉ nút bấm được hydrate. Với các trang nhiều nội dung, cách này thường loại bỏ phần lớn JavaScript phía client.
Chọn đúng chiến lược render
Next.js cho phép bạn quyết định theo từng route khi nào HTML được tạo. Chọn đúng là quyết định hiệu năng lớn nhất mà bạn đưa ra:
- Render tĩnh: HTML được tạo lúc build và phục vụ từ bộ nhớ đệm hoặc CDN. Lý tưởng cho trang marketing và tài liệu.
- Incremental Static Regeneration (ISR): trang tĩnh được làm mới sau một khoảng thời gian hoặc theo yêu cầu với
revalidatePath(). Lý tưởng cho blog, danh mục sản phẩm và trang lấy nội dung từ CMS. - Render động: HTML được tạo cho mỗi request. Bắt buộc với trang cá nhân hóa, dashboard và mọi thứ phụ thuộc vào cookie.
- Streaming: bọc những phần chậm của trang trong
<Suspense>để khung trang hiện ngay và dữ liệu chậm được truyền tới khi sẵn sàng.
Một route blog động được lưu đệm và làm mới mỗi năm phút, với metadata SEO riêng cho từng bài, trông như sau. Lưu ý rằng trong các phiên bản Next.js hiện tại, params là một Promise:
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { getPost } from '@/lib/posts'
type Props = { params: Promise<{ slug: string }> }
// Serve the cached page, regenerating it at most every five minutes
export const revalidate = 300
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params
const post = await getPost(slug)
if (!post) return {}
return {
title: post.title,
description: post.excerpt,
openGraph: { type: 'article', title: post.title, images: [post.coverUrl] },
}
}
export default async function PostPage({ params }: Props) {
const { slug } = await params
const post = await getPost(slug)
if (!post) notFound() // renders the 404 page with the correct status code
return (
<article>
<h1>{post.title}</h1>
{/* … */}
</article>
)
}TypeScript xứng đáng với công sức bỏ ra
Bật chế độ strict ngay từ commit đầu tiên; bật sau này rất vất vả. noUncheckedIndexedAccess cũng đáng bật, vì nó buộc bạn xử lý trường hợp một phần tử mảng hoặc một mục trong map không tồn tại:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true
}
}Hãy nhớ rằng kiểu biến mất khi chạy. Dữ liệu đi qua ranh giới (dữ liệu form, phản hồi API, tham số URL, biến môi trường) phải được kiểm tra hợp lệ, không chỉ khai báo kiểu. Một thư viện schema như zod cho bạn cả hai từ một định nghĩa duy nhất:
import { z } from 'zod'
export const PostSchema = z.object({
slug: z.string().regex(/^[a-z0-9-]+$/),
title: z.string().min(1).max(200),
publishedAt: z.coerce.date(),
tags: z.array(z.string()).default([]),
})
// One source of truth for the runtime check and the compile-time type
export type Post = z.infer<typeof PostSchema>
export function parsePost(input: unknown): Post {
return PostSchema.parse(input) // throws a descriptive error for invalid data
}Mô hình hóa trạng thái một cách tường minh bằng discriminated union, ví dụ { status: 'loading' } | { status: 'error'; message: string } | { status: 'ready'; data: Post[] }. Trình biên dịch khi đó buộc mọi component xử lý trạng thái đang tải và lỗi, thay vì giả định mọi thứ luôn suôn sẻ.
Thay đổi dữ liệu với Server Actions
Server Actions cho phép form gọi trực tiếp một hàm trên server mà không cần tự viết API route. Chúng tiện lợi, nhưng bên dưới vẫn là các endpoint HTTP công khai. Hãy đối xử với mọi action như một API: kiểm tra dữ liệu đầu vào, kiểm tra xác thực và phân quyền trên server, và trả về lỗi có cấu trúc để form hiển thị. Ẩn một nút trên giao diện không phải là bảo mật.
Danh sách kiểm tra hiệu năng và khả năng tiếp cận
- Dùng
next/imagevới thuộc tínhsizeschính xác để điện thoại không bao giờ tải ảnh cỡ máy tính, và chỉ đánh dấu ảnh chính phía trên màn hình làpriority. - Tải font bằng
next/fontđể tự lưu trữ và tránh xê dịch bố cục. - Lấy các dữ liệu độc lập song song bằng
Promise.allthay vì một chuỗiawaitnối tiếp nhau. - Giữ client component nhỏ và ở nhánh lá, đồng thời tải trễ các thành phần nặng như bản đồ, biểu đồ và trình soạn thảo.
- Theo dõi Core Web Vitals trên môi trường thật. "Tốt" nghĩa là Largest Contentful Paint dưới 2,5 giây, Interaction to Next Paint dưới 200 ms và Cumulative Layout Shift dưới 0,1 ở phân vị thứ 75.
- Dùng HTML có ngữ nghĩa (
<nav>,<main>,<article>,<button>thật), gắn nhãn cho mọi trường form, giữ trạng thái focus luôn hiển thị và kiểm tra độ tương phản màu.
Những điểm chính
- Mặc định dùng Server Components và chỉ thêm
'use client'ở nơi cần tương tác. - Chọn chiến lược render theo từng route: tĩnh khi có thể, ISR cho nội dung, động chỉ khi cần thiết.
- Kết hợp TypeScript strict với kiểm tra hợp lệ lúc chạy tại mọi ranh giới.
- Đo hiệu năng từ người dùng thật và coi khả năng tiếp cận là một phần của định nghĩa hoàn thành.
Kết luận
Bộ công nghệ React hiện đại mang lại lợi thế cho những đội hiểu rõ code của mình chạy ở đâu. Render trên server theo mặc định, chỉ gửi JavaScript cho những tương tác thực sự, và áp đặt kiểu cùng kiểm tra hợp lệ tại các ranh giới sẽ cho bạn những ứng dụng tải nhanh, xếp hạng tốt và vẫn dễ thay đổi khi phát triển.
Đọc thêm
- #Next.js
- #React
- #TypeScript
- #Web Performance




