Bạn có một menu tốt. Khách hàng thích nó. Nó hoạt động tốt. Nhưng mỗi khi bạn kiểm tra store bằng PageSpeed Insights, Google lại cảnh báo về một file cụ thể: script menu của bạn. Nhãn nói “render-blocking JavaScript”, file 120KB, và nó đang thêm 600 mili giây vào LCP trên mobile. Bạn biết cần phải sửa, nhưng lần trước khi ai đó gợi ý dùng defer, menu đã bị hỏng. Nửa số dropdown không mở được, toggle mobile không hoạt động, và bạn đã phải khôi phục trong vòng một giờ.
Đây là vấn đề phổ biến. Render-blocking scripts là một trong những thắc mắc hiệu suất ảnh hưởng nhất trên các cửa hàng Shopify, và menu là nơi thường xuyên gặp phải. Nhưng hoãn JavaScript không chỉ là thêm một thuộc tính vào thẻ script. Nếu bạn làm sai, menu sẽ không hoạt động. Nếu bạn làm đúng, bạn có thể giảm nửa giây khỏi LCP mà không cần chỉnh sửa bất kỳ logic menu nào.
Bài viết này giải thích render-blocking thực sự là gì, tại sao async và defer hoạt động khác nhau, và cách tải script menu mà không làm hỏng chức năng.
- Render-blocking scripts dừng toàn bộ quá trình render trang cho đến khi chúng tải xuống, phân tích và thực thi.
defertải xuống scripts song song nhưng trì hoãn thực thi cho đến khi phân tích HTML hoàn thành.asynctải xuống và thực thi ngay lập tức, điều này có thể làm hỏng menu phụ thuộc vào thứ tự DOM.type="module"trì hoãn theo mặc định và hỗ trợ các tính năng JavaScript hiện đại.- Hầu hết các lỗi menu sau khi hoãn được gây ra bởi thời gian khởi tạo, không phải bởi chính thuộc tính defer.
Render-Blocking Thực Sự Là Gì
Khi trình duyệt gặp thẻ <script> trong HTML mà không có thuộc tính tải nào, nó dừng mọi thứ. Nó tạm dừng phân tích HTML, tải xuống script, phân tích nó, thực thi nó, và chỉ sau đó mới tiếp tục xây dựng trang. Đây được gọi là hoạt động đồng bộ hoặc render-blocking.
Logic đằng sau hành vi này cũ nhưng hợp lý: JavaScript có thể sửa đổi DOM bằng document.write(), vì vậy trình duyệt giả định rằng mỗi script có thể thay đổi cấu trúc của trang và do đó phải chạy trước khi tiếp tục.
Đây là vấn đề. Script menu của bạn không dùng document.write(). Nó không cần phải chạy trước khi phần còn lại của trang được phân tích. Nhưng trình duyệt không biết điều đó, vì vậy nó vẫn chặn.
Theo tài liệu riêng của Chrome về tối ưu hóa Largest Contentful Paint, loại bỏ render-blocking JavaScript là một trong những cải tiến có tác động cao nhất mà bạn có thể thực hiện. Khi một script trong phần đầu của tài liệu chặn trong 400 mili giây, mỗi phần tử dưới nó trong DOM—hình ảnh hero, tiêu đề, lưới sản phẩm—đều phải chờ 400 mili giây thêm để render.
Ba Chiến Lược Tải: Mặc Định, Defer, và Async
Có ba cách mà trình duyệt có thể tải một script.
Mặc Định (Đồng Bộ)
<script src="menu.js"></script>
Trình duyệt dừng phân tích HTML, tải xuống menu.js, phân tích nó, thực thi nó, rồi tiếp tục. Điều này chặn render cho toàn bộ thời gian.
Defer
<script src="menu.js" defer></script>
Trình duyệt tải xuống menu.js song song với phân tích HTML nhưng không thực thi nó cho đến khi toàn bộ tài liệu HTML đã được phân tích. Nhiều deferred scripts thực thi theo thứ tự chúng xuất hiện trong HTML.
Async
<script src="menu.js" async></script>
Trình duyệt tải xuống menu.js song song với phân tích HTML và thực thi nó ngay khi tải xuống hoàn thành, ngay cả khi phân tích HTML chưa hoàn thành. Thứ tự thực thi không thể dự đoán được nếu nhiều scripts dùng async.
Đây là bảng tóm tắt hành vi:
| Thuộc tính | Tải Xuống | Thực Thi | Chặn Phân Tích HTML | Thứ Tự Thực Thi |
|---|---|---|---|---|
| Không có (mặc định) | Chặn phân tích | Ngay lập tức | Có | Tuần tự |
defer |
Song song | Sau phân tích HTML | Không | Tuần tự |
async |
Song song | Sẵn sàng ngay lập tức | Có (trong quá trình thực thi) | Không thể dự đoán |
Đối với hầu hết menu điều hướng, defer là lựa chọn đúng. Nó giữ scripts theo thứ tự, điều này quan trọng khi một script phụ thuộc vào script khác (ví dụ, script menu phụ thuộc vào thư viện tiện ích). Và nó đảm bảo DOM sẵn sàng trước khi script chạy, điều này rất quan trọng đối với bất kỳ code nào truy vấn các phần tử.
Tại Sao Defer Làm Hỏng Một Số Menu (Và Cách Sửa)
Bạn thêm defer vào thẻ script menu, tải lại trang, và menu không hoạt động. Các dropdown không mở. Toggle mobile không làm gì. Bạn xóa defer và mọi thứ hoạt động trở lại. Có gì xảy ra?
Vấn đề gần như luôn là thời gian khởi tạo. Đây là ba nguyên nhân phổ biến nhất.
Vấn Đề 1: Script Chạy Trước Khi Theme JavaScript Sẵn Sàng
Nhiều theme Shopify có một đối tượng JavaScript toàn cục hoặc hàm khởi tạo mà các scripts khác phụ thuộc vào. Nếu script menu của bạn chạy trước khi đối tượng đó được tạo, nó sẽ thất bại.
Ví dụ:
// theme.js (tải đầu tiên)
window.Theme = { utils: { ... } };
// menu.js (tải thứ hai, mong đợi Theme tồn tại)
Theme.utils.initDropdown();
Nếu menu.js được hoãn nhưng theme.js không, menu.js có thể chạy trước khi theme.js kết thúc thực thi, và Theme sẽ không xác định.
Giải pháp: Hoãn cả hai scripts, hoặc đảm bảo menu.js kiểm tra các phụ thuộc trước khi khởi tạo.
if (typeof Theme !== 'undefined') {
Theme.utils.initDropdown();
}
Vấn Đề 2: Inline Scripts Chạy Trước Deferred Scripts
Inline scripts (JavaScript viết trực tiếp trong HTML, không tải từ file bên ngoài) thực thi theo thứ tự tài liệu khi chúng được gặp. Deferred scripts thực thi sau khi toàn bộ HTML được phân tích, có nghĩa là sau tất cả inline scripts.
Nếu bạn có một inline script cố gắng khởi tạo menu, và thư viện menu được hoãn, khởi tạo sẽ thất bại.
Giải pháp: Di chuyển logic khởi tạo vào deferred script, hoặc bao bọc khởi tạo inline trong một listener DOMContentLoaded.
document.addEventListener('DOMContentLoaded', function() {
if (typeof MenuApp !== 'undefined') {
MenuApp.init();
}
});
Vấn Đề 3: Race Conditions với Nhiều Deferred Scripts
Nếu menu của bạn phụ thuộc vào một thư viện tiện ích, và cả hai được hoãn, chúng sẽ thực thi theo thứ tự chúng xuất hiện trong HTML. Nhưng nếu thư viện tiện ích được tải không đồng bộ (sử dụng async thay vì defer), thứ tự thực thi trở nên không thể dự đoán.
Giải pháp: Sử dụng defer cho tất cả scripts có phụ thuộc lẫn nhau, không phải async. Nếu bạn phải sử dụng async, hãy thêm các kiểm tra phụ thuộc rõ ràng hoặc sử dụng dynamic imports.
Type=”module”: Giải Pháp Hiện Đại
JavaScript hiện đại hỗ trợ ES6 modules, có hoãn tải tích hợp.
<script type="module" src="menu.js"></script>
Scripts với type="module" tự động hoãn. Chúng tải xuống song song và thực thi sau khi phân tích HTML hoàn thành, giống như defer. Chúng cũng hỗ trợ import và export, giúp quản lý phụ thuộc rõ ràng.
Nếu script menu của bạn được viết dưới dạng module, bạn có thể nhập các phụ thuộc trực tiếp:
import { initDropdown } from './dropdown.js';
initDropdown();
Điều này loại bỏ các vấn đề về thời gian gây ra lỗi với defer truyền thống, vì trình duyệt biết chính xác scripts nào phụ thuộc vào cái nào.
Nhược điểm: các trình duyệt cũ hơn (Internet Explorer, các phiên bản Safari rất cũ) không hỗ trợ modules. Nhưng theo Can I Use, hơn 95% lưu lượng trình duyệt toàn cầu hỗ trợ ES6 modules kể từ năm 2025. Đối với hầu hết các cửa hàng Shopify, điều này là an toàn.
Những Cân Nhắc Riêng Cho Shopify
Các theme Shopify tải scripts theo các cách khác nhau tùy thuộc vào kiến trúc theme. Dưới đây là cách hoãn hoạt động trong các mẫu phổ biến nhất.
Theme.liquid Script Tags
Nếu script menu của bạn được tải trong theme.liquid như thế này:
{{ 'menu.js' | asset_url | script_tag }}
Bộ lọc script_tag không thêm defer theo mặc định. Bạn cần thay thế nó bằng thẻ <script> thủ công:
<script src="{{ 'menu.js' | asset_url }}" defer></script>
App Embed Blocks
Nếu menu của bạn được cài đặt dưới dạng app embed (phổ biến đối với các app menu của bên thứ ba), ứng dụng kiểm soát cách script được tải. Bạn không thể tự thêm defer. Bạn cần liên hệ với nhà phát triển ứng dụng hoặc chuyển sang một app menu tải scripts không đồng bộ.
Các công cụ như Navi+ tải JavaScript của chúng với defer theo mặc định, đó là một lý do tại sao chúng hoạt động tốt hơn trong các bài kiểm tra LCP so với các app menu cũ.
Section Rendering và Deferred Scripts
Các theme Shopify 2.0 tải sections động. Nếu một section bao gồm một deferred script, và section được thêm vào trang sau khi tải ban đầu (ví dụ, qua AJAX cho quick view hoặc infinite scroll), script có thể không thực thi như mong đợi.
Giải pháp: Chỉ dùng DOMContentLoaded cho tải trang ban đầu. Đối với các sections được thêm động, kích hoạt khởi tạo thủ công bằng cách sử dụng custom event hoặc mutation observer.
Cách Kiểm Tra Xem Defer Có Hoạt Động Không
Trước khi triển khai hoãn script loading trên cửa hàng trực tiếp của bạn, hãy kiểm tra kỹ lưỡng. Dưới đây là danh sách kiểm tra.
- Desktop Chrome: Mở cửa hàng của bạn, mở DevTools, đi tới tab Network, tải lại và xác minh script menu tải xuống song song với các tài nguyên khác và không chặn tài liệu.
- Mô phỏng mobile: Trong DevTools, chuyển sang mô phỏng mobile (iPhone hoặc Android), giảm tốc độ mạng xuống “Fast 3G” và tải lại. Menu vẫn nên khởi tạo đúng.
- Trình duyệt chéo: Kiểm tra trong Safari, Firefox và Edge. Module scripts hoạt động hơi khác nhau trong Safari.
- Thiết bị thực: Mở cửa hàng trên một điện thoại thực tế qua kết nối di động thực. Mô phỏng rất hữu ích, nhưng độ trễ thế giới thực lộ ra các lỗi thời gian mà kiểm tra desktop bỏ lỡ.
- Kiểm tra chức năng: Mở mỗi dropdown, kiểm tra toggle menu mobile, nhấp vào mỗi liên kết và xác minh không có gì bị hỏng.
Trước khi bạn đẩy lênChạy một bài kiểm tra PageSpeed Insights trên cùng một trang trước và sau khi bật defer. Bạn sẽ thấy một sự giảm đáng kể trong LCP và giảm tài nguyên render-blocking. Nếu LCP không cải thiện, script có thể không phải là thắc mắc, hoặc có thể có các tài nguyên blocking khác để giải quyết.
Khi Nào Async Có Ý Nghĩa (Và Khi Nào Không)
async phù hợp với các scripts hoàn toàn độc lập và không phụ thuộc vào thứ tự DOM hoặc các scripts khác. Ví dụ bao gồm analytics trackers, chat widgets, và một số ad scripts.
Đối với menu điều hướng, async là rủi ro. Menu cần DOM sẵn sàng, nó thường phụ thuộc vào theme utilities, và nó thường cần chạy trước khi khách hàng tương tác với trang. Nếu script thực thi trong khi HTML vẫn đang được phân tích, nó có thể cố gắng truy vấn các phần tử chưa tồn tại.
Nếu bạn đang xem xét async cho một script menu, script phải:
- Không phụ thuộc vào bất kỳ scripts nào khác
- Không truy vấn phần tử DOM (hoặc bao bọc tất cả các truy vấn trong kiểm tra
DOMContentLoaded) - Đủ nhỏ để thời gian thực thi của nó không chặn phân tích
Hầu hết các scripts menu không đáp ứng các tiêu chí này. Gắn bó với defer hoặc type="module" trừ khi bạn có lý do cụ thể để sử dụng async.
Menu Script Loading Tốt Trông Như Thế Nào
Một script menu được tối ưu hóa đúng cách trên một cửa hàng Shopify trông như thế này:
<script src="{{ 'menu.js' | asset_url }}" defer></script>
Hoặc, nếu dùng modules:
<script type="module" src="{{ 'menu.js' | asset_url }}"></script>
Script tải xuống song song với phần còn lại của trang, thực thi sau khi DOM sẵn sàng, và không chặn hình ảnh hero hoặc bất kỳ phần tử LCP nào khác từ render.
Trong Chrome DevTools Performance timeline, bạn sẽ thấy hình ảnh hero render trước khi script menu thực thi. Nếu script menu chạy trước, nó vẫn đang chặn.
Hầu hết các cửa hàng có thể cắt 300 đến 600 mili giây khỏi LCP di động chỉ bằng cách chuyển từ script loading đồng bộ sang hoãn. Menu vẫn hoạt động. Khách hàng không nhận thấy sự khác biệt. Nhưng Google có, và quan trọng hơn, trình duyệt có. Trang trở nên hiển thị nhanh hơn, đó là toàn bộ ý nghĩa.
Bài viết này là một phần của hướng dẫn lớn hơn về Menu và LCP: cách điều hướng chặn largest contentful paint của bạn.