Bài 23.5: Bài tập thực hành tối ưu performance

Bài 23.5: Bài tập thực hành tối ưu performance
Chào mừng trở lại! Sau khi đã xây dựng được những giao diện đẹp mắt và có tính tương tác cao, đã đến lúc chúng ta nâng cấp "tốc độ" cho website của mình. Performance không chỉ là một yếu tố kỹ thuật, mà còn là yếu tố then chốt quyết định trải nghiệm của người dùng và thậm chí là thứ hạng trên các công cụ tìm kiếm. Một trang web chậm có thể khiến người dùng rời đi ngay lập tức, bất kể nội dung hay giao diện có tuyệt vời đến đâu.
Trong bài thực hành này, chúng ta sẽ đi sâu vào các chiến lược và kỹ thuật thực tế để tối ưu hiệu suất Front-end, biến website của bạn từ "rùa bò" thành "tia chớp"!
Tại Sao Tối Ưu Performance Lại Quan Trọng Đến Vậy?
Hãy nghĩ về lần gần nhất bạn truy cập một trang web mà mất quá nhiều thời gian để tải. Cảm giác của bạn thế nào? Chắc chắn là khó chịu, đúng không?
- Trải nghiệm người dùng (UX): Người dùng hiện đại không có kiên nhẫn. Họ mong đợi các trang web tải nhanh, phản hồi ngay lập tức. Một trang web nhanh mang lại trải nghiệm mượt mà, chuyên nghiệp và giữ chân người dùng lâu hơn.
- Tỷ lệ chuyển đổi (Conversion Rate): Đặc biệt quan trọng với các trang thương mại điện tử, blog (thời gian đọc), hay trang giới thiệu sản phẩm. Mỗi giây chậm trễ có thể làm giảm đáng kể tỷ lệ người dùng thực hiện hành động mong muốn (mua hàng, đăng ký, đọc hết bài...).
- SEO (Search Engine Optimization): Google và các công cụ tìm kiếm khác coi trọng tốc độ tải trang. Một trang web nhanh có xu hướng được xếp hạng cao hơn trong kết quả tìm kiếm, giúp bạn tiếp cận được nhiều người dùng hơn.
- Tiết kiệm tài nguyên: Tối ưu performance cũng giúp giảm tải cho server, giảm băng thông tiêu thụ, từ đó giảm chi phí vận hành.
- Khả năng tiếp cận (Accessibility): Trang web nhanh hơn dễ tiếp cận hơn trên các thiết bị cũ hoặc kết nối mạng yếu.
Như bạn thấy, tối ưu performance không phải là tùy chọn, mà là một yêu cầu bắt buộc trong lập trình web hiện đại.
Các Khu Vực Trọng Tâm Cần Tối Ưu
Có nhiều khía cạnh ảnh hưởng đến hiệu suất của một trang web Front-end. Chúng ta sẽ tập trung vào các khu vực chính sau:
- Tối Ưu Tài Nguyên (Assets Optimization): Hình ảnh, CSS, JavaScript, Font chữ.
- Giảm Thiểu Yêu Cầu (Reducing Requests): Số lượng kết nối đến server.
- Tận Dụng Bộ Nhớ Đệm (Leveraging Caching): Cache trình duyệt, Service Workers.
- Tối Ưu Quá Trình Render (Rendering Optimization): DOM, CSSOM, Browser Painting.
- Tối Ưu Mã JavaScript (Efficient JavaScript): Cách viết, tải và thực thi JS.
Hãy cùng đi sâu vào từng phần!
1. Tối Ưu Tài Nguyên (Assets Optimization)
Tài nguyên là những "viên gạch" xây dựng nên trang web của bạn. Nếu các viên gạch quá nặng hoặc không được sắp xếp hợp lý, ngôi nhà của bạn sẽ chậm chạp và cồng kềnh.
a. Hình ảnh (Images): Hung Thủ Số Một!
Hình ảnh thường chiếm dung lượng lớn nhất trong tổng dung lượng tải về của một trang web. Tối ưu hình ảnh mang lại hiệu quả đáng kinh ngạc.
- Nén hình ảnh (Compression): Luôn nén hình ảnh trước khi đưa lên server. Có rất nhiều công cụ online và offline miễn phí, hoặc các plugin cho các CMS/framework (ví dụ:
imagemin
cho Webpack). Hãy cố gắng đạt được sự cân bằng giữa chất lượng và dung lượng. - Chọn định dạng phù hợp:
- JPEG: Tốt cho ảnh chụp (có nhiều màu sắc).
- PNG: Tốt cho ảnh có nền trong suốt hoặc đồ họa đơn giản (ít màu sắc).
- SVG: Tốt cho biểu tượng (icons), logo, đồ họa vector. Dung lượng rất nhỏ và có thể scale mà không vỡ hình.
- WebP & AVIF: Đây là những định dạng thế hệ mới mang lại tỷ lệ nén vượt trội so với JPEG và PNG mà vẫn giữ được chất lượng tương đương. Hãy cố gắng sử dụng chúng nếu có thể, có thể dùng thẻ
<picture>
để cung cấp các định dạng khác cho trình duyệt cũ không hỗ trợ.
- Thay đổi kích thước (Resizing): Đừng bao giờ hiển thị một ảnh có kích thước lớn hơn kích thước thực tế nó được hiển thị trên trang. Nếu ảnh hiển thị chỉ 300x200px, hãy resize ảnh gốc về gần kích thước đó thay vì tải về ảnh 3000x2000px rồi để trình duyệt co lại.
Lazy Loading (Tải lười): Kỹ thuật này trì hoãn việc tải các hình ảnh "ở dưới màn hình" (below-the-fold) cho đến khi người dùng cuộn trang đến gần chúng. Điều này giúp trang tải ban đầu nhanh hơn đáng kể.
- Với HTML hiện đại, bạn có thể dùng thuộc tính
loading="lazy"
:
<img src="duong-dan-anh.jpg" alt="Mo ta anh" loading="lazy">
- Giải thích: Thuộc tính
loading="lazy"
là một cách cực kỳ đơn giản và hiệu quả để trình duyệt tự động xử lý việc tải lười cho hình ảnh và iframe mà không cần viết JavaScript.
- Với HTML hiện đại, bạn có thể dùng thuộc tính
Responsive Images: Sử dụng thuộc tính
srcset
trên thẻ<img>
hoặc thẻ<picture>
để trình duyệt có thể chọn ảnh phù hợp nhất với kích thước màn hình và độ phân giải của thiết bị.<img srcset="anh-nho.jpg 500w, anh-vua.jpg 800w, anh-lon.jpg 1200w" sizes="(max-width: 600px) 500px, (max-width: 900px) 800px, 1200px" src="anh-lon.jpg" alt="Mo ta anh responsive">
- Giải thích:
srcset
định nghĩa danh sách các ảnh và chiều rộng (ví dụ:500w
nghĩa là ảnh này rộng 500px).sizes
cho trình duyệt biết ảnh sẽ hiển thị rộng bao nhiêu trong các điều kiện khác nhau ((max-width: 600px) 500px
nghĩa là nếu màn hình nhỏ hơn 600px, ảnh sẽ hiển thị rộng 500px). Trình duyệt sẽ dùng thông tin này để chọn ảnh tối ưu nhất từsrcset
. Thẻ<picture>
cung cấp sự kiểm soát chi tiết hơn cho các trường hợp phức tạp (ví dụ: hiển thị định dạng ảnh khác nhau).
- Giải thích:
b. CSS và JavaScript: Nén, Gộp và Dọn dẹp
Minification (Thu gọn): Xóa bỏ các ký tự không cần thiết như khoảng trắng, tab, xuống dòng, comment khỏi file CSS và JS. Điều này làm giảm đáng kể dung lượng file mà không ảnh hưởng đến chức năng.
- Ví dụ (CSS): ```css / CSS gốc / .button { padding: 10px 15px; / Khoảng cách bên trong / margin: 5px; / Khoảng cách bên ngoài / border: 1px solid blue; border-radius: 4px; font-size: 16px; }
/ CSS sau khi Minify / .button{padding:10px 15px;margin:5px;border:1px solid blue;border-radius:4px;font-size:16px;} ```
- Giải thích: Minification đơn giản là loại bỏ những thứ chỉ dành cho con người đọc (whitespace, comments) để giảm kích thước file. Các build tool hiện đại (Webpack, Rollup, Parcel) hoặc task runner (Gulp, Grunt) đều có các plugin làm việc này tự động.
Bundling (Gộp file): Gộp nhiều file CSS nhỏ thành một hoặc vài file lớn hơn, và nhiều file JS nhỏ thành một hoặc vài file lớn hơn.
- Tại sao lại gộp? Mỗi file CSS hoặc JS riêng lẻ cần một HTTP request để tải về. Gộp file giúp giảm số lượng request mà trình duyệt cần thực hiện, tiết kiệm thời gian thiết lập kết nối (overhead).
- Ví dụ (JS - Conceptual): ```javascript // src/utils.js export function formatName(name) { return name.trim().toUpperCase(); }
// src/main.js import { formatName } from './utils.js'; const userName = " alice "; console.log(formatName(userName)); // Output: "ALICE"
// Sau khi Bundle và Minify (Conceptual) // (function() { // function formatName(name) { return name.trim().toUpperCase(); } // const userName = " alice "; // console.log(formatName(userName)); // })(); ```
- Giải thích: Các module JS (
export
,import
) rất tiện lợi cho việc tổ chức code, nhưng nếu mỗi module là một file riêng thì trình duyệt phải tải rất nhiều file. Bundling gom chúng lại thành một file duy nhất, giảm đáng kể số lượng request. Các bundler thông minh như Webpack còn có thể thực hiện "tree shaking" để loại bỏ code JS không dùng tới.
Nén Gzip/Brotli (Compression): Cấu hình server để nén các file văn bản (HTML, CSS, JS, JSON, SVG) trước khi gửi về trình duyệt. Hầu hết các trình duyệt hiện đại đều hỗ trợ giải nén Gzip hoặc Brotli (Brotli thường hiệu quả hơn).
- Đây là kỹ thuật server-side, nhưng kết quả là file được truyền qua mạng sẽ nhỏ hơn rất nhiều.
Loại bỏ code không dùng tới (Dead Code Elimination / Tree Shaking): Đặc biệt quan trọng với JavaScript trong các ứng dụng lớn sử dụng framework/thư viện. Tree shaking là quá trình phân tích code và loại bỏ những hàm, biến, hoặc module không bao giờ được gọi/sử dụng.
c. Font chữ (Web Fonts):
Font chữ đẹp có thể làm tăng tính thẩm mỹ, nhưng tải font từ bên ngoài cũng tốn thời gian.
- Sử dụng định dạng WOFF2: Đây là định dạng hiện đại với tỷ lệ nén tốt nhất. Cung cấp fallback bằng WOFF cho trình duyệt cũ hơn.
- Subset Font: Nếu bạn chỉ dùng một vài ký tự đặc biệt hoặc chỉ cần bảng chữ cái Latin cơ bản, hãy subset font để chỉ bao gồm các ký tự cần thiết, giảm dung lượng file font.
font-display
CSS property: Kiểm soát cách font được hiển thị trong quá trình tải.font-display: swap;
: Hiển thị text bằng font fallback ngay lập tức và chuyển sang font web khi nó tải xong. Tốt cho trải nghiệm người dùng vì text luôn hiển thị, tránh hiệu ứng FOIT (Flash of Invisible Text).font-display: fallback;
: Cung cấp một khoảng thời gian nhỏ để tải font, nếu không tải kịp sẽ dùng font fallback và không bao giờ thử lại.font-display: optional;
: Font chỉ được tải nếu kết nối đủ nhanh. Nếu không, dùng font fallback và font web sẽ không được tải hoặc dùng cho lần truy cập sau.
@font-face { font-family: 'Ten Awesome Font'; src: url('/fonts/ten-awesome-font.woff2') format('woff2'), url('/fonts/ten-awesome-font.woff') format('woff'); font-weight: normal; font-style: normal; font-display: swap; /* Quan trọng cho performance */ }
- Giải thích:
font-display: swap
là lựa chọn phổ biến nhất để cân bằng giữa tốc độ hiển thị nội dung và việc sử dụng font custom.
2. Giảm Thiểu Yêu Cầu (Reducing Requests)
Mỗi tài nguyên (HTML, CSS, JS, hình ảnh, font, API call...) mà trình duyệt cần tải đều yêu cầu một "chuyến đi" đến server. Mỗi chuyến đi này tốn thời gian thiết lập kết nối (TCP handshake, SSL negotiation) và thời gian chờ phản hồi (latency). Giảm số lượng chuyến đi này sẽ tăng tốc độ tải trang đáng kể.
- Bundling File: Như đã nói ở trên, gộp CSS và JS.
- CSS Sprites (Lỗi thời nhưng khái niệm vẫn đúng): Gộp nhiều ảnh nhỏ (như icon) thành một ảnh lớn duy nhất, sau đó dùng CSS
background-position
để hiển thị phần cần thiết. Ngày nay, với HTTP/2 và SVG, sprites ít phổ biến hơn, nhưng ý tưởng giảm số lượng file ảnh nhỏ vẫn còn giá trị. Nhúng tài nguyên nhỏ trực tiếp vào code (Inlining): Đối với các file CSS hoặc SVG rất nhỏ và quan trọng cho render ban đầu, bạn có thể nhúng chúng trực tiếp vào thẻ
<style>
hoặc<svg>
trong HTML.- Ví dụ (CSS):
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Trang web của tôi</title> <style> /* Các CSS quan trọng cho phần hiển thị đầu tiên (critical CSS) */ body { font-family: sans-serif; margin: 0; } .header { background-color: #f0f0f0; padding: 10px; } </style> <link rel="stylesheet" href="/css/main.css"> <!-- Tải CSS còn lại sau --> </head> <body> <header class="header">...</header> <!-- Nội dung khác --> <script src="/js/main.js" defer></script> </body> </html>
- Giải thích: Bằng cách nhúng CSS quan trọng (
critical CSS
) trực tiếp vào HTML, trình duyệt có thể render phần trên của trang (above-the-fold) mà không cần chờ tải file CSS bên ngoài. Điều này cải thiện First Contentful Paint (FCP) và Largest Contentful Paint (LCP) - các chỉ số đo tốc độ trải nghiệm người dùng.
HTTP/2 và HTTP/3: Đây là các phiên bản mới của giao thức HTTP cho phép tải nhiều tài nguyên đồng thời trên cùng một kết nối (multiplexing) và nén header, giảm đáng kể overhead của mỗi request. Hãy đảm bảo server của bạn hỗ trợ và cấu hình sử dụng HTTP/2 hoặc HTTP/3.
3. Tận Dụng Bộ Nhớ Đệm (Leveraging Caching)
Caching giúp trình duyệt lưu trữ các tài nguyên đã tải về (CSS, JS, ảnh, font...) và sử dụng lại chúng cho các lần truy cập sau hoặc các trang khác trên cùng website mà không cần tải lại từ server. Điều này giúp các lần tải trang tiếp theo nhanh như chớp!
Cache Trình Duyệt (Browser Cache): Sử dụng các HTTP header phù hợp trên server để chỉ định tài nguyên nào nên được cache và trong bao lâu (
Cache-Control
,Expires
,ETag
,Last-Modified
).- Ví dụ (Header phản hồi từ Server):
HTTP/1.1 200 OK Content-Type: text/css Content-Length: 1234 Cache-Control: public, max-age=31536000 // Cache 1 năm ETag: "abcdef1234567890"
- Giải thích:
Cache-Control: max-age=31536000
yêu cầu trình duyệt cache file CSS này trong 31536000 giây (tương đương 1 năm).public
cho phép cache bởi các máy chủ proxy.ETag
là một mã định danh duy nhất cho phiên bản của file; trình duyệt có thể gửi nó trong request tiếp theo để server kiểm tra xem file có thay đổi không mà không cần tải lại toàn bộ (Conditional Request).
Service Workers: Một công nghệ nâng cao cho phép bạn kiểm soát lập trình cách trình duyệt xử lý các request mạng và cache tài nguyên. Service Workers mở ra khả năng xây dựng Progressive Web Apps (PWA) với các tính năng như hoạt động offline, chiến lược caching phức tạp (cache first, network first...), push notifications.
- Ví dụ (Một phần code Service Worker khái niệm):
// service-worker.js const CACHE_NAME = 'my-website-cache-v1'; const urlsToCache = [ '/', '/css/main.css', '/js/main.js', '/images/logo.png' ]; // Lắng nghe sự kiện 'install' self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME) .then(cache => { console.log('Opened cache'); return cache.addAll(urlsToCache); // Cache các tài nguyên ban đầu }) ); }); // Lắng nghe sự kiện 'fetch' (mỗi khi trình duyệt request tài nguyên) self.addEventListener('fetch', event => { event.respondWith( caches.match(event.request) // Kiểm tra xem request có trong cache không .then(response => { if (response) { return response; // Nếu có, trả về từ cache } return fetch(event.request); // Nếu không, thực hiện request mạng bình thường }) ); });
- Giải thích: Đoạn code này minh họa chiến lược "Cache First, then Network". Khi Service Worker được cài đặt, nó cache một danh sách các tài nguyên quan trọng. Với mỗi request mạng sau đó, nó đầu tiên kiểm tra trong cache. Nếu tìm thấy, nó trả về ngay lập tức (rất nhanh). Nếu không, nó mới thực hiện request mạng thực tế.
4. Tối Ưu Quá Trình Render (Rendering Optimization)
Sau khi tải về tài nguyên, trình duyệt cần phân tích (parsing) HTML, CSS, JS, xây dựng cây DOM (Document Object Model), cây CSSOM (CSS Object Model), kết hợp chúng thành cây Render Tree, bố cục (Layout/Reflow) và vẽ lên màn hình (Paint/Repaint). Quá trình này có thể tốn thời gian, đặc biệt với các trang phức tạp hoặc khi có nhiều thay đổi đến bố cục.
Giảm thiểu Reflow và Repaint:
- Reflow (hoặc Layout): Xảy ra khi thay đổi bố cục của một phần tử làm ảnh hưởng đến bố cục của các phần tử khác (ví dụ: thay đổi width, height, padding, margin, position). Reflow là quá trình tốn kém nhất.
- Repaint: Xảy ra khi chỉ thay đổi các thuộc tính hình ảnh của phần tử mà không ảnh hưởng đến bố cục (ví dụ: background-color, color, visibility). Repaint ít tốn kém hơn Reflow.
- Tránh các thao tác gây Reflow lặp đi lặp lại: Ví dụ, khi thay đổi nhiều thuộc tính CSS, hãy gộp chúng lại và thay đổi class hoặc sử dụng thuộc tính
style
để thay đổi một lần. - Ví dụ (JavaScript - thao tác DOM):
// Kém hiệu quả: Mỗi lần thay đổi một thuộc tính có thể gây Reflow/Repaint const element = document.getElementById('myElement'); element.style.width = '100px'; // Có thể gây Reflow element.style.height = '50px'; // Có thể gây Reflow element.style.marginTop = '10px'; // Có thể gây Reflow // Hiệu quả hơn: Thay đổi nhiều thuộc tính một lúc const element = document.getElementById('myElement'); element.style.cssText = 'width: 100px; height: 50px; margin-top: 10px;'; // Thay đổi một lần // Hoặc: Thay đổi class const element = document.getElementById('myElement'); element.classList.add('new-styles'); /* Trong CSS: .new-styles { width: 100px; height: 50px; margin-top: 10px; } */
- Giải thích: Trình duyệt cố gắng tối ưu bằng cách "gom" các thay đổi và thực hiện Reflow/Repaint một lần. Nhưng nếu bạn yêu cầu thông tin bố cục giữa các lần thay đổi (ví dụ: đọc
offsetHeight
sau khi thay đổiwidth
), trình duyệt sẽ buộc phải thực hiện Reflow ngay lập tức. Thay đổi style hoặc class một lần giúp trình duyệt tối ưu tốt hơn.
Sử dụng
DocumentFragment
khi thêm nhiều phần tử DOM: Khi thêm một danh sách các phần tử mới vào DOM, việc thêm từng phần tử một có thể gây Reflow/Repaint lặp đi lặp lại. Sử dụngDocumentFragment
cho phép bạn xây dựng cấu trúc DOM "ngoài luồng" và chỉ thêm nó vào DOM thật một lần duy nhất.- Ví dụ:
const list = document.getElementById('myList'); const items = ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5']; // Cách kém hiệu quả: // items.forEach(itemText => { // const li = document.createElement('li'); // li.textContent = itemText; // list.appendChild(li); // Appending 5 times => tiềm năng 5 Reflows/Repaints // }); // Cách hiệu quả hơn với DocumentFragment: const fragment = document.createDocumentFragment(); // Tạo fragment items.forEach(itemText => { const li = document.createElement('li'); li.textContent = itemText; fragment.appendChild(li); // Thêm vào fragment (không ảnh hưởng DOM thật) }); list.appendChild(fragment); // Chỉ 1 lần thêm vào DOM thật => 1 Reflow/Repaint
- Giải thích:
DocumentFragment
hoạt động như một container tạm thời trong bộ nhớ. Bạn có thể thêm bao nhiêu phần tử tùy thích vào đó mà không gây ra bất kỳ thay đổi nào trên màn hình. Chỉ khi bạn thêm chínhfragment
vào DOM thật, quá trình render mới diễn ra.
Sử dụng
transform
vàopacity
cho animation: Các thuộc tính CSS nhưwidth
,height
,top
,left
thường gây Reflow khi thay đổi. Ngược lại, các thuộc tính nhưtransform
(translate, scale, rotate) vàopacity
có thể được xử lý bởi GPU (Graphics Processing Unit), dẫn đến animation mượt mà hơn và hiệu quả hơn về mặt hiệu suất.- Ví dụ (CSS Animation):
.moving-box { width: 50px; height: 50px; background-color: red; /* Tránh dùng 'left' hoặc 'top' cho animation phức tạp */ /* transition: left 0.5s ease; left: 0; */ /* Ưu tiên dùng transform */ transition: transform 0.5s ease; transform: translateX(0); } .moving-box.move { /* left: 100px; */ /* Gây Reflow */ transform: translateX(100px); /* Thường được GPU hỗ trợ */ }
- Giải thích: Khi bạn thêm class
move
vào.moving-box
, thay đổitransform
sẽ được xử lý hiệu quả hơn rất nhiều so với thay đổileft
.
Tránh CSS
@import
: Thay vì dùng@import url("another.css");
trong file CSS, hãy dùng<link rel="stylesheet" href="another.css">
trong HTML.@import
khiến trình duyệt phải tải về file CSS chính trước, sau đó mới phát hiện ra file CSS cần import và tải tiếp, tạo ra "chuỗi yêu cầu" (request chain) không cần thiết và trì hoãn quá trình render.
5. Tối Ưu Mã JavaScript (Efficient JavaScript)
JavaScript là ngôn ngữ mạnh mẽ nhưng cũng có thể là nguyên nhân gây chậm trang nếu không được sử dụng cẩn thận. JS có thể chặn quá trình phân tích HTML và render nếu nó được tải và thực thi theo cách mặc định.
Tải bất đồng bộ (Asynchronous Loading):
- Sử dụng thuộc tính
async
hoặcdefer
trên thẻ<script>
.<script src="script.js" async></script>
: Tải script song song với việc phân tích HTML. Script sẽ được thực thi ngay khi tải xong và có thể chặn quá trình phân tích HTML nếu nó tải xong trước khi HTML được phân tích hoàn toàn. Thứ tự thực thi của các scriptasync
không được đảm bảo. Tốt cho các script độc lập, không phụ thuộc vào script khác và không cần thao tác DOM ngay lập tức (ví dụ: Google Analytics).<script src="script.js" defer></script>
: Tải script song song với việc phân tích HTML. Script sẽ được thực thi sau khi toàn bộ HTML đã được phân tích xong, nhưng trước khi sự kiệnDOMContentLoaded
fired. Thứ tự thực thi của các scriptdefer
được đảm bảo theo thứ tự chúng xuất hiện trong HTML. Tốt cho các script phụ thuộc vào DOM hoặc phụ thuộc lẫn nhau (ví dụ: script chính của ứng dụng, plugin jQuery).- Không sử dụng
async
hoặcdefer
: Đây là mặc định. Trình duyệt sẽ dừng việc phân tích HTML, tải script, thực thi script, sau đó mới tiếp tục phân tích HTML. Điều này rất tệ cho performance nếu script nằm ở<head>
.
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>JS Optimization</title> <!-- Script chặn render (tránh dùng ở đây nếu có thể) --> <!-- <script src="/js/blocking-script.js"></script> --> <!-- Script tải bất đồng bộ, thực thi ngay khi tải xong --> <script src="/js/analytics.js" async></script> <!-- Script tải bất đồng bộ, thực thi sau khi DOM hoàn thành --> <script src="/js/main-app.js" defer></script> </head> <body> <!-- Nội dung HTML --> </body> </html>
- Giải thích: Đặt các thẻ
<script>
ở cuối thẻ<body>
cũng là một cách để tránh chặn phân tích HTML, nhưngasync
vàdefer
linh hoạt hơn và được khuyến khích sử dụng.defer
thường là lựa chọn an toàn và hiệu quả nhất cho hầu hết các script cần thao tác với DOM.
- Sử dụng thuộc tính
Tránh các tác vụ JS dài và chặn luồng chính (Avoid Long Running Tasks): JavaScript chạy trên luồng chính (main thread) của trình duyệt, cùng với việc xử lý layout, painting. Nếu một script thực thi quá lâu mà không "nhường lại" cho trình duyệt, nó sẽ khiến trang web bị đơ, không phản hồi các tương tác của người dùng.
- Giải pháp: Chia nhỏ các tác vụ lớn thành các tác vụ nhỏ hơn, sử dụng
setTimeout
hoặcrequestAnimationFrame
để lên lịch cho các phần của tác vụ chạy trong các khung hình khác nhau. Hoặc sử dụng Web Workers cho các tác vụ tính toán nặng (chạy trên một luồng riêng biệt).
- Giải pháp: Chia nhỏ các tác vụ lớn thành các tác vụ nhỏ hơn, sử dụng
Tối ưu Event Handling:
- Event Delegation: Thay vì gắn hàng trăm event listener vào từng phần tử nhỏ (ví dụ: từng button trong danh sách), hãy gắn một event listener duy nhất vào phần tử cha của chúng. Khi một sự kiện (như click) xảy ra trên phần tử con, nó sẽ "nổi bọt" (bubble up) lên phần tử cha. Trong event listener của phần tử cha, bạn có thể kiểm tra xem sự kiện đó đến từ phần tử con nào. Kỹ thuật này giảm số lượng listener, tiết kiệm bộ nhớ và cải thiện hiệu suất, đặc biệt với các danh sách động.
- Ví dụ:
<ul id="myList"> <li>Item 1</li> <li>Item 2</li> <li>Item 3</li> </ul> <script> // Kém hiệu quả: Gắn listener cho từng li (nếu danh sách dài hoặc thay đổi động) // const items = document.querySelectorAll('#myList li'); // items.forEach(item => { // item.addEventListener('click', function() { // console.log('Clicked:', this.textContent); // }); // }); // Hiệu quả hơn với Event Delegation: document.getElementById('myList').addEventListener('click', function(event) { // Kiểm tra xem sự kiện có xuất phát từ thẻ LI không if (event.target.tagName === 'LI') { console.log('Clicked:', event.target.textContent); } }); </script>
- Giải thích: Chỉ có một listener duy nhất được gắn vào thẻ
<ul>
. Khi một<li>
được click, sự kiện click sẽ "nổi bọt" lên<ul>
. Listener của<ul>
sẽ bắt sự kiện này và dùngevent.target
để xác định phần tử con nào đã gây ra sự kiện.
Hủy đăng ký Event Listener (Remove Listeners): Nếu bạn gắn event listener vào các phần tử mà sau này bị xóa khỏi DOM (ví dụ: khi đóng modal, chuyển trang trong SPA), hãy nhớ hủy đăng ký listener đó để tránh rò rỉ bộ nhớ (memory leak). Framework/thư viện (React, Vue) thường xử lý việc này tự động trong lifecycle methods của component.
Các Công Cụ Hỗ Trợ Tối Ưu
Bạn không cần phải "đoán" xem trang web của mình nhanh hay chậm. Có rất nhiều công cụ mạnh mẽ để đo lường và phân tích hiệu suất:
- Google PageSpeed Insights: Phân tích tốc độ tải trang trên cả di động và máy tính, đưa ra các chỉ số Core Web Vitals và gợi ý chi tiết để cải thiện.
- WebPageTest: Cho phép bạn kiểm tra tốc độ tải từ nhiều địa điểm trên thế giới, trên các loại kết nối và trình duyệt khác nhau. Cung cấp biểu đồ Waterfall chi tiết của tất cả các request.
- Chrome DevTools (Tab Performance, Network, Lighthouse): Công cụ tích hợp sẵn trong trình duyệt Chrome, cực kỳ mạnh mẽ để phân tích hiệu suất runtime (tab Performance), kiểm tra chi tiết từng request (tab Network), và chạy audit tổng thể (tab Lighthouse - tích hợp PageSpeed Insights ngay trong trình duyệt).
- Lighthouse (CLI): Chạy Lighthouse từ dòng lệnh, tiện lợi cho việc tích hợp vào quy trình CI/CD.
Hãy sử dụng thường xuyên các công cụ này để xác định điểm nghẽn (bottleneck) và đo lường hiệu quả của các biện pháp tối ưu mà bạn áp dụng.
Thực Hành: Áp Dụng Các Kỹ Thuật
Bây giờ là lúc bạn áp dụng những kiến thức này vào thực tế. Hãy chọn một trang web bạn đã xây dựng (có thể là bài tập từ các bài trước hoặc một dự án cá nhân) và thử áp dụng các kỹ thuật sau:
- Phân tích ban đầu: Dùng Lighthouse hoặc PageSpeed Insights để xem điểm hiệu suất hiện tại. Ghi lại các chỉ số như FCP, LCP, TBT (Total Blocking Time).
- Tối ưu hình ảnh: Nén, chọn định dạng phù hợp, dùng
loading="lazy"
,srcset
hoặc<picture>
. - Thu gọn và Gộp CSS/JS: Nếu chưa dùng các build tool tự động, bạn có thể dùng các công cụ online để minify thử. (Trong các dự án thực tế, build tool sẽ làm việc này).
- Kiểm tra việc tải JS: Đảm bảo các script lớn có thuộc tính
defer
. - Xem xét CSS quan trọng: Liệu có thể nhúng một phần CSS nhỏ vào
<head>
? - Kiểm tra lại: Chạy lại công cụ phân tích và so sánh kết quả. Bạn sẽ thấy sự khác biệt!
Hãy nhớ: Tối ưu performance là một quá trình liên tục, không phải là làm một lần rồi xong. Khi bạn thêm tính năng mới, hãy luôn để ý đến tác động của nó lên hiệu suất.
Comments