Các nguyên nhân phổ biến gây chậm render

Trong phần trước, chúng ta đã tìm hiểu về quá trình render trong trình duyệt. Bây giờ, chúng ta sẽ đi sâu vào các nguyên nhân phổ biến gây chậm render và ảnh hưởng của chúng đến hiệu suất trang web.


  flowchart LR
    A[Các nguyên nhân chậm render] --> B[Tài nguyên chặn render]
    A --> C[Tài nguyên không tối ưu]
    A --> D[JS không hiệu quả]
    A --> E[Layout Thrashing]
    A --> F[Thiếu Resource Hints]
    A --> G[Third-party Scripts]
    A --> H[Không sử dụng CDN]

    B --> B1[CSS chặn render]
    B --> B2[JS chặn render]
    B --> B3[Fonts chặn render]

    C --> C1[Hình ảnh không tối ưu]
    C --> C2[JS/CSS không tối ưu]

    D --> D1[Long tasks]
    D --> D2[Hydration không hiệu quả]

    style A fill:#f96,stroke:#333,stroke-width:2px
    style B fill:#bbf,stroke:#333,stroke-width:1px
    style C fill:#bbf,stroke:#333,stroke-width:1px
    style D fill:#bbf,stroke:#333,stroke-width:1px
    style E fill:#bbf,stroke:#333,stroke-width:1px
    style F fill:#bbf,stroke:#333,stroke-width:1px
    style G fill:#bbf,stroke:#333,stroke-width:1px
    style H fill:#bbf,stroke:#333,stroke-width:1px

1. Tài nguyên chặn render (Render-blocking resources)

Tài nguyên chặn render là một trong những nguyên nhân hàng đầu gây chậm render. Đây là các tài nguyên mà trình duyệt phải tải và xử lý trước khi có thể tiếp tục render trang.

CSS chặn render

Theo mặc định, CSS được coi là tài nguyên chặn render vì trình duyệt sẽ tạm dừng việc render trang cho đến khi tất cả các file CSS được tải và phân tích xong. Điều này là cần thiết vì nếu không có CSS, trang web có thể hiển thị không đúng cách, gây ra hiện tượng nhấp nháy nội dung không có kiểu dáng (Flash of Unstyled Content - FOUC).

Vấn đề:

  • CSS không cần thiết ngay lập tức (ví dụ: CSS cho phần footer hoặc các phần không nằm trong viewport ban đầu) vẫn chặn render
  • Các file CSS lớn làm tăng thời gian tải và phân tích
  • Nhiều file CSS riêng biệt tạo ra nhiều HTTP request

Giải pháp:

  1. Phân chia CSS:

    • Tách CSS thành CSS quan trọng (critical) và không quan trọng
    • Inline CSS quan trọng trực tiếp vào HTML
    • Tải CSS không quan trọng một cách không đồng bộ
  2. Tối ưu hóa CSS:

    • Loại bỏ CSS không sử dụng
    • Nén và minify CSS
    • Sử dụng CSS có chọn lọc và hiệu quả
  3. Sử dụng media queries:

    <link
      rel="stylesheet"
      href="desktop.css"
      media="screen and (min-width: 900px)"
    />
    <link
      rel="stylesheet"
      href="mobile.css"
      media="screen and (max-width: 900px)"
    />
    

JavaScript chặn render

JavaScript mặc định cũng là tài nguyên chặn parser. Khi trình duyệt gặp thẻ <script>, nó sẽ dừng việc phân tích HTML cho đến khi JavaScript được tải và thực thi xong.

Vấn đề:

  • JavaScript lớn làm tăng thời gian tải và phân tích
  • JavaScript đồng bộ chặn cả quá trình phân tích HTML
  • JavaScript có thể thay đổi DOM và CSSOM, dẫn đến việc phải tính toán lại Render Tree

Giải pháp:

  1. Sử dụng async và defer:

    <!-- Tải không đồng bộ và thực thi ngay khi tải xong -->
    <script async src="analytics.js"></script>
    
    <!-- Tải không đồng bộ nhưng chỉ thực thi sau khi HTML được phân tích xong -->
    <script defer src="non-critical.js"></script>
    
  2. Tối ưu hóa JavaScript:

    • Loại bỏ JavaScript không sử dụng
    • Nén và minify JavaScript
    • Sử dụng code-splitting để chia nhỏ bundle JavaScript
  3. Lazy-loading JavaScript:

    • Chỉ tải JavaScript khi cần thiết (ví dụ: khi người dùng tương tác với một phần cụ thể của trang)

Fonts chặn render

Fonts web có thể gây ra vấn đề hiệu suất đáng kể, đặc biệt là khi không được xử lý đúng cách.

Vấn đề:

  • Font mặc định có thể gây ra Flash of Invisible Text (FOIT) hoặc Flash of Unstyled Text (FOUT)
  • Tải font từ bên thứ ba (như Google Fonts) tạo ra thêm HTTP request và DNS lookup
  • Font nặng làm tăng thời gian tải

Giải pháp:

  1. Sử dụng font-display:

    @font-face {
      font-family: "MyFont";
      src: url("myfont.woff2") format("woff2");
      font-display: swap; /* Hiển thị font dự phòng cho đến khi font tùy chỉnh được tải */
    }
    
  2. Preload fonts quan trọng:

    <link
      rel="preload"
      href="myfont.woff2"
      as="font"
      type="font/woff2"
      crossorigin
    />
    
  3. Sử dụng font subset:

    • Chỉ bao gồm các ký tự cần thiết
    • Sử dụng định dạng font hiệu quả (WOFF2)
  4. Cân nhắc sử dụng system fonts:

    body {
      font-family:
        -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial,
        sans-serif;
    }
    

2. Tài nguyên không được tối ưu

Hình ảnh không được tối ưu

Hình ảnh thường chiếm phần lớn kích thước trang web và có thể ảnh hưởng đáng kể đến hiệu suất nếu không được tối ưu hóa.

Vấn đề:

  • Kích thước file lớn
  • Định dạng không hiệu quả
  • Độ phân giải cao hơn mức cần thiết
  • Không có kích thước xác định (width/height)

Giải pháp:

  1. Nén hình ảnh:

    • Sử dụng công cụ như ImageOptim, TinyPNG, Squoosh
    • Cân bằng giữa chất lượng và kích thước
  2. Sử dụng định dạng hiện đại:

    • WebP cho hầu hết các trường hợp (tiết kiệm 25-35% so với JPEG/PNG)
    • AVIF cho hiệu suất nén tốt hơn nữa
    • Cung cấp fallback cho các trình duyệt cũ
    <picture>
      <source srcset="image.avif" type="image/avif" />
      <source srcset="image.webp" type="image/webp" />
      <img src="image.jpg" alt="Description" width="800" height="600" />
    </picture>
    
  3. Responsive images:

    • Sử dụng srcset và sizes để cung cấp hình ảnh phù hợp với kích thước màn hình
    <img
      srcset="small.jpg 500w, medium.jpg 1000w, large.jpg 1500w"
      sizes="(max-width: 600px) 500px, (max-width: 1200px) 1000px, 1500px"
      src="fallback.jpg"
      alt="Description"
      width="800"
      height="600"
    />
    
  4. Lazy-loading hình ảnh:

    • Chỉ tải hình ảnh khi chúng gần đến viewport
    <img
      src="image.jpg"
      loading="lazy"
      alt="Description"
      width="800"
      height="600"
    />
    
  5. Luôn chỉ định width và height:

    • Giúp trình duyệt dự trữ không gian, giảm layout shift

JavaScript và CSS không được tối ưu

Mã JavaScript và CSS không được tối ưu có thể làm tăng đáng kể thời gian tải và xử lý.

Vấn đề:

  • Code thừa không sử dụng
  • Không minify
  • Không nén
  • Không sử dụng caching hiệu quả

Giải pháp:

  1. Loại bỏ code không sử dụng:

    • Sử dụng công cụ như PurgeCSS để loại bỏ CSS không sử dụng
    • Sử dụng tree-shaking để loại bỏ JavaScript không sử dụng
  2. Minify và nén:

    • Minify để giảm kích thước file
    • Bật GZIP hoặc Brotli compression trên server
  3. Code-splitting:

    • Chia nhỏ bundle JavaScript thành các chunk nhỏ hơn
    • Chỉ tải code cần thiết cho mỗi trang
  4. Caching hiệu quả:

    • Sử dụng cache-control headers
    • Sử dụng versioning hoặc content hashing để vô hiệu hóa cache khi cần

3. Thực thi JavaScript không hiệu quả

JavaScript không hiệu quả có thể chiếm main thread trong thời gian dài, gây ra độ trễ và làm giảm khả năng phản hồi của trang web.

Long tasks

Long tasks là các tác vụ JavaScript chạy trên main thread và chiếm hơn 50ms, làm chặn các tương tác của người dùng.

Vấn đề:

  • Xử lý dữ liệu lớn trên main thread
  • Vòng lặp phức tạp
  • DOM manipulation quá nhiều

Giải pháp:

  1. Chia nhỏ các tác vụ:

    // Thay vì xử lý 1000 mục cùng một lúc
    function processItems(items) {
      // Chia nhỏ thành các batch
      const BATCH_SIZE = 50;
      let index = 0;
    
      function processNextBatch() {
        const batch = items.slice(index, index + BATCH_SIZE);
        index += BATCH_SIZE;
    
        // Xử lý batch
        batch.forEach((item) => {
          // Xử lý item
        });
    
        if (index < items.length) {
          // Lên lịch batch tiếp theo
          setTimeout(processNextBatch, 0);
        }
      }
    
      processNextBatch();
    }
    
  2. Sử dụng Web Workers:

    // main.js
    const worker = new Worker("worker.js");
    
    worker.postMessage({ data: largeDataSet });
    
    worker.onmessage = function (e) {
      const result = e.data;
      // Sử dụng kết quả
    };
    
    // worker.js
    self.onmessage = function (e) {
      const data = e.data.data;
      const result = processData(data); // Xử lý nặng
      self.postMessage(result);
    };
    
  3. Sử dụng requestIdleCallback và requestAnimationFrame:

    // Cho các tác vụ không khẩn cấp
    requestIdleCallback(() => {
      // Thực hiện tác vụ không khẩn cấp
    });
    
    // Cho các animation và visual updates
    requestAnimationFrame(() => {
      // Cập nhật UI
    });
    

Hydration không hiệu quả

Trong các framework hiện đại như React, Vue, và Angular, quá trình hydration (kết nối JavaScript với HTML đã render) có thể gây ra độ trễ đáng kể.

Vấn đề:

  • Hydration đồng bộ cho toàn bộ trang
  • Hydration không cần thiết cho các phần không tương tác
  • Hydration sớm cho các phần không nằm trong viewport

Giải pháp:

  1. Partial hydration:

    • Chỉ hydrate các phần tương tác của trang
    • Để các phần tĩnh là HTML thuần
  2. Progressive hydration:

    • Hydrate các phần quan trọng trước
    • Hydrate các phần còn lại sau khi trang đã tương tác được
  3. Lazy hydration:

    • Chỉ hydrate khi phần tử gần đến viewport hoặc khi người dùng tương tác

4. Layout Thrashing (Forced Reflow)

Layout thrashing xảy ra khi JavaScript liên tục đọc và ghi vào DOM, buộc trình duyệt phải tính toán lại layout nhiều lần.

Vấn đề:

  • Đọc các thuộc tính liên quan đến layout (như offsetWidth, offsetHeight) sau khi thay đổi DOM
  • Thay đổi style của nhiều phần tử riêng lẻ
  • Animation không hiệu quả

Ví dụ về layout thrashing:

// Không tốt - gây layout thrashing
const boxes = document.querySelectorAll(".box");
boxes.forEach((box) => {
  const width = box.offsetWidth; // Buộc reflow
  box.style.width = width * 2 + "px"; // Thay đổi DOM
  const height = box.offsetHeight; // Buộc reflow lại
  box.style.height = height * 2 + "px"; // Thay đổi DOM
});

Giải pháp:

  1. Batch DOM reads và writes:

    // Tốt - tránh layout thrashing
    const boxes = document.querySelectorAll(".box");
    
    // Đọc trước
    const dimensions = boxes.map((box) => ({
      width: box.offsetWidth,
      height: box.offsetHeight,
    }));
    
    // Sau đó ghi
    boxes.forEach((box, i) => {
      const dim = dimensions[i];
      box.style.width = dim.width * 2 + "px";
      box.style.height = dim.height * 2 + "px";
    });
    
  2. Sử dụng requestAnimationFrame:

    requestAnimationFrame(() => {
      // Đọc
      const width = element.offsetWidth;
    
      requestAnimationFrame(() => {
        // Ghi
        element.style.width = width * 2 + "px";
      });
    });
    
  3. Sử dụng CSS transforms thay vì thuộc tính gây reflow:

    /* Thay vì thay đổi width/height/top/left */
    .box {
      transform: scale(2) translateX(10px);
    }
    

5. Không sử dụng resource hints

Resource hints cho phép chúng ta thông báo cho trình duyệt về các tài nguyên mà trang sẽ cần, giúp tối ưu hóa quá trình tải.

Vấn đề:

  • Trình duyệt không biết trước tài nguyên nào sẽ cần
  • Thời gian thiết lập kết nối (DNS lookup, TCP handshake, TLS negotiation) chiếm thời gian đáng kể
  • Tài nguyên quan trọng được phát hiện muộn trong quá trình tải trang

Giải pháp:

  1. dns-prefetch:

    • Giúp giải quyết DNS sớm
    • Phù hợp cho các domain bên thứ ba sẽ được sử dụng
    <link rel="dns-prefetch" href="//fonts.googleapis.com" />
    
  2. preconnect:

    • Thiết lập kết nối sớm (DNS, TCP, TLS)
    • Phù hợp cho các tài nguyên quan trọng từ domain khác
    <link rel="preconnect" href="https://fonts.googleapis.com" crossorigin />
    
  3. prefetch:

    • Tải tài nguyên với độ ưu tiên thấp cho việc sử dụng trong tương lai
    • Phù hợp cho tài nguyên sẽ cần cho trang tiếp theo
    <link rel="prefetch" href="next-page.js" />
    
  4. preload:

    • Tải tài nguyên với độ ưu tiên cao
    • Phù hợp cho tài nguyên quan trọng cần sớm nhưng có thể bị phát hiện muộn
    <link
      rel="preload"
      href="critical-font.woff2"
      as="font"
      type="font/woff2"
      crossorigin
    />
    <link rel="preload" href="hero-image.jpg" as="image" />
    
  5. prerender:

    • Tải và render trang ngầm
    • Phù hợp khi bạn chắc chắn người dùng sẽ truy cập trang đó
    <link rel="prerender" href="likely-next-page.html" />
    

6. Không tối ưu hóa third-party scripts

Các script từ bên thứ ba (analytics, quảng cáo, chat widgets, v.v.) thường là nguyên nhân chính gây chậm trang web.

Vấn đề:

  • Tải đồng bộ và chặn render
  • Kích thước lớn
  • Không kiểm soát được nội dung
  • Có thể gây ra long tasks

Giải pháp:

  1. Tải không đồng bộ hoặc defer:

    <script async src="https://analytics.example.com/script.js"></script>
    
  2. Sử dụng Intersection Observer để tải lazy:

    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          // Tải script khi phần tử xuất hiện trong viewport
          const script = document.createElement("script");
          script.src = "https://widget.example.com/script.js";
          document.body.appendChild(script);
          observer.disconnect();
        }
      });
    });
    
    observer.observe(document.querySelector("#widget-container"));
    
  3. Sử dụng Service Worker để kiểm soát tài nguyên bên thứ ba:

    • Cache tài nguyên bên thứ ba
    • Kiểm soát thời gian tải
  4. Sử dụng thuộc tính importance:

    <script
      src="https://analytics.example.com/script.js"
      importance="low"
    ></script>
    

7. Không sử dụng Content Delivery Network (CDN)

Không sử dụng CDN có thể làm tăng độ trễ, đặc biệt là đối với người dùng ở xa server của bạn.

Vấn đề:

  • Độ trễ cao do khoảng cách địa lý
  • Tải server quá mức
  • Không tận dụng được caching toàn cầu

Giải pháp:

  1. Sử dụng CDN cho tài nguyên tĩnh:

    • JavaScript, CSS, hình ảnh, fonts
    • Tận dụng caching toàn cầu
  2. Sử dụng CDN cho API:

    • Giảm độ trễ cho các API call
    • Cải thiện trải nghiệm người dùng toàn cầu
  3. Sử dụng CDN có tính năng tối ưu hóa:

    • Tự động nén và minify
    • Tự động chuyển đổi hình ảnh sang định dạng hiện đại
    • HTTP/2 hoặc HTTP/3 support

Kết luận

Hiểu rõ các nguyên nhân gây chậm render là bước đầu tiên để tối ưu hóa hiệu suất frontend. Bằng cách xác định và giải quyết các vấn đề này, chúng ta có thể cải thiện đáng kể trải nghiệm người dùng và các chỉ số hiệu suất quan trọng.


  flowchart TD
    A[Tối ưu hóa hiệu suất] --> B[Xác định nguyên nhân]
    B --> C[Đo lường hiệu suất]
    C --> D[Áp dụng giải pháp]
    D --> E[Đo lường lại]
    E -->|Chưa đạt mục tiêu| B
    E -->|Đạt mục tiêu| F[Duy trì và giám sát]

    style A fill:#f96,stroke:#333,stroke-width:2px
    style F fill:#6f6,stroke:#333,stroke-width:2px