Phỏng Vấn & Sự Nghiệp • 10 phút đọc • 01/06/2026

Phỏng Vấn Frontend: Trả Lời Câu Hỏi Về Render Performance Và Core Web Vitals

Bài này có bản tiếng Anh. Read in English →

Nguyen Hai Trinh

Nguyen Hai Trinh • Senior Software Engineer @ SmileViet

Màn hình theo dõi biểu đồ
Ảnh: Federal Bureau of Investigation · Public domain

Ngồi phỏng vấn, câu hỏi hay gặp nhất là: “Sao em biết chỗ nào đang chậm?” Trả lời “em có tối ưu performance” thì chưa nói được gì.

Người kể được “LCP 3.8s, sửa xong còn dưới 1.2s, đây là cách em đo” thì khác hẳn. Mấy ví dụ dưới đây lấy từ dashboard analytics ~150 chart ở Cognisian.

Re-render thừa đến từ ba chỗ

Câu mở đầu kinh điển. Trả lời “do props thay đổi” là trượt. Ba nguyên nhân phổ biến nhất nằm ở đây:

  1. Reference mới mỗi lần render: object/array/function được tạo inline trong JSX (<Chart config={{ theme: 'dark' }} />) tạo ra reference mới mỗi lần component cha render, khiến React.memo ở component con vô dụng vì so sánh reference luôn khác.
  2. Context thay đổi lan toả rộng: một Context Provider bọc quá nhiều component, khi value của context đổi (dù chỉ một phần nhỏ), toàn bộ cây con re-render — kể cả component không dùng tới phần value đã đổi.
  3. State đặt sai tầng: state chỉ cần cho một component con nhỏ nhưng lại đặt ở component cha lớn, khiến mỗi lần state đổi, toàn bộ cây con (kể cả phần không liên quan) re-render theo.

Trả lời thuyết phục thì phải kèm cách fix tương ứng: useMemo/useCallback cho case 1, tách context thành nhiều context nhỏ theo domain cho case 2, và đẩy state xuống gần nơi sử dụng nhất cho case 3.

Ba nguyên nhân, ba hướng sửa, ghép lại thành một bảng đối chiếu:

flowchart TD
    R["Re-render thừa"] --> C1["1. Reference mới mỗi lần render<br/>object, array, function tạo inline trong JSX"]
    R --> C2["2. Context thay đổi lan toả rộng<br/>Provider bọc quá nhiều component"]
    R --> C3["3. State đặt sai tầng<br/>state ở cha lớn, chỉ dùng ở con nhỏ"]
    C1 --> F1["useMemo / useCallback"]
    C2 --> F2["Tách nhiều context nhỏ theo domain"]
    C3 --> F3["Đẩy state xuống gần nơi sử dụng nhất"]

Đo Core Web Vitals thế nào cho đúng

Định nghĩa thì nhiều người nói được: LCP đo tốc độ load phần tử lớn nhất, CLS đo độ dịch chuyển layout, INP đo độ trễ phản hồi tương tác. Đo trong thực tế thế nào mới là phần khó. Câu trả lời tốt nên nhắc tới:

  • Lab data (Lighthouse, WebPageTest) để debug chủ động trong lúc phát triển — kiểm soát được điều kiện mạng/CPU.
  • Field data (Chrome UX Report, package web-vitals gửi RUM về analytics) để biết trải nghiệm thật của người dùng. Lab data không phản ánh đúng thiết bị và mạng thực tế của họ.
import { onLCP, onINP, onCLS } from "web-vitals";

onLCP((metric) => sendToAnalytics("LCP", metric.value));
onINP((metric) => sendToAnalytics("INP", metric.value));
onCLS((metric) => sendToAnalytics("CLS", metric.value));

Con số trước/sau: dashboard ~150 chart ở Cognisian

“Em có tối ưu performance” nghe không thuyết phục bằng một cặp số trước/sau. Ví dụ mình hay dùng: dashboard analytics ở Cognisian ban đầu LCP ~3.8s, vì toàn bộ ~150 chart fetch và render ngay khi mount trang. Hướng xử lý:

  • Dùng IntersectionObserver chỉ render chart khi cuộn tới gần viewport (virtualize phần không nhìn thấy).
  • Tách query fetch theo từng chart thay vì một request khổng lồ gộp tất cả — chart đầu tiên (above-the-fold) có data sớm hơn nhiều.
  • Dùng skeleton loading đúng kích thước để tránh CLS khi chart thật load xong.

Kết quả: LCP giảm từ 3.8s xuống dưới 1.2s. CLS gần như về 0, vì skeleton giữ đúng không gian trước khi chart thật render.

useMemo/useCallback: khi nào thật sự cần

Đây là chỗ bị hỏi xoáy sâu nhất. Cũng là chỗ dễ lộ “học thuộc” với “hiểu bản chất”.

useMemo/useCallback không miễn phí. Bản thân nó cũng tốn chi phí so sánh dependency array mỗi lần render. Chỉ nên dùng khi:

  • Giá trị tính toán thực sự nặng (ví dụ xử lý mảng hàng nghìn phần tử), hoặc
  • Reference cần ổn định để tránh re-render component con được bọc React.memo, hoặc để tránh re-trigger useEffect phụ thuộc vào nó.

Dùng useMemo cho mọi phép tính nhỏ (ví dụ a + b) là phản tác dụng — chi phí so sánh dependency còn cao hơn chi phí tính lại giá trị đơn giản đó.

Bẫy: tối ưu khi chưa đo

Câu trả lời sai phổ biến là liệt kê hàng loạt kỹ thuật tối ưu — memo hoá mọi thứ, virtualize mọi list, code-split mọi route — mà không nói tới bước đầu tiên bắt buộc: đo trước khi tối ưu.

Interviewer giỏi sẽ hỏi ngược: “làm sao bạn biết chỗ đó là bottleneck?” Câu trả lời tốt nhất luôn bắt đầu bằng React DevTools Profiler hoặc Chrome Performance tab để xác định chính xác component/hàm nào đang tốn thời gian, trước khi áp bất kỳ kỹ thuật tối ưu nào.

Tối ưu mà không đo trước thường dẫn tới việc thêm useMemo khắp nơi, code khó đọc hơn, nhưng performance thực tế không cải thiện đáng kể — vì bottleneck thật nằm ở chỗ khác, ví dụ network waterfall chứ không phải re-render.

Khung trả lời dùng được cho mọi câu hỏi performance

Nêu đúng nguyên nhân kỹ thuật, cách đo để xác nhận, cách fix, rồi số liệu trước/sau. Bốn bước. Áp được cho gần như mọi câu hỏi performance trong vòng phỏng vấn frontend.

Đi đúng thứ tự thì câu trả lời tự có sức nặng:

flowchart LR
    A["1. Nêu đúng nguyên nhân kỹ thuật"] --> B["2. Cách đo để xác nhận"]
    B --> C["3. Cách fix"]
    C --> D["4. Số liệu trước/sau"]

Người “thuộc lòng” thường nhảy thẳng vào bước 3. Bước 2 mới là chỗ phân biệt.

Cần người cho dự án contract hoặc freelance?

Mình nhận dự án fullstack — backend Go, frontend React/Next.js, hoặc cả hai đầu. Gửi vài dòng về phạm vi công việc và thời gian, mình phản hồi trong 48 giờ.