Web Performance

Vì sao site của bạn nên gửi ít request hơn, không chỉ gửi các request nhỏ hơn

Tệp nhỏ không miễn phí. HTTP hiện đại làm chi phí request nhỏ hơn, chứ không khiến nó trở nên không đáng kể.

The Wux Webtools Team The Wux Webtools Team 17 phút đọc Hỗ trợ AI, được con người xem xét
A stylized browser waterfall showing many small requests simplified into fewer intentional requests.
Mục lục
  1. Huyền thoại dễ chịu về việc “chỉ cần làm mọi tệp nhỏ hơn”
  2. Request không chỉ là byte
  3. “Nhưng HTTP/2 đã sửa chuyện này rồi,” phần lớn là không
  4. Waterfall là nơi sự thật nằm lại
  5. Tệp nhỏ hơn vẫn quan trọng — chỉ là không quan trọng như nhau
  6. Chi phí ẩn của nhiều tệp nhỏ
  7. 1. Phát hiện muộn
  8. 2. Chi phí header
  9. 3. Gián đoạn main-thread
  10. 4. Độ phức tạp của cache
  11. Bundling đã trở lại, nhưng cần phán đoán
  12. Request bên thứ ba đáng bị nghi ngờ hơn
  13. Checklist thực tế để giảm request
  14. Gỡ bỏ
  15. Kết hợp có cân nhắc
  16. Trì hoãn
  17. Cache đúng cách
  18. Đo lại
  19. Trạng thái tốt trông như thế nào

Huyền thoại dễ chịu về việc “chỉ cần làm mọi tệp nhỏ hơn”

Trong nhiều năm, lời khuyên về hiệu năng web nghe rất đơn giản: nén mọi thứ, minify mọi thứ, làm mọi asset nhỏ hơn.

Lời khuyên đó nhìn chung vẫn đúng. Một script 40 KB thường tốt hơn một script 400 KB. Một hình ảnh đã tối ưu tốt hơn một bản export thô. Brotli, AVIF, minification CSS, tree shaking và font subsetting đều quan trọng.

Nhưng trên nhiều site production, vấn đề lớn hơn không còn là một tệp quá khổ. Đó là số lượng thứ mà trình duyệt phải yêu cầu trước khi trang có cảm giác dùng được.

Một trang có thể trông rất kỷ luật về kích thước tệp mà vẫn chậm vì nó gửi 120 request: các mảnh CSS, chunk JavaScript, tag bên thứ ba, tệp font, tracking pixel, icon sprite, endpoint JSON, preload, analytics beacon và các lần tái xác thực cache. Từng thứ có thể “nhỏ.” Nhưng gộp lại, chúng tạo ra một waterfall dài và mong manh.

Quy tắc thực tế là: khi từng asset riêng lẻ đã được nén hợp lý, việc giảm số lượng request thường cải thiện trải nghiệm người dùng nhiều hơn so với việc gọt thêm vài kilobyte khỏi mỗi tệp.

Request không chỉ là byte

Một network request không chỉ là việc truyền dữ liệu. Nó là một chuỗi công việc.

Trình duyệt phải phát hiện tài nguyên, quyết định khi nào fetch, xếp lịch nó so với các tài nguyên khác, gửi header, chờ server, nhận header, parse response, thường là giải nén nó, rồi làm một việc hữu ích nào đó với nó.

“Việc hữu ích” đó có thể tốn kém. Một tệp JavaScript phải được parse, compile và execute. Một tệp CSS có thể chặn rendering. Một tệp font có thể làm chậm việc hiển thị văn bản đọc được hoặc gây layout shift. Một hình ảnh có thể ảnh hưởng đến phần tử Largest Contentful Paint. Một script bên thứ ba có thể kéo theo chuỗi dependency riêng.

Đây là lý do số lượng request vẫn quan trọng ngay cả khi các tệp nhỏ. Một script 3 KB có thể tệ hơn một hình ảnh 30 KB nếu nó chặn rendering, đến muộn và execute trên main thread sai thời điểm.

Nếu bạn đang đọc một báo cáo Lighthouse và cảm thấy bị “phạt” bởi hàng chục cảnh báo riêng lẻ, hãy bắt đầu bằng cách nhìn vào request waterfall thay vì từng điểm số. Chúng tôi có một hướng dẫn thực tế trong bài cách đọc báo cáo Lighthouse mà không hoảng loạn, nhưng phiên bản ngắn gọn là: tìm thứ chặn first render và thứ làm chậm nội dung chính.

“Nhưng HTTP/2 đã sửa chuyện này rồi,” phần lớn là không

HTTP/2 và HTTP/3 đã thay đổi tính kinh tế của request. Chúng giới thiệu multiplexing, nén header và hành vi kết nối tốt hơn. Nói đơn giản, trình duyệt đã giỏi hơn nhiều trong việc gửi nhiều request qua ít kết nối hơn.

Đó là một cải tiến thật sự. Nó cũng loại bỏ một số thói quen cũ, chẳng hạn như CSS sprite cực đoan và các bundle nối khổng lồ được tạo chỉ để tránh giới hạn kết nối.

Nhưng HTTP/2 không làm request trở nên miễn phí.

Multiplexing hữu ích khi nhiều tài nguyên dùng chung một kết nối, nhưng trình duyệt vẫn phải ưu tiên chúng. Server vẫn phải phản hồi. Client vẫn phải xử lý từng response. Tắc nghẽn, mất gói, đàm phán TLS, tra cứu DNS, cache miss và áp lực main-thread vẫn tồn tại.

HTTP/3 cải thiện một số hành vi transport, đặc biệt quanh connection migration và head-of-line blocking ở tầng transport. Nó không loại bỏ chi phí phát hiện, xếp lịch, tải xuống, parse và execute tài nguyên.

Vì vậy mục tiêu hiện đại không phải là “bundle mọi thứ vào một tệp khổng lồ.” Mà là “gửi ít request quan trọng hơn, và khiến các request còn lại có chủ đích.”

Waterfall là nơi sự thật nằm lại

Vấn đề hiệu năng hiếm khi tự bộc lộ trong một metric duy nhất. Chúng hiện ra qua hình dạng.

Mở network panel của trình duyệt và nhìn vào vài giây đầu tiên. Hãy hỏi:

  • Có bao nhiêu request bắt đầu trước khi nội dung chính xuất hiện?
  • Request nào chặn rendering?
  • Các tài nguyên quan trọng có bị phát hiện muộn không?
  • Script bên thứ ba có đang cạnh tranh với CSS, font hoặc hình ảnh first-party không?
  • Có nhiều tệp trả về response 304 thay vì được phục vụ trực tiếp từ cache không?
  • Icon, font hoặc các mảnh UI có bị tách thành nhiều tệp hơn mức trang cần không?

Một trang nhanh thường có waterfall giai đoạn đầu khá nhàm chán. Một số ít tài nguyên quan trọng đến sớm. Tài nguyên không quan trọng chờ. Script bên thứ ba được trì hoãn, giới hạn hoặc gỡ bỏ. Trình duyệt không bị buộc phải xoay xở hai mươi mức ưu tiên trước khi có thể paint trang.

Một trang chậm thường có waterfall bồn chồn: nhiều tệp nhỏ, nhiều origin và nhiều phát hiện muộn.

Tệp nhỏ hơn vẫn quan trọng — chỉ là không quan trọng như nhau

Đây không phải là lập luận chống lại nén hoặc tối ưu hóa. Đây là lập luận chống lại việc tối ưu byte trong khi bỏ qua phối hợp.

Tệp nhỏ hơn quan trọng nhất khi tài nguyên lớn, chặn render hoặc là một phần của đường dẫn nội dung chính. Ví dụ:

  • Hero image nên được định cỡ và mã hóa đúng cách.
  • CSS chặn render nên gọn.
  • JavaScript cần cho tương tác đầu tiên nên tối thiểu.
  • Font nên được subset, nén và giới hạn ở đúng các weight thực sự được dùng.

Font là một ví dụ phổ biến. Các đội thường ám ảnh chuyện một tệp font là 24 KB hay 31 KB, trong khi lại ship sáu weight, hai style và nhiều family. Cách sửa tốt hơn không phải là gọt 7 KB khỏi một tệp. Mà là gửi ít tệp font hơn. Nếu typography là một phần trong công việc hiệu năng của bạn, web font vẫn là một trong những chiến thắng dễ nhất trên hầu hết các site.

Hình ảnh cũng theo cùng mô thức. AVIF hoặc WebP có thể tiết kiệm đáng kể số byte, nhưng gửi mười hình trang trí above the fold vẫn là một kế hoạch tệ. Hãy chọn định dạng tốt hơn, đúng, nhưng cũng hãy đặt câu hỏi liệu từng hình có thật sự cần được request hay không. Với các quyết định về định dạng, hướng dẫn của chúng tôi về khi nào AVIF thắng WebP và khi nào thì không là tài liệu đồng hành hữu ích cho công việc giảm số lượng request này.

Chi phí ẩn của nhiều tệp nhỏ

Nhiều request nhỏ thường tạo ra các vấn đề không hiện ra nếu bạn chỉ nhìn vào tổng số byte được truyền.

1. Phát hiện muộn

Trình duyệt không thể request thứ mà nó chưa phát hiện. Một tệp CSS có thể tham chiếu một font. Một script có thể import một script khác. Một component có thể request JSON sau hydration. Mỗi dependency tạo ra thêm một bước trong chuỗi.

Chuỗi càng sâu, công việc quan trọng càng bắt đầu muộn.

2. Chi phí header

Mỗi request và response đều bao gồm header. Nén header có ích, đặc biệt qua HTTP/2 và HTTP/3, nhưng không loại bỏ hoàn toàn chi phí. Cookie có thể làm điều này tệ hơn nhiều. Nếu site của bạn gửi cookie lớn cùng mọi request, các asset nhỏ trong thực tế sẽ không còn nhỏ như vậy.

Đây là một lý do các static asset thường nên nằm trên path hoặc domain không có cookie, và vì sao cache header đáng được chú ý. Nếu header đang hành xử lạ trong production, debug redirect và HTTP header thường nhanh hơn đoán mò.

3. Gián đoạn main-thread

Nhiều chunk JavaScript có thể tạo ra công việc parse và execute lặp lại. Ngay cả khi mỗi chunk nhỏ, trình duyệt vẫn có thể liên tục dừng lại để đánh giá code. Điều này có thể làm hại Interaction to Next Paint và khiến trang có cảm giác giật cục.

Người dùng không quan tâm rằng từng tệp nhỏ. Họ quan tâm rằng việc chạm vào menu mất 600 mili giây.

4. Độ phức tạp của cache

Tách asset có thể cải thiện caching khi được làm cẩn thận. Một vendor bundle ổn định và một app bundle thay đổi có thể là một cách tách tốt.

Nhưng chunking quá mức có thể phản tác dụng. Nhiều tệp hơn nghĩa là nhiều cache lookup hơn, nhiều cơ hội revalidation hơn, nhiều phối hợp phiên bản hơn và nhiều cách hơn để vô tình invalidate các tài nguyên vốn không cần thay đổi.

Bundling đã trở lại, nhưng cần phán đoán

Kỷ nguyên đầu của hiệu năng web yêu thích bundling vì trình duyệt có giới hạn kết nối nghiêm ngặt. Rồi HTTP/2 xuất hiện và nhiều đội xoay mạnh sang code splitting quyết liệt. Một phần trong đó hữu ích. Một phần trở thành mê tín.

Điểm giữa hợp lý là bundling theo route.

Với một marketing site hoặc content site điển hình:

  • Inline hoặc chỉ load CSS cần cho rendering ban đầu.
  • Giữ JavaScript global nhỏ.
  • Tránh tách các module rất nhỏ thành các network request riêng.
  • Trì hoãn các tính năng tương tác không cần ngay lập tức.
  • Gỡ bỏ script bên thứ ba không biện minh được chi phí của chúng.

Với một application:

  • Tách theo route hoặc tính năng lớn, không tách theo từng component.
  • Giữ shared dependency ổn định và cacheable.
  • Chỉ preload tài nguyên chắc chắn sẽ sớm cần.
  • Tránh load code admin, dashboard, editor hoặc experiment trên các trang public.
  • Đo chi phí tương tác, không chỉ bundle size.

Bundling không tự động là tốt. Code splitting không tự động là tốt. Câu hỏi hữu ích là: cách tách này có giúp trình duyệt đưa trải nghiệm người dùng có ý nghĩa tiếp theo đến sớm hơn không?

Request bên thứ ba đáng bị nghi ngờ hơn

Request first-party ít nhất còn nằm trong quyền kiểm soát của bạn. Request bên thứ ba thường chậm hơn, khó dự đoán hơn và đắt hơn vẻ ngoài của chúng.

Một tag manager duy nhất có thể kích hoạt analytics, quảng cáo, heatmap, chat widget, A/B testing, công cụ consent và script cá nhân hóa. Mỗi vendor có thể kéo thêm request. Một số sẽ chạy sớm. Một số sẽ chặn main thread. Một số sẽ thay đổi mà không đi qua quy trình release của bạn.

Tối ưu bên thứ ba tốt nhất là xóa. Tốt thứ hai là trì hoãn.

Trước khi thêm một script bên thứ ba, hãy hỏi:

  • Thứ này có cần load trước khi người dùng thấy trang không?
  • Nó có cần load trên mọi trang không?
  • Nó có thể load sau consent, tương tác hoặc idle time không?
  • Ai sở hữu nó trong nội bộ?
  • Metric nào chứng minh nó xứng đáng với chi phí hiệu năng?

Đây là nơi hiệu năng trở thành quản trị. Phải có người được phép nói không.

Checklist thực tế để giảm request

Bắt đầu với các trang quan trọng nhất: homepage, trang pricing, trang product, checkout, signup hoặc các landing page hàng đầu. Sau đó đi qua waterfall.

Gỡ bỏ

  • Xóa JavaScript và CSS không dùng.
  • Gỡ bỏ experiment cũ, pixel bị bỏ quên và analytics trùng lặp.
  • Bỏ các font weight và icon library không dùng.
  • Thay hình trang trí bằng CSS khi phù hợp.

Kết hợp có cân nhắc

  • Bundle các module JavaScript nhỏ luôn load cùng nhau.
  • Gộp các tệp CSS nhỏ chặn cùng một đường render.
  • Dùng SVG sprite hoặc inline SVG cho icon lặp lại khi điều đó giảm request mà không làm hại khả năng bảo trì.

Trì hoãn

  • Lazy-load hình ảnh below-the-fold.
  • Trì hoãn script không quan trọng cho đến sau first paint hoặc tương tác của người dùng.
  • Chỉ load comment, embed, map, chat và video player khi cần.

Cache đúng cách

  • Dùng caching dài hạn cho static asset có version.
  • Tránh revalidation không cần thiết cho các tệp hiếm khi thay đổi.
  • Giữ HTML luôn mới, nhưng để hashed asset ở lại trong cache.

Đo lại

Sau mỗi thay đổi, kiểm tra lại waterfall. Mục tiêu không phải là một điểm số hoàn hảo. Mục tiêu là ít request quan trọng hơn, rendering hữu ích sớm hơn và ít gián đoạn main-thread hơn.

Trạng thái tốt trông như thế nào

Một trang khỏe mạnh không nhất thiết có số request ít nhất có thể. Nó có một critical path nhỏ và có chủ đích.

Trình duyệt nhận HTML, CSS thiết yếu, hình ảnh nội dung chính nếu có, có thể là một script nhỏ cần cho điều hướng hoặc tương tác above-the-fold, và bộ font tối thiểu cần để văn bản đọc được. Mọi thứ khác chờ đến lượt.

Đó là khác biệt giữa một trang chỉ được tối ưu và một trang có cảm giác nhanh.

Thu nhỏ tệp vẫn đáng làm. Nhưng nếu site đã được nén hợp lý, chiến thắng hiệu năng tiếp theo thường không phải là tiết kiệm thêm 2 KB khỏi một bundle. Đó là bớt một request chặn, bớt một tệp font, bớt một script bên thứ ba, bớt một chuỗi dependency.

Ít request hơn khiến công việc của trình duyệt đơn giản hơn. Sự đơn giản thường nhanh hơn điều chúng ta muốn thừa nhận.

Câu hỏi thường gặp

Một bundle lớn có tốt hơn nhiều tệp nhỏ không?
Không tự động. Một bundle khổng lồ có thể làm chậm mọi thứ, đặc biệt ở lần tải đầu. Nhiều tệp rất nhỏ có thể tạo ra chi phí xếp lịch và execution. Mô thức tốt hơn là bundle các tài nguyên luôn cần cùng nhau và tách theo route hoặc tính năng lớn.
HTTP/2 có nghĩa là số lượng request không còn quan trọng không?
Không. HTTP/2 giảm một phần chi phí kết nối thông qua multiplexing và nén header, nhưng mỗi request vẫn có chi phí phát hiện, ưu tiên, server, cache, parsing và execution.
Tôi có nên inline toàn bộ critical CSS không?
Inline một lượng nhỏ CSS thật sự quan trọng có thể giúp first render, nhưng inline quá nhiều làm HTML nặng hơn và khó cache hơn. Giữ nó tối thiểu và đo tác động.
Đâu là nơi dễ nhất để giảm request?
Font và script bên thứ ba thường là các chiến thắng nhanh nhất. Nhiều site ship font weight không dùng, analytics trùng lặp, pixel cũ, chat widget hoặc embed không cần load ngay lập tức.
Một trang nên có bao nhiêu request?
Không có mục tiêu phổ quát. Một trang nội dung nhỏ nên có rất ít request quan trọng. Một app phức tạp có thể cần nhiều hơn. Tập trung giảm request trước first render và trước đường tương tác chính.

Nguồn & tài liệu tham khảo thêm

  1. MDN Web Docs: HTTP caching
  2. web.dev: Optimize Largest Contentful Paint
  3. RFC 9113: HTTP/2
  4. HTTP Archive Web Almanac: Page Weight
Về tác giả
The Wux Webtools Team

Cập nhật lần cuối:

Tiếp tục đọc