Tổng hợp các cách tối ưu hoá cho website (Frontend)
Mở đầu
Trong thế giới lập trình website đang phát triển rất nhanh như hiện nay cả về công nghệ lẫn AI thì việc tạo ra 1 trang web hiện nay đang ngày càng trở nên dễ dàng hơn với những người bán chuyên về lập trình web.
Trong đó, Frontend đang dẫn đầu về số thư viện và framework,hay thậm chí là những tool,extension cho phép người dùng làm việc đó đó dễ dàng hơn.
Tuy nhiên, cùng với sự phát triển nhanh chóng như vậy (đặc biệt với sự bùng nổ của chatGPT), developer ngày càng lười và không chịu tư duy về những kiến thức căn bản và bản chất của vấn đề.Dẫn đến sự phụ thuộc vào AI và vô tính tự biến mình thành "thợ code".
Và hậu quả là chính chúng ta sẽ dần dần bị thay thế bởi AI nếu không tự biến mình thành 1 lập trình viên đúng nghĩa bằng cách tập trung vào những giá trị cốt lõi của mọi vấn đề.
Và để củng cố điều đó,mình có tổng hợp 1 số kiến thức căn bản và cách tối ưu hoá về frontend qua các chỉ mục sau:
- Cách hoạt động khi mở 1 website
- Tối ưu hoá lượng data tải lên (sizing optimization)
- Tối ưu hoá bằng Cache (caching optimization)
- Tối ưu hoá thời gian chờ tải data (waiting optimization)
- Ví dụ cụ thể
Website hoạt động như thế nào (frontend)
Như hình vẽ, chúng ta có 1 số khái niệm cơ bản như HTML,CSS,JS và DOM, CSSOM (document object model và CSS object model)
Với HTML,CSS,JS thì chúng ta sẽ có 2 khái niệm được dùng để chỉ việc upload data lên trình duyệt.Đó là Render Blocking Resource và Parser Blocking Resource
-
Render Blocking là việc chặn hiển thị xảy ra khi trình duyệt không thể hiển thị nội dung lên màn hình do phải tải, phân tích hoặc xử lý một số tài nguyên nhất định, thường là các tệp CSS hoặc JavaScript.
-
Parser Blocking là quá trình chặn trình phân tích cú pháp xảy ra khi trình phân tích cú pháp HTML của trình duyệt ngừng hoạt động do một số tài nguyên nhất định, đặc biệt là JavaScript.
Nguyên nhân và cách khắc phục sẽ được trình bày trong những phần sau.
DOM là một giao diện trung lập về ngôn ngữ và nền tảng cho phép các chương trình và tập lệnh truy cập và cập nhật nội dung, cấu trúc và kiểu của tài liệu một cách động. (theo W3C)
Nói cách khác,DOM có thể hiểu là cầu nối giữa HTML và JS và nó đã được define sẵn cho chúng ta sử dụng
Còn CSSOM là một tập hợp các API cho phép thao tác CSS từ JavaScript. Nó rất giống DOM, nhưng dành cho CSS chứ không phải HTML. Nó cho phép người dùng đọc và sửa đổi kiểu CSS một cách động (và cũng thể hiểu là nó như 1 'plugin' giữa CSS và JS)
Tiếp theo chúng ta có Render Tree
Quá trình kết hợp giữa DOM và CSSOM tạo ra Render Tree (cây kết xuất)
Render Tree sẽ làm những gì?
- Render tất cả các Node cần thiết để hiển thị(visible Node) (ví dụ những Node có display: none, thẻ script, thẻ meta sẽ không được hiển thị)
- Những visible Node sẽ được được match với các CSSOM tương ứng
- Truyền visible Nodes gồm content và các CSS đã đc tính toán của chúng đến Layout
Tiếp theo sẽ là Layout
Cho tới thời điểm này,chúng ta đã có được sản phầm bao gồm các thẻ HTML cần hiển thị cùng với CSS của chúng đã được "chuẩn hoá và tối giản".Tuy nhiên vị trí của chúng so với màn hình(viewport) và kích thước chính xác thì chưa được thực hiện.Và Layout sẽ thực hiện điều đó.
Ví dụ các bước dưới đây sẽ giúp chúng ta tưởng tượng dễ dàng hơn:
- Sản phẩm của bước Render Tree:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Critial Path: Hello world!</title>
</head>
<body>
<div style="width: 50%">
<div style="width: 50%">Hello world!</div>
</div>
</body>
</html>
- Với các giá trị tương đối như width: 50%, Layout process sẽ tính toán kích thước của các thẻ
- Và kết quả của đầu ra chúng ta có dạng 'box model', với các giá trị được tính toán từ tương đối thành tuyệt đối ứng với đơn vị pĩxel.Output này sẽ cần 1 khoảng thời gian để trình duyệt làm việc và đi đến bước cuối là Paint. Chúng ta có thể kiểm tra quá trình này bằng Chrome DevTool ở mục record,và được thể hiện qua hình ảnh sau:
- Quá trình Paint là thực tế hoá các model từ render tree như output được đề cập ở trên, chúng ta có sản phẩm là các pixel điểm ảnh được xuất hiện ở trên bộ nhớ.
- Sau đó được thông qua bước Composite sẽ ghép các pixel lại với nhau nếu có bất kỳ pixel nào chồng lên nhau.
- Cuối cùng Display sẽ là vẽ vật lý tất cả các điểm ảnh thu được lên màn hình.
Tối ưu hoá data tải lên (sizing optimization)
- Minify resource gửi lên từ server:
- Là việc remove các khoảng trắng,comment,cắt ngắn tên function,gộp các giá trị có chung thuộc tính là cách giảm dung lượng của file CSS hoặc JS, giúp làm giảm việc Render-blocking đã được nhắc ở trên.
- Ngoài ra,chúng ta còn 1 cách nữa đó là dùng gzip, nén các file lại rồi để trình duyệt unzip file để sử dụng.Cách này chúng ta sẽ đánh đổi tốc độ gửi file với tốc độ unzip.Nhưng với tốc độ giải nén của các máy tính bây giờ thì vấn đề này ko đáng kể.
- Cách sử dụng:
- Đối với minify resource thông thường, thì có thể tham khảo 1 số tool có sẵn trên mạng.Ngoài ra, nếu sử dụng NodeJS thì ta có thể sử dụng thư viện để làm nhanh hơn.
- Với việc nén file GZIP, chúng ta có thể cấu hình server để compress file trước khi được đẩy lên browser, hoặc cấu hình từ code frontend trước thông qua sử dụng thư viện kết hợp cấu hình webpack.
- Tree-shaking.
- Đây là kỹ thuật 'rung cây nhưng ko doạ khỉ' 🙂, nó là kỹ thuật nâng cao,cần có độ hiểu biết nhất định về JS,nhiệm vụ chính là loại bỏ những deadcode để giảm bundle code.
- Thường được dùng trong các thư viện và framework hiện này,đặc điểm chung là đều sử dụng NodeJS và sử dụng 1 số JS bundler như: Webpack, Vite, Rollup,...
- Cách sử dụng: Tree-shaking dựa vào ES6 module và bundler(như trên) để phân chia code sao cho hợp lý,chi tiết cách sử dụng thì rất nhiều và phụ thuộc nhiều vào kinh nghiệm của DEV.
- Code-splitting
- Là kỹ thuật chia để trị. Đơn giản là chia nhỏ 1 file bundle lớn thành nhiều file bundle nhỏ,làm các giảm thời gian tải,giúp dễ quản lý code cũng như tối ưu hoá thời gian loading.
- Cách sử dụng: phụ thuộc vào kinh nghiệm của DEV,1 số ví dụ như chia nhỏ module trong angular,kết hợp lazy-loading các component ở hầu hết các lib hay framework hiện nay.
- Tối ưu hoá việc tải media (video, image, audio, ...)
- Sử dụng JS bundler: Webpack, Vite, ... kết hợp cùng 1 số plugin để minimize size image khi build
- Tự giảm size của ảnh trước khi dùng trong project:
- TinyPNG tool (giảm chất lượng ảnh, áp dụng với ảnh loại nhỏ)
- Sử dụng format ảnh như WEBP,AVIF (nó sẽ compress ảnh làm giảm dung lượng nhưng chất lượng ko thay đổi).
Caching optimization
- Cache bằng CDN (Content Delivery Network).
- Khái niệm: CDN là một mạng lưới phân tán các máy chủ được đặt ở những vị trí chiến lược trên toàn cầu. Nó lưu trữ nội dung web của bạn (ví dụ: HTML, CSS, JavaScript, hình ảnh, video) và phân phối từ máy chủ gần nhất với người dùng.
- Mục đích: Giảm độ trễ, cải thiện thời gian tải và giảm tải lưu lượng truy cập khỏi máy chủ gốc.
- Cache như nào?
- Khi người dùng yêu cầu nội dung, CDN sẽ kiểm tra xem có bản sao được lưu trong bộ nhớ đệm ở máy chủ gần nhất hay không.
- Nếu nội dung tồn tại trong bộ nhớ đệm (cache hit), nội dung đó sẽ được phục vụ trực tiếp.
- Nếu không (cache miss), yêu cầu sẽ được chuyển tiếp đến máy chủ gốc, lấy và lưu trữ trong CDN để sử dụng sau này.
- Cache những gì?
- Nội dung tĩnh: Hình ảnh, CSS, JavaScript, video.
- Nội dung động: Với CDN hiện đại ngày nay, ngay cả nội dung động cũng có thể được lưu vào bộ nhớ đệm bằng các thuật toán thông minh hoặc logic biên.
- Cách sử dụng:
- Chọn nhà cung cấp CDN: Cloudflare(điển hình nhất), các nhà cung cấp CDN của dịch vụ đám mây:Azure,Google,AWS
- Config CDN: bao gồm setting Domain và config Caching (bao gồm config default and custom rules) (mn có thể search để tìm hiểu thêm để rõ hơn)
- Phù hợp các trang web có ít sự thay đổi
- Ví dụ:
- Với ảnh,phần config CDN được set như trong hình
- Với file JS (được thay đổi thường xuyên)
- IndexDB
- Khái niệm:
- Tương tự như localStorage,IndexDB cho phép lưu trữ dữ liệu, bao gồm các tệp và thông tin có cấu trúc, trực tiếp trong trình duyệt của người dùng, giúp bạn có thể truy cập ngay cả khi thiết bị ngoại tuyến.
- Một cơ sở dữ liệu NoSQL cấp thấp trong trình duyệt để lưu trữ lượng dữ liệu lớn. Nó không đồng bộ và giao dịch.
- Khác nhau với localSotrage và service worker(sẽ được nhắc đến phần sau)
Chức năng IndexedDB Service Worker Cache API LocalStorage Giới hạn dung lượng Lớn (up to GBs) Phụ thuộc browser ~5MB Kiểu dữ liệu Objects, files Request/Response pairs Strings only Hỗ trợ Offline Yes Yes Limited Use Case Structured, dynamic Static assets Simple key-value pairs
- Use case:
- Offline data storage.
- Caching API responses or static assets (e.g., images, JSON).
- Lưu trữ liên tục cho Progressive Web Apps (PWAs).
- Được ứng dụng vào react-persist để lưu trữ store khi offline
- Cách sử dụng:
- Cache data response khi offline (thường là data lớn, với data nhỏ có thể lưu ở localStorage)
- Lưu trữ config của 1 component phức tạp. Ví dụ: config table,...
Waiting Optimization
-
Async - defer - module
Chắc hẳn mọi người khi bắt đầu học HTML,JS đều được nói rằng: "lên để thẻ script ở cuối cùng và thẻ link trên cùng". Mục đích của việc đó đơn giản là muốn user nhìn thấy toàn bộ giao diện trước khi thực thi bất kỳ logic nào.
Tuy nhiên vẫn còn 1 số case ngoài lệ chúng ta cần load JS đầu tiên(bao gồm cả download và execution script),ví dụ như:- Load critical authentication or security scripts
- Load performance monitoring: track important metrics(page load timing, user behavior, error tracking)
- Khi sử dụng module patterns:(angular,vuejs,...)
Với async, như hình vẽ script sẽ được download cùng lúc với việc parsing HTML,sau đó browser sẽ tạm dừng việc parse html để excution JS.Sẽ đỡ 1 phần lớn download JS về,nó sẽ thích hợp cho case load performance monitoring hay critical authen phía trên.
Với defer, script được download đồng thời với việc parse HTML, và excute JS cuối cùng sau khi parse toàn bộ HTML, việc sử dụng defer sẽ thích hợp với các loại file JS có 'liên quan' đến UI,component hoặc như script để hiện quảng cáo,.. sao cho việc chưa excute JS không làm ảnh hưởng đến UI.
Với module, được phát triển từ ES6,nó có nhiều lợi ích như:- Tự động defer
- Trong module chúng ta có import,và tất cả import được thực hiện đồng thời
- Loading optimization: modules được load 1 lần và được cache
- Dynamic Imports: async... await import.
-
Lazy loading
Như cái tên của nó,chúng ta có thể hiểu nó sẽ tối ưu tài nguyên được tải lên cho đến khi cần thiết. 1 số ứng dụng thực tế:- Dynamic Import (như trình bày phần module phía trên)
- Route lazy loading (ứng dụng trong các framework,lib hiện đại ngày nay như react, angular,vue,...)
- Kết hợp cùng IntersectionObserver để tối ưu loading page, tài nguyên bao gồm: image(thêm attribute loading="lazy"), data from API, component, ...
- Ngoài ra có thể kết hợp với pagination hoặc event scroll để tiết kiệm tài nguyên tải lên
3. Split long task
Thực ra đây ko phải phương pháp nào cụ thể mà nó chính là vấn đề về trình độ của mỗi DEV,mình có thể tóm tắt sơ qua nội dung như sau:
Như ta đã biết thì JS là đơn luồng (single thread).Vì vậy mọi thứ sẽ chạy theo thứ tự từ trên đúng dưới, trừ 2 loại đó là macrotask queue và microtask queue (từ khoá tìm hiểu: Event Loop, microtasks and macrotasks,....Khi nào mình xong bài về EventLoop sẽ đổi link ).
Ta sẽ có 1 số ví dụ minh hoạ:
1. Chia nhỏ chunk (ở tab network ở devtool mn sẽ thấy những thanh biểu thị dung lượng và thời gian load tài nguyên): việc thưc hiện 1 task quá nặng(ex: chạy 1 vòng loop vài trăm nghìn item, callback hell,...) sẽ dẫn đến treo máy và ảnh hưởng đến User experiences.Và cách giải quyết như trong hình
2. Chia nhỏ file data khi upload,download.
3.Chia nhỏ các task lớn thành các task nhỏ,ưu tiên những task ảnh hưởng đến UI,sau đó mới đến các task thực hiện logic hay call api,...
4. Sử dụng WebWorker.
-
Khái niệm:
Một tài nguyên miễn phí mà khá ít những DEV mới vào nghề biết đến đó là WebWorker.
Vậy thì WebWorker có tác dụng gì? Và những usecase thực tế của nó sẽ được sử dụng như thế nào?
Đầu tiên chúng ta sẽ tìm hiểu vấn đề của JS,như ta đã biết JS hoạt động trên 1 single main thread(đơn luồng), và điều đó khiến 1 số tác vụ tính toán nặng nề(ví dụ như chart realtime hay những tính toán liên quan đến cryptocurrency, website sử dụng 3D animation) có thể bị lag khi chạy.
Và hậu quả là màn hình bị đơ,giật lag và ảnh hưởng đến trải nghiệm người dùng.
Và để giải quyết vấn đề đó,Webworker được sinh ra.Thay vì chạy những tính toán nặng nề trên tại main thread thì chúng ta có thể chạy nó trên webworker.Web Workers nói một cách đơn giản, là một phương tiện để chạy một tập hợp các lệnh ở chế độ nền(background hay worker thread).
Thread này (worker context) sẽ hoạt động tách biệt với main thread của JS (hay chính là main context của window).Điều đó sẽ khiến nó không thể sử dụng được 1 số api của window ví dụ như: Không thể modify trực tiếp DOM từ worker thread hay 1 số default method của window.Tuy nhiên vẫn có thể sử dụng được phần lớn các hàm có sẵn trong window như WebSocket hay IndexDB. Bạn có thể tham khảo các hàm khác ở đây -
Use case:
- Xử lý hình ảnh: Dành cho các ứng dụng web có các tính năng như thay đổi kích thước hình ảnh, lọc, thao tác và nhận dạng khuôn mặt. Web workers có thể được sử dụng để thao tác canvas phức tạp hoặc hiển thị hình ảnh ở chế độ nền.
- Mã hóa và giải mã video: Các nhà phát triển sử dụng web worker trong các ứng dụng phát trực tuyến để xử lý các tệp video để phát lại hoặc chỉnh sửa mà không làm đóng băng giao diện người dùng.
- Ứng dụng thời gian thực: Ứng dụng thời gian thực sử dụng socket.io để lấy dữ liệu. Các ứng dụng web như nền tảng nhắn tin và crypto dashboard hiển thị giá tiền điện tử mới nhất trong biểu đồ.
- Background Tasks : Web workers có thể xử lý các nhiệm vụ không yêu cầu cập nhật trực quan ngay lập tức trên trang. Ví dụ bao gồm highlight cú pháp trong coding editor online(vd: vscode online, sandbox,...). Nó cũng được sử dụng để lấy lượng lớn dữ liệu API phụ trợ.
5. Preload and prefetch
- Preload
- Khái niệm: Như tên gọi, Nó sử dụng để pre-loading tài nguyên quan trọng và cần thiết đối với website, preload là một lệnh declarative fetch, cho phép bạn buộc trình duyệt thực hiện yêu cầu về một tài nguyên mà không chặn sự kiện onload document của tài liệu(đưa priority loading lên đầu khi init website)
- Use case:
- Fonts used immediately
- Hero/banner images (usually e-commerce website)
- Critical CSS/JS (JS for SEO,...)
- Authentication scripts
- Initial API data
- Main navigation assets
- Shopping cart data
- Payment processing scripts
- User profile information
- Critical third-party scripts
- Prefetch:
- Khái niệm: Prefetch là một gợi ý cho trình duyệt rằng có thể cần đến một tài nguyên nào đó, nhưng việc quyết định có nên tải tài nguyên đó hay không và thời điểm tải là do trình duyệt quyết định.Vì vậy độ ưu tiên của nó sẽ thấp hơn Preload.
- Usecases:
- DNS prefetching
- Critical resources
- Preconnect
- Viewport-Based Prefetching
- Link Prefetching for Navigation
Kết luận:
Trên đây là nhưng phương pháp rất căn bản mà chúng ta cần phải nắm vững để khi làm với thư viện và framework có thể hiểu cơ chế và khắc phục chúng nếu bài toán chúng ta gặp phải cần đến.Chúc mn sẽ vận dụng tốt để có thể xử lý các case study 1 cách tốt nhất 😀
All rights reserved