Series 7 — Web Quality – Engineering

Bài 3: Testing – Unit Testing, Integration Testing và End-to-End Testing trong ứng dụng web

1. Vì sao ứng dụng web cần Testing?

Trong quá trình phát triển Frontend, chúng ta thường kiểm tra ứng dụng bằng cách chạy website trên trình duyệt, nhấn vào các nút, nhập dữ liệu vào biểu mẫu và quan sát kết quả. Cách kiểm tra này hữu ích, nhưng khi ứng dụng ngày càng lớn, việc kiểm tra thủ công mọi chức năng sau mỗi lần thay đổi sẽ mất nhiều thời gian và dễ bỏ sót lỗi.

Hãy tưởng tượng bạn đang phát triển một website bán hàng. Sau khi thêm chức năng giảm giá, phép tính tổng tiền có thể bị sai. Một thay đổi trong component giỏ hàng cũng có thể khiến nút thanh toán không hoạt động. Nếu không phát hiện sớm, lỗi có thể xuất hiện khi người dùng sử dụng ứng dụng thực tế.

Testing là quá trình kiểm tra phần mềm để xác minh rằng ứng dụng hoạt động đúng theo yêu cầu và giúp phát hiện những lỗi có thể xảy ra.

Trong phát triển web, ba cấp độ kiểm thử phổ biến là:

  • Unit Testing
  • Integration Testing
  • End-to-End Testing (E2E)

Mỗi loại tập trung vào một phạm vi khác nhau. Hiểu được sự khác biệt giữa chúng giúp Developer lựa chọn cách kiểm thử phù hợp thay vì chỉ viết thật nhiều test mà không rõ mục đích.

2. Unit Testing

Unit Testing là gì?

Unit Testing là kiểm thử một đơn vị nhỏ của chương trình một cách độc lập. Đơn vị đó có thể là một hàm, một phương thức hoặc một phần logic có phạm vi rõ ràng.

Mục tiêu là xác minh rằng với đầu vào cụ thể, đơn vị đang kiểm thử tạo ra kết quả đúng như mong đợi.

Ví dụ, website bán hàng có hàm tính tổng tiền sau khi giảm giá. Đây là một đơn vị logic có thể kiểm thử mà không cần mở trình duyệt hay kết nối đến cơ sở dữ liệu.

Ví dụ với JavaScript

Giả sử chúng ta có hàm tính giá sau giảm giá:

export function calculateDiscount(price, percent) {
  return price * (1 - percent / 100);
}

Ta có thể viết Unit Test bằng Vitest:

import { describe, expect, test } from "vitest";
import { calculateDiscount } from "./discount";

describe("calculateDiscount", () => {
  test("calculates a 20% discount", () => {
    expect(calculateDiscount(100, 20)).toBe(80);
  });

  test("returns the original price for 0% discount", () => {
    expect(calculateDiscount(100, 0)).toBe(100);
  });
});

Trong ví dụ trên:

  • describe nhóm những test có liên quan.
  • test mô tả một trường hợp cần kiểm tra.
  • expect xác định kết quả mong đợi.
  • toBe so sánh giá trị thực tế với giá trị mong đợi.

Vitest là một lựa chọn phổ biến cho các dự án JavaScript và TypeScript sử dụng Vite. https://vitest.dev/guide/

Nên kiểm thử những trường hợp nào?

Không nên chỉ kiểm tra trường hợp hoạt động bình thường. Hãy xem xét thêm các trường hợp biên (edge cases).

Ví dụ, với hàm tính giá sau giảm giá, bạn có thể đặt câu hỏi:

  • Giá bằng 0 thì kết quả là bao nhiêu?
  • Phần trăm giảm giá bằng 0 thì sao?
  • Phần trăm giảm giá vượt quá 100 có được phép không?
  • Giá âm hoặc dữ liệu không hợp lệ được xử lý như thế nào?

Cách xử lý các trường hợp này phụ thuộc vào yêu cầu nghiệp vụ. Test cần phản ánh yêu cầu đã xác định, không phải tự đặt ra quy tắc tùy ý.

Ưu điểm và hạn chế

Unit Testing thường chạy nhanh, dễ xác định nguyên nhân lỗi và phù hợp với những hàm có logic rõ ràng.

Tuy nhiên, một hàm có thể hoạt động đúng khi kiểm tra độc lập nhưng vẫn gặp lỗi khi kết hợp với API, component hoặc những phần khác của ứng dụng. Đó là lý do chúng ta cần những cấp độ kiểm thử tiếp theo.

3. Integration Testing

Integration Testing là gì?

Integration Testing kiểm tra cách nhiều thành phần phối hợp với nhau.

Trong một ứng dụng React, điều cần kiểm tra có thể là mối liên hệ giữa component, state, sự kiện người dùng và dữ liệu trả về từ API.

Ví dụ, một biểu mẫu đăng nhập có thể bao gồm:

  1. Người dùng nhập email và mật khẩu.
  2. Người dùng nhấn nút đăng nhập.
  3. Component gọi hàm xử lý đăng nhập.
  4. Ứng dụng cập nhật trạng thái khi đăng nhập thành công hoặc thất bại.
  5. Giao diện hiển thị kết quả phù hợp.

Một Unit Test có thể kiểm tra riêng hàm xác thực dữ liệu. Integration Test có thể kiểm tra cả biểu mẫu phản hồi đúng khi người dùng nhập dữ liệu và gửi biểu mẫu.

Ví dụ với React Testing Library

Giả sử có component đăng nhập đơn giản:

export function LoginForm({ onLogin }) {
  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();

        const formData = new FormData(event.currentTarget);

        onLogin({
          email: formData.get("email"),
        });
      }}
    >
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required />

      <button type="submit">Đăng nhập</button>
    </form>
  );
}

Chúng ta muốn kiểm tra rằng khi người dùng nhập email và gửi biểu mẫu, hàm onLogin nhận được dữ liệu chính xác.

Ví dụ test với Vitest và React Testing Library:

import { expect, test, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { LoginForm } from "./LoginForm";

test("submits the entered email", async () => {
  const user = userEvent.setup();
  const onLogin = vi.fn();

  render(<LoginForm onLogin={onLogin} />);

  await user.type(
    screen.getByLabelText("Email"),
    "[email protected]"
  );

  await user.click(
    screen.getByRole("button", { name: "Đăng nhập" })
  );

  expect(onLogin).toHaveBeenCalledWith({
    email: "[email protected]",
  });
});

Đoạn test kiểm tra cách component hoạt động khi người dùng tương tác với nó. Bạn cần cài đặt và cấu hình Vitest, React Testing Library, user-event cùng môi trường DOM phù hợp để chạy ví dụ này.

Vì sao nên kiểm tra theo hành vi người dùng?

Khi kiểm thử giao diện, nên ưu tiên những gì người dùng có thể nhìn thấy hoặc thao tác, chẳng hạn:

  • Tìm nút theo tên hiển thị.
  • Tìm ô nhập thông qua nhãn.
  • Nhập dữ liệu và nhấn nút.
  • Kiểm tra thông báo hoặc kết quả xuất hiện.

Cách tiếp cận này thường giúp test ít phụ thuộc vào cấu trúc HTML nội bộ hoặc tên class CSS. Khi giao diện được tái cấu trúc nhưng hành vi vẫn giữ nguyên, test có thể không cần thay đổi.

Integration Testing có thay thế Unit Testing không?

Không. Hai loại kiểm thử bổ sung cho nhau.

Unit Testing giúp xác định lỗi trong logic nhỏ. Integration Testing giúp phát hiện vấn đề khi những phần đó kết hợp với nhau.

Tuy nhiên, ngay cả khi các component hoạt động đúng trong môi trường test, ứng dụng thực tế vẫn có thể gặp lỗi liên quan đến định tuyến, cấu hình máy chủ hoặc luồng nghiệp vụ xuyên suốt. Đây là lúc End-to-End Testing phát huy tác dụng.

4. End-to-End Testing (E2E)

E2E Testing là gì?

End-to-End Testing kiểm thử một luồng hoạt động xuyên suốt ứng dụng từ góc nhìn người dùng, thường bằng cách điều khiển trình duyệt thực tế.

Thay vì chỉ kiểm tra một hàm hoặc một component, E2E có thể kiểm tra cả quy trình như đăng nhập, tìm kiếm sản phẩm, thêm sản phẩm vào giỏ hàng và hoàn tất đơn hàng.

Ví dụ về luồng E2E

Với website bán hàng, một kịch bản E2E có thể gồm:

  1. Mở trang sản phẩm.
  2. Chọn một sản phẩm.
  3. Thêm sản phẩm vào giỏ hàng.
  4. Mở giỏ hàng và kiểm tra sản phẩm.
  5. Tiến hành thanh toán.
  6. Xác nhận trang hiển thị trạng thái đặt hàng thành công.

Nếu bất kỳ bước quan trọng nào không hoạt động, test sẽ phát hiện lỗi trong luồng nghiệp vụ.

Ví dụ với Playwright

Playwright là công cụ hỗ trợ tự động hóa trình duyệt và kiểm thử E2E.

import { test, expect } from "@playwright/test";

test("user can open the product page", async ({ page }) => {
  await page.goto("/products");

  await expect(
    page.getByRole("heading", { name: "Sản phẩm" })
  ).toBeVisible();
});

Ví dụ này mở trang sản phẩm và kiểm tra tiêu đề có xuất hiện hay không. Trong dự án thực tế, cần cấu hình địa chỉ ứng dụng và chuẩn bị dữ liệu phù hợp trước khi chạy test.

Từ đây, bạn có thể mở rộng kịch bản để kiểm tra việc thêm sản phẩm vào giỏ hàng hoặc gửi biểu mẫu đặt hàng.

Ưu điểm và hạn chế

E2E Testing có thể phát hiện những lỗi mà Unit Test hoặc Integration Test không nhìn thấy, đặc biệt là lỗi liên quan đến luồng nghiệp vụ và sự phối hợp giữa nhiều lớp của ứng dụng.

Tuy nhiên, E2E thường chạy chậm hơn, cần môi trường phù hợp và có thể dễ thất bại nếu dữ liệu test hoặc trạng thái ứng dụng không ổn định.

Vì vậy, không nên dùng E2E để kiểm tra mọi trường hợp nhỏ. Những logic đơn giản thường phù hợp hơn với Unit Testing.

5. So sánh ba loại Testing

Tiêu chí Unit Testing Integration Testing E2E Testing
Phạm vi Một đơn vị logic Nhiều thành phần phối hợp Luồng nghiệp vụ xuyên suốt
Ví dụ Tính giá sau giảm Biểu mẫu gọi handler đúng Người dùng đặt hàng
Tốc độ Thường nhanh nhất Trung bình Thường chậm nhất
Phát hiện lỗi Logic cục bộ Tương tác giữa các phần Lỗi trong luồng tổng thể
Công cụ tham khảo Vitest Vitest, React Testing Library Playwright

Không có một tỷ lệ cố định phù hợp cho mọi dự án. Việc phân bổ test phụ thuộc vào độ phức tạp, rủi ro, yêu cầu nghiệp vụ và chi phí bảo trì.

Một chiến lược hợp lý thường kiểm tra nhiều logic ở cấp Unit, kiểm tra các tương tác quan trọng ở cấp Integration và dành E2E cho những luồng người dùng có giá trị cao.

6. Testing trong quy trình phát triển Frontend

Testing không chỉ được thực hiện ở giai đoạn cuối cùng trước khi bàn giao sản phẩm. Nó có thể được tích hợp xuyên suốt quá trình phát triển.

Một quy trình đơn giản có thể như sau:

  1. Developer viết code và tạo các test tương ứng.
  2. Chạy test tại máy cá nhân để phát hiện lỗi sớm.
  3. Tạo Pull Request để người khác review code.
  4. Hệ thống CI tự động chạy test khi có thay đổi.
  5. Chỉ đưa code vào nhánh phát hành khi những điều kiện kiểm tra bắt buộc đã đạt yêu cầu.

Test tự động giúp giảm công việc kiểm tra lặp lại, nhưng không thay thế hoàn toàn việc kiểm tra thủ công, đánh giá trải nghiệm người dùng hoặc kiểm thử khả năng tiếp cận.

7. Bài tập thực hành

Hãy sử dụng một ứng dụng React nhỏ có chức năng quản lý danh sách công việc (Todo List).

Nhiệm vụ 1: Unit Testing

Viết hàm lọc các công việc đã hoàn thành và kiểm tra kết quả với nhiều bộ dữ liệu.

Nhiệm vụ 2: Integration Testing

Kiểm tra việc nhập tên công việc, nhấn nút thêm và xác nhận công việc mới xuất hiện trên giao diện.

Nhiệm vụ 3: E2E Testing

Viết kịch bản mở ứng dụng, thêm một công việc, đánh dấu công việc đã hoàn thành và xác nhận trạng thái hiển thị đúng.

Nhiệm vụ 4: Đánh giá

Với mỗi test, hãy giải thích tại sao bạn chọn cấp độ đó và những lỗi nào test có thể phát hiện.

Kết luận

Unit Testing, Integration Testing và End-to-End Testing giải quyết những phạm vi kiểm thử khác nhau. Unit Testing tập trung vào logic nhỏ, Integration Testing kiểm tra sự phối hợp giữa các thành phần, còn E2E

 

 

 

 

Bài 4: Web Performance – Core Web Vitals, Code Splitting, Lazy Loading và tối ưu hiệu năng

 

 

 

1. Web Performance là gì?

Một website có giao diện đẹp và đầy đủ chức năng chưa chắc mang lại trải nghiệm tốt nếu người dùng phải chờ quá lâu để xem nội dung, nhấn nút nhưng không thấy phản hồi hoặc gặp tình trạng bố cục liên tục thay đổi khi trang tải.

Web Performance là việc đo lường và tối ưu hiệu năng của ứng dụng web nhằm giúp trang tải nhanh hơn, phản hồi tương tác tốt hơn và hiển thị ổn định hơn.

Đối với Frontend Developer, tối ưu hiệu năng không chỉ có nghĩa là giảm dung lượng JavaScript hay tối ưu hình ảnh. Chúng ta còn cần hiểu trình duyệt tải tài nguyên, thực thi JavaScript, dựng giao diện và xử lý tương tác như thế nào.

Trong bài viết này, chúng ta sẽ tìm hiểu bốn nhóm kiến thức quan trọng:

  • Core Web Vitals
  • Code Splitting
  • Lazy Loading
  • Rendering & Resource Optimization

2. Core Web Vitals

Core Web Vitals là gì?

Core Web Vitals là nhóm chỉ số của Google dùng để đánh giá những khía cạnh quan trọng trong trải nghiệm thực tế của người dùng trên website.

Hiện tại, ba chỉ số chính là:

  • LCP (Largest Contentful Paint): thời gian hiển thị nội dung chính.
  • INP (Interaction to Next Paint): khả năng phản hồi sau tương tác.
  • CLS (Cumulative Layout Shift): mức độ ổn định của bố cục.

Ba chỉ số này tập trung vào ba câu hỏi: nội dung có xuất hiện nhanh không, website có phản hồi nhanh không và giao diện có bị dịch chuyển bất ngờ không? https://web.dev/articles/vitals

2.1. LCP – Largest Contentful Paint

LCP đo thời gian phần tử nội dung lớn nhất được hiển thị trong vùng nhìn thấy của trình duyệt. Phần tử đó có thể là ảnh lớn, khối văn bản hoặc hình ảnh chính của trang.

Ví dụ, khi người dùng mở trang chủ một website nha khoa, ảnh banner lớn hoặc khối nội dung chính có thể là phần tử LCP.

Ngưỡng đánh giá thường dùng:

LCP Đánh giá
Không quá 2,5 giây Tốt
Trên 2,5 đến 4 giây Cần cải thiện
Trên 4 giây Kém

Để cải thiện LCP, hãy xác định phần tử chính đang tải chậm và tìm nguyên nhân thực sự.

Một số hướng tối ưu phổ biến:

  • Nén và sử dụng định dạng hình ảnh phù hợp.
  • Giảm thời gian phản hồi của máy chủ.
  • Không trì hoãn việc tải ảnh chính của trang bằng lazy loading.
  • Ưu tiên tài nguyên quan trọng và giảm CSS hoặc JavaScript cản trở quá trình hiển thị.

Ví dụ, với ảnh banner quan trọng nằm ngay đầu trang:

<img
  src="/images/hero.webp"
  alt="Dịch vụ nha khoa"
  fetchpriority="high"
  width="1200"
  height="600"
/>

fetchpriority="high" là gợi ý ưu tiên tải tài nguyên. Chỉ nên sử dụng khi phù hợp, không áp dụng tùy tiện cho tất cả hình ảnh.

2.2. INP – Interaction to Next Paint

INP đo khả năng phản hồi của trang đối với các tương tác người dùng, chẳng hạn nhấn nút, chọn một mục hoặc nhập dữ liệu. Chỉ số này phản ánh độ trễ từ tương tác đến khi trình duyệt có thể hiển thị phản hồi tiếp theo.

Ngưỡng đánh giá:

INP Đánh giá
Không quá 200 ms Tốt
Trên 200 đến 500 ms Cần cải thiện
Trên 500 ms Kém

Một website có thể tải nhanh nhưng vẫn có INP kém nếu JavaScript thực hiện quá nhiều công việc trên luồng chính (main thread), khiến trình duyệt không thể phản hồi tương tác kịp thời.

Ví dụ, một thao tác tìm kiếm phải xử lý hàng chục nghìn bản ghi ngay trong trình duyệt có thể khiến giao diện bị đơ.

Một số hướng cải thiện:

  • Giảm lượng JavaScript cần thực thi.
  • Chia nhỏ những tác vụ JavaScript kéo dài.
  • Tránh cập nhật giao diện quá lớn sau mỗi thao tác.
  • Debounce những thao tác như tìm kiếm khi nhập liệu nếu phù hợp.
  • Đưa công việc tính toán nặng sang Web Worker khi kiến trúc cho phép.

2.3. CLS – Cumulative Layout Shift

CLS đo mức độ dịch chuyển bất ngờ của các phần tử trong bố cục khi trang đang hiển thị.

Ví dụ, người dùng chuẩn bị nhấn nút đăng ký nhưng một ảnh chưa tải xong khiến nội dung phía trên thay đổi kích thước, đẩy nút xuống vị trí khác.

Ngưỡng đánh giá:

CLS Đánh giá
Không quá 0,1 Tốt
Trên 0,1 đến 0,25 Cần cải thiện
Trên 0,25 Kém

Để giảm CLS, hãy dành trước không gian cho hình ảnh và nội dung có kích thước thay đổi.

<img
  src="/images/clinic.webp"
  alt="Không gian phòng khám"
  width="800"
  height="500"
/>

Kích thước rõ ràng giúp trình duyệt tính toán bố cục trước khi hình ảnh tải xong.

Ngoài ra, cần cẩn thận khi chèn banner, quảng cáo hoặc thông báo vào phía trên nội dung đã hiển thị. Nếu phải bổ sung nội dung động, hãy thiết kế không gian và hành vi hiển thị để tránh làm người dùng mất vị trí đang tương tác.

Vì sao cần quan tâm cả ba chỉ số?

Một website có LCP tốt nhưng INP kém vẫn có thể khiến người dùng khó chịu khi thao tác. Tương tự, trang tải nhanh nhưng CLS cao có thể làm người dùng nhấn nhầm nút.

Do đó, không nên chỉ tập trung vào một chỉ số duy nhất. Hãy đo lường và xác định vấn đề cụ thể trước khi tối ưu.

3. Code Splitting

Code Splitting là gì?

Code Splitting là kỹ thuật chia mã JavaScript thành nhiều phần nhỏ thay vì tải toàn bộ mã ứng dụng ngay từ đầu.

Trong một ứng dụng lớn, người dùng có thể chỉ cần xem trang chủ nhưng trình duyệt vẫn phải tải mã phục vụ trang quản trị, biểu đồ thống kê hoặc trình chỉnh sửa nội dung nếu tất cả được gộp vào một bundle lớn.

Việc chia mã giúp giảm lượng JavaScript cần tải ban đầu và có thể cải thiện thời gian hiển thị cũng như khả năng tương tác.

Ví dụ với React

React hỗ trợ lazy và Suspense để tải component theo nhu cầu.

import { lazy, Suspense, useState } from "react";

const AdminDashboard = lazy(
  () => import("./AdminDashboard")
);

export default function App() {
  const [showAdmin, setShowAdmin] = useState(false);

  return (
    <main>
      <h1>Trang chủ</h1>

      <button onClick={() => setShowAdmin(true)}>
        Mở bảng quản trị
      </button>

      {showAdmin && (
        <Suspense fallback={<p>Đang tải bảng quản trị...</p>}>
          <AdminDashboard />
        </Suspense>
      )}
    </main>
  );
}

Trong ví dụ này:

  • lazy cho phép React tải component từ một module riêng khi cần.
  • Suspense hiển thị nội dung tạm thời trong lúc component đang được tải.
  • showAdmin quyết định thời điểm bảng quản trị xuất hiện.

Khi người dùng chưa mở bảng quản trị, ứng dụng không cần tải component này ngay từ đầu nếu công cụ build đã tạo chunk riêng như mong đợi.

Code Splitting trong thực tế

Code Splitting có thể được thực hiện ở nhiều cấp độ:

Theo route: mỗi trang hoặc nhóm trang được tải thành những phần riêng.

Theo component: những component lớn hoặc ít được sử dụng được tải khi cần.

Theo thư viện: những thư viện nặng chỉ được tải ở những nơi thực sự sử dụng chúng.

Tuy nhiên, chia mã không phải lúc nào cũng giúp ứng dụng nhanh hơn. Quá nhiều chunk nhỏ có thể làm tăng số lần yêu cầu tài nguyên hoặc tạo ra độ trễ khi người dùng cần mở một chức năng.

Mục tiêu là giảm tải ban đầu nhưng vẫn đảm bảo những phần quan trọng được tải đúng thời điểm.

4. Lazy Loading

Lazy Loading là gì?

Lazy Loading là kỹ thuật trì hoãn việc tải tài nguyên hoặc thành phần cho đến khi chúng thực sự cần thiết.

Ví dụ, một trang giới thiệu phòng khám có thể chứa hàng chục hình ảnh, video và các phần nội dung nằm phía dưới. Người dùng chưa cần tải tất cả những tài nguyên đó ngay khi mở trang.

Lazy Loading giúp trình duyệt tập trung tài nguyên cho phần nội dung người dùng đang nhìn thấy hoặc sắp sử dụng.

4.1. Lazy Loading hình ảnh

Với ảnh nằm bên dưới vùng nhìn thấy ban đầu, HTML hỗ trợ thuộc tính loading="lazy":

<img
  src="/images/doctor.webp"
  alt="Bác sĩ đang tư vấn"
  width="640"
  height="480"
  loading="lazy"
/>

Trình duyệt có thể trì hoãn tải ảnh cho đến khi ảnh gần đi vào vùng nhìn thấy.

Lưu ý: không nên áp dụng lazy loading cho ảnh chính có khả năng là phần tử LCP. Việc trì hoãn ảnh quan trọng có thể khiến nội dung chính xuất hiện chậm hơn. https://web.dev/articles/lcp

4.2. Lazy Loading component

Trong React, có thể sử dụng lazy cùng Suspense như ví dụ Code Splitting ở trên. Hai khái niệm có liên quan nhưng không hoàn toàn đồng nghĩa:

  • Code Splitting chia mã thành những phần có thể tải riêng.
  • Lazy Loading trì hoãn việc tải cho đến khi cần.

Code Splitting tạo điều kiện để lazy loading hoạt động hiệu quả, còn lazy loading quyết định thời điểm một phần tài nguyên được yêu cầu.

4.3. Lazy Loading trong Next.js

Với Next.js, có thể sử dụng cơ chế tải động cho những component phù hợp, chẳng hạn một trình chỉnh sửa hoặc biểu đồ chỉ xuất hiện khi người dùng mở một chức năng.

Tuy nhiên, cách triển khai cần phù hợp với App Router, Server Components và Client Components. Không nên mặc định rằng mọi component đều cần tải động; hãy ưu tiên những phần có chi phí tải đáng kể và không cần thiết cho lần hiển thị đầu tiên.

5. Rendering & Resource Optimization

Tối ưu hiệu năng không chỉ dừng ở Code Splitting và Lazy Loading. Chúng ta còn cần giảm chi phí render và tối ưu tài nguyên được tải về.

5.1. Tối ưu hình ảnh

Hình ảnh có thể chiếm phần lớn dung lượng tải của một trang web. Vì vậy, cần:

  • Chọn kích thước ảnh phù hợp với kích thước hiển thị.
  • Sử dụng định dạng hiện đại như WebP hoặc AVIF khi phù hợp.
  • Nén ảnh để giảm dung lượng mà vẫn giữ chất lượng chấp nhận được.
  • Khai báo kích thước để hạn chế dịch chuyển bố cục.
  • Chỉ lazy-load những ảnh không cần thiết ngay từ đầu.

Với Next.js, component next/image hỗ trợ các tính năng tối ưu hình ảnh khi được cấu hình và sử dụng phù hợp với môi trường triển khai.

5.2. Giảm JavaScript không cần thiết

JavaScript có thể làm tăng thời gian tải, phân tích cú pháp, biên dịch và thực thi.

Một số cách tối ưu:

  • Xóa thư viện hoặc đoạn code không còn sử dụng.
  • Tránh đưa thư viện lớn vào bundle ban đầu nếu không cần.
  • Sử dụng Code Splitting ở những điểm phù hợp.
  • Kiểm tra bundle để xác định phần nào chiếm nhiều dung lượng.
  • Hạn chế xử lý đồng bộ nặng trên main thread.

Không nên chỉ nhìn vào kích thước bundle rồi kết luận ứng dụng nhanh hay chậm. Cần kết hợp dữ liệu tải trang, thực thi JavaScript và trải nghiệm thực tế.

5.3. Tránh render không cần thiết

Trong React, việc component render lại không phải lúc nào cũng là vấn đề. Render là hoạt động bình thường của React khi state hoặc props thay đổi.

Điều cần quan tâm là những lần render hoặc tính toán có thực sự gây ra chi phí đáng kể hay không.

Một số hướng cải thiện:

  • Giữ state ở phạm vi hợp lý.
  • Tránh truyền dữ liệu mới không cần thiết khiến nhiều component cập nhật.
  • Dùng React.memo, useMemo hoặc useCallback khi có lý do rõ ràng và đo lường cho thấy chúng hữu ích.
  • Tránh tối ưu hóa quá sớm khi chưa xác định được điểm nghẽn.

Việc sử dụng useMemo hoặc useCallback ở mọi nơi không tự động làm ứng dụng nhanh hơn. Chúng cũng có chi phí và có thể khiến code khó hiểu hơn nếu dùng không cần thiết.

5.4. Tối ưu tài nguyên mạng

Các tài nguyên như CSS, font, JavaScript, hình ảnh và API đều có thể ảnh hưởng đến tốc độ tải trang.

Hãy cân nhắc:

  • Nén tài nguyên và sử dụng cơ chế cache phù hợp.
  • Dùng CDN khi phù hợp với kiến trúc.
  • Giảm các yêu cầu mạng không cần thiết.
  • Tải trước tài nguyên quan trọng khi có cơ sở rõ ràng.
  • Tránh tải những tài nguyên không cần thiết cho nội dung đầu tiên.

Điều quan trọng là không tối ưu theo cảm tính. Hãy sử dụng công cụ đo lường để tìm đúng tài nguyên hoặc tác vụ đang gây chậm.

6. Công cụ đo lường hiệu năng

Có một số công cụ hữu ích dành cho Frontend Developer:

Chrome DevTools

Dùng tab Performance để phân tích hoạt động của main thread, tab Network để kiểm tra yêu cầu mạng và kích thước tài nguyên, đồng thời quan sát thời điểm các thành phần được tải.

Lighthouse

Đánh giá nhiều khía cạnh của trang web, bao gồm hiệu năng và một số vấn đề có thể ảnh hưởng đến trải nghiệm người dùng. Kết quả trong môi trường kiểm thử không hoàn toàn giống dữ liệu sử dụng thực tế.

PageSpeed Insights

Cung cấp kết quả phân tích và, khi có dữ liệu phù hợp, thông tin về trải nghiệm thực tế của người dùng.

Khi đánh giá hiệu năng, cần phân biệt:

  • Lab data: dữ liệu thu được trong môi trường kiểm thử có kiểm soát.
  • Field data: dữ liệu từ trải nghiệm thực tế của người dùng.

Lab data giúp tái hiện và tìm nguyên nhân lỗi. Field data cho biết người dùng thực sự trải nghiệm website như thế nào. Hai loại dữ liệu bổ sung cho nhau.

7. Quy trình tối ưu hiệu năng

Khi phát hiện website tải chậm, đừng lập tức thêm useMemo, lazy-load mọi hình ảnh hoặc chia nhỏ tất cả component.

Hãy thực hiện theo quy trình:

  1. Đo lường: dùng Lighthouse, DevTools hoặc PageSpeed Insights để xác định vấn đề.
  2. Tìm nguyên nhân: kiểm tra LCP, INP, CLS, kích thước tài nguyên và hoạt động của main thread.
  3. Chọn giải pháp: xử lý điểm nghẽn có tác động lớn nhất trước.
  4. Đo lại: so sánh kết quả trước và sau khi thay đổi.
  5. Theo dõi: kiểm tra lại trong điều kiện gần với trải nghiệm thực tế của người dùng.

Ví dụ, nếu LCP chậm vì ảnh banner quá lớn, giảm dung lượng và ưu tiên tải ảnh có thể hiệu quả hơn việc tối ưu một hàm JavaScript nhỏ không liên quan đến quá trình hiển thị ban đầu.

8. Bài tập thực hành

Hãy chọn một dự án Frontend bạn đang xây dựng, chẳng hạn landing page giới thiệu dịch vụ nha khoa.

Nhiệm vụ 1: Đánh giá Core Web Vitals

Chạy công cụ đo hiệu năng và ghi lại LCP, INP, CLS nếu có dữ liệu tương ứng. Xác định chỉ số nào cần cải thiện.

Nhiệm vụ 2: Tối ưu hình ảnh

Kiểm tra kích thước và định dạng ảnh. Tối ưu ảnh banner chính nhưng không trì hoãn việc tải ảnh quan trọng bằng lazy loading.

Nhiệm vụ 3: Áp dụng Code Splitting

Chọn một component lớn hoặc ít sử dụng, chẳng hạn biểu đồ thống kê hoặc bảng quản trị, rồi tách mã để tải khi cần.

Nhiệm vụ 4: Áp dụng Lazy Loading

Trì hoãn tải những hình ảnh nằm bên dưới vùng nhìn thấy ban đầu. Kiểm tra xem có ảnh nào quan trọng đối với LCP bị trì hoãn nhầm hay không.

Nhiệm vụ 5: Đo lường lại

So sánh kết quả trước và sau tối ưu. Ghi lại thay đổi nào mang lại hiệu quả, thay đổi nào không đáng kể và lý do.

Kết luận

Web Performance là một phần quan trọng của chất lượng ứng dụng web. Core Web Vitals giúp đánh giá trải nghiệm tải trang, tương tác và độ ổn định của giao diện. Code Splitting giúp giảm lượng mã cần tải ban đầu, Lazy Loading trì hoãn tài nguyên chưa cần thiết, còn Rendering & Resource Optimization giúp giảm chi phí hiển thị và tải tài nguyên.

Đối với Frontend Developer, mục tiêu không phải là áp dụng càng nhiều kỹ thuật tối ưu càng tốt, mà là xác định đúng vấn đề, chọn giải pháp phù hợp và kiểm tra kết quả bằng dữ liệu.

Kết hợp Testing với Web Performance sẽ giúp bạn xây dựng ứng dụng không chỉ hoạt động đúng mà còn phản hồi tốt và mang lại trải nghiệm ổn định cho người dùng.

Hoàn thành Series 7 — Web Quality & Engineering.

Bài viết khác

Series 7 — Web Quality – Engineering : Authentication & Authorization

Series 7 — Web Quality – Engineering Bài 1: Authentication & Authorization – Xác thực và phân quyền trong ứng dụng web 1. Mở đầu Khi xây dựng một ứng dụng web có chức năng đăng nhập, chúng ta thường nghĩ rằng chỉ cần tạo biểu mẫu nhập email, mật khẩu và kiểm tra thông […]

Series 6 – Next.js Advanced – Production: Next.js Build & Deployment

Series 6 – Next.js Advanced & Production Bài 5: Next.js Build & Deployment – Từ mã nguồn đến môi trường Production 1. Mở đầu Trong quá trình phát triển một ứng dụng web, việc chạy thành công trên máy tính cá nhân chưa có nghĩa là ứng dụng đã sẵn sàng để đưa đến người […]

Series 6 – Next.js Advanced – Production: Next.js Internationalized Routing Next.js Static Export

Series 6 – Next.js Advanced & Production   Bài 3: Next.js Internationalized Routing – Tổ chức URL cho website đa ngôn ngữ 1. Mở đầu Sau khi xây dựng cơ chế dịch nội dung và định dạng theo locale, ứng dụng cần một cách để người dùng truy cập đúng phiên bản ngôn ngữ. Ví […]

Series 6 – Next.js Advanced & Production: Metadata & SEO, Internationalization (i18n) & Localization (l10n)

Series 6 – Next.js Advanced & Production Bài 1: Metadata & SEO trong Next.js – Tối ưu thông tin và khả năng hiển thị trên công cụ tìm kiếm 1. Mở đầu Khi xây dựng một website, việc hoàn thiện giao diện và chức năng mới chỉ là một phần của quá trình phát triển. […]

Series 5 – Next.js Fundamentals: CSR, SSR, SSG, ISR và cách lựa chọn

Series 5 – Next.js Fundamentals   Bài 7 : CSR, SSR, SSG và ISR – Các chiến lược Rendering trong Next.js 1. Mở đầu Khi xây dựng một website bằng React, chúng ta thường tập trung vào việc tạo component, quản lý state và hiển thị giao diện trên trình duyệt. Tuy nhiên, khi ứng dụng […]

Series 5 – Rendering – Data Fetching: Server và Client,  Data Fetching, Caching và Revalidation

Series 5 – Rendering – Data Fetching   Bài 5: Server Components, Client Components và Component Boundaries trong Next.js 1. Mở đầu Khi làm việc với React, chúng ta thường xây dựng component bằng JSX, sử dụng useState để quản lý trạng thái và useEffect để xử lý các tác vụ phụ. Phần lớn logic giao […]

Leave a Reply

Your email address will not be published. Required fields are marked *