Giới thiệu
Trong bài viết này, tôi sẽ hướng dẫn bạn cách tải một số JavaScript trong trình duyệt và thực thi mã đó.
Không, đợi đã, quay lại! Tôi biết điều này nghe có vẻ đơn giản và tầm thường, nhưng hãy nhớ rằng việc này đang diễn ra trong trình duyệt, nơi những thứ đơn giản về mặt lý thuyết trở thành một lỗ hổng kỳ quặc do các tính năng cũ gây ra. Khi biết những điều kỳ quặc này, bạn có thể chọn cách tải tập lệnh nhanh nhất và ít gây gián đoạn nhất. Nếu bạn có lịch trình bận rộn, hãy chuyển đến phần tham khảo nhanh.
Để bắt đầu, sau đây là cách thông số kỹ thuật xác định nhiều cách mà tập lệnh có thể tải xuống và thực thi:
Giống như tất cả các thông số kỹ thuật của WHATWG, ban đầu, nó trông giống như hậu quả của một quả bom chùm trong một nhà máy scrabble, nhưng sau khi bạn đọc nó lần thứ 5 và lau sạch máu khỏi mắt, nó thực sự khá thú vị:
Kịch bản đầu tiên của tôi bao gồm
<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>
Ôi, sự đơn giản tuyệt vời. Tại đây, trình duyệt sẽ tải cả hai tập lệnh xuống song song và thực thi chúng sớm nhất có thể, duy trì thứ tự của các tập lệnh đó. “2.js” sẽ không thực thi cho đến khi “1.js” thực thi (hoặc không thực thi được), “1.js” sẽ không thực thi cho đến khi tập lệnh hoặc tệp kiểu trước đó thực thi, v.v.
Rất tiếc, trình duyệt chặn việc hiển thị trang trong khi tất cả những điều này đang diễn ra. Điều này là do các API DOM từ "thời đại đầu tiên của web" cho phép các chuỗi được thêm vào nội dung mà trình phân tích cú pháp đang nhai, chẳng hạn như document.write
. Các trình duyệt mới hơn sẽ tiếp tục quét hoặc phân tích cú pháp tài liệu ở chế độ nền và kích hoạt quá trình tải nội dung bên ngoài xuống (js, hình ảnh, css, v.v.) nếu cần, nhưng vẫn chặn việc hiển thị.
Đó là lý do tại sao các chuyên gia về hiệu suất khuyên bạn nên đặt các phần tử tập lệnh ở cuối tài liệu, vì việc này sẽ chặn ít nội dung nhất có thể. Thật không may, điều đó có nghĩa là trình duyệt sẽ không nhìn thấy tập lệnh của bạn cho đến khi tập lệnh tải xuống tất cả HTML của bạn và vào thời điểm đó, tập lệnh bắt đầu tải xuống nội dung khác như CSS, hình ảnh và iframe. Các trình duyệt hiện đại đủ thông minh để ưu tiên JavaScript hơn hình ảnh, nhưng chúng ta có thể làm tốt hơn thế.
Cảm ơn IE! (không, tôi không thể hiện sự châm biếm)
<script src="//other-domain.com/1.js" defer></script>
<script src="2.js" defer></script>
Microsoft đã nhận ra những vấn đề về hiệu suất này và giới thiệu tính năng "trì hoãn" vào Internet Explorer 4. Về cơ bản, điều này có nghĩa là "Tôi hứa sẽ không chèn nội dung vào trình phân tích cú pháp bằng các tính năng như document.write
. Nếu tôi vi phạm lời hứa đó, bạn có thể phạt tôi theo bất cứ cách nào bạn thấy phù hợp". Thuộc tính này đã chuyển thành HTML4 và xuất hiện trong các trình duyệt khác.
Trong ví dụ trên, trình duyệt sẽ tải cả hai tập lệnh xuống song song và thực thi các tập lệnh đó ngay trước khi DOMContentLoaded
kích hoạt, duy trì thứ tự của các tập lệnh.
Giống như một quả bom chùm trong nhà máy cừu, tính năng "trì hoãn" đã trở thành một mớ hỗn độn. Giữa thuộc tính “src” và “defer” cũng như thẻ tập lệnh so với tập lệnh được thêm động, chúng ta có 6 mẫu để thêm một tập lệnh. Đương nhiên là các trình duyệt đã không đi đến thống nhất về thứ tự thực thi. Mozilla đã viết một bài viết hay về vấn đề này vào năm 2009.
whatWG làm rõ hành vi, tuyên bố "defer" (trì hoãn) không ảnh hưởng đến các tập lệnh được thêm vào một cách linh động hoặc thiếu "src". Nếu không, các tập lệnh bị trì hoãn sẽ chạy sau khi tài liệu được phân tích cú pháp theo thứ tự chúng được thêm vào.
Cảm ơn IE! (được rồi, giờ tôi đang nói đùa)
Nó cho đi, được nhận lại. Rất tiếc, có một lỗi khó chịu trong IE4-9 có thể khiến các tập lệnh thực thi theo thứ tự không mong muốn. Sau đây là những gì sẽ xảy ra:
1.js
console.log('1');
document.getElementsByTagName('p')[0].innerHTML = 'Changing some content';
console.log('2');
2.js
console.log('3');
Giả sử có một đoạn văn trên trang, thứ tự nhật ký dự kiến là [1, 2, 3], mặc dù trong IE9 trở xuống, bạn sẽ nhận được [1, 3, 2]. Một số thao tác DOM cụ thể khiến IE tạm dừng quá trình thực thi tập lệnh hiện tại và thực thi các tập lệnh khác đang chờ xử lý trước khi tiếp tục.
Tuy nhiên, ngay cả trong các phương thức triển khai không gặp lỗi, chẳng hạn như IE10 và các trình duyệt khác, quá trình thực thi tập lệnh sẽ bị trì hoãn cho đến khi toàn bộ tài liệu được tải xuống và phân tích cú pháp. Điều này có thể thuận tiện nếu bạn vẫn sẽ đợi DOMContentLoaded
, nhưng nếu muốn thực sự linh hoạt về hiệu suất, bạn có thể bắt đầu thêm trình nghe và khởi động sớm hơn...
HTML5 có thể giải quyết vấn đề
<script src="//other-domain.com/1.js" async></script>
<script src="2.js" async></script>
HTML5 cung cấp cho chúng tôi một thuộc tính mới có tên là “async” (không đồng bộ), giả định rằng bạn sẽ không sử dụng document.write
, nhưng không đợi đến khi tài liệu được phân tích cú pháp để thực thi. Trình duyệt sẽ tải cả hai tập lệnh xuống song song và thực thi chúng sớm nhất có thể.
Rất tiếc, vì các tệp này sẽ thực thi sớm nhất có thể, nên “2.js” có thể thực thi trước “1.js”. Điều này không sao nếu các tệp này độc lập, có thể “1.js” là một tập lệnh theo dõi không liên quan gì đến “2.js”. Nhưng nếu “1.js” là một bản sao CDN của jQuery mà “2.js” phụ thuộc vào đó, thì trang của bạn sẽ bị lỗi, giống như một quả bom chùm trong… Tôi không biết… Tôi không có gì cho lỗi này.
Tôi biết chúng ta cần gì, một thư viện JavaScript!
Mục tiêu chính là tải một tập hợp tập lệnh xuống ngay lập tức mà không chặn quá trình kết xuất và thực thi càng sớm càng tốt theo thứ tự thêm. Rất tiếc, HTML không cho phép bạn làm như vậy.
JavaScript đã giải quyết vấn đề này theo một số cách. Một số yêu cầu bạn phải thực hiện thay đổi đối với JavaScript, gói JavaScript đó trong một lệnh gọi lại mà thư viện gọi theo đúng thứ tự (ví dụ: RequireJS). Các tập lệnh khác sẽ sử dụng XHR để tải xuống song song, sau đó là eval()
theo đúng thứ tự. Điều này không hoạt động đối với các tập lệnh trên một miền khác, trừ phi các tập lệnh đó có tiêu đề CORS và trình duyệt hỗ trợ tiêu đề đó. Một số người thậm chí còn sử dụng kỹ năng siêu ảo thuật như LabJS.
Các vụ xâm nhập liên quan đến việc lừa trình duyệt tải tài nguyên xuống theo cách kích hoạt một sự kiện hoàn tất, nhưng tránh thực thi tài nguyên đó. Trong LabJS, tập lệnh sẽ được thêm bằng một loại mime không chính xác, ví dụ: <script type="script/cache" src="...">
. Sau khi tải tất cả tập lệnh xuống, các tập lệnh đó sẽ được thêm lại với loại chính xác, hy vọng trình duyệt sẽ lấy các tập lệnh đó ngay từ bộ nhớ đệm và thực thi ngay theo thứ tự. Điều này phụ thuộc vào hành vi thuận tiện nhưng không xác định và lỗi khi trình duyệt HTML5 được khai báo không nên tải xuống tập lệnh thuộc loại không được nhận dạng. Xin lưu ý rằng LabJS đã điều chỉnh cho phù hợp với những thay đổi này và hiện sử dụng kết hợp các phương thức trong bài viết này.
Tuy nhiên, trình tải tập lệnh có vấn đề về hiệu suất riêng, bạn phải đợi JavaScript của thư viện tải xuống và phân tích cú pháp trước khi bất kỳ tập lệnh nào mà thư viện quản lý có thể bắt đầu tải xuống. Ngoài ra, chúng ta sẽ tải trình tải tập lệnh như thế nào? Chúng ta sẽ tải tập lệnh để trình tải tập lệnh biết cần tải gì? Ai giám sát Watchmen? Tại sao tôi lại khoả thân? Đây đều là những câu hỏi khó.
Về cơ bản, nếu bạn phải tải thêm một tệp tập lệnh xuống trước khi nghĩ đến việc tải các tập lệnh khác xuống, bạn đã thua cuộc về hiệu suất ngay tại đó.
DOM sẽ giúp bạn giải quyết vấn đề
Câu trả lời thực sự nằm trong thông số kỹ thuật HTML5, mặc dù nó bị ẩn ở cuối phần tải tập lệnh.
Hãy dịch cụm từ đó thành "Người Trái Đất":
[
'//other-domain.com/1.js',
'2.js'
].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
document.head.appendChild(script);
});
Các tập lệnh được tạo động và thêm vào tài liệu không đồng bộ theo mặc định. Các tập lệnh này sẽ không chặn quá trình kết xuất và thực thi ngay khi tải xuống, có nghĩa là các tập lệnh này có thể xuất hiện theo thứ tự không chính xác. Tuy nhiên, chúng ta có thể đánh dấu rõ ràng các phương thức này là không không đồng bộ:
[
'//other-domain.com/1.js',
'2.js'
].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
script.async = false;
document.head.appendChild(script);
});
Điều này khiến các tập lệnh của chúng tôi có một tổ hợp các hành vi mà HTML thuần tuý không thể làm được. Bằng cách rõ ràng không đồng bộ, các tập lệnh sẽ được thêm vào hàng đợi thực thi, cùng một hàng đợi mà các tập lệnh được thêm vào trong ví dụ HTML thuần tuý đầu tiên của chúng ta. Tuy nhiên, do được tạo động nên các tệp này được thực thi bên ngoài quá trình phân tích cú pháp tài liệu, vì vậy, quá trình kết xuất sẽ không bị chặn trong khi tải xuống (đừng nhầm lẫn việc tải tập lệnh không đồng bộ với XHR đồng bộ, điều này không bao giờ là tốt).
Tập lệnh ở trên phải được đưa cùng dòng vào phần đầu trang, xếp hàng để tải tập lệnh xuống càng sớm càng tốt mà không làm gián đoạn quá trình hiển thị tăng dần, đồng thời thực thi càng sớm càng tốt theo thứ tự bạn đã chỉ định. Tải xuống miễn phí “2.js” trước “1.js”, nhưng nó sẽ không được thực thi cho đến khi “1.js” được tải xuống và thực thi thành công hoặc không thực hiện được. Hoan hô! Tải xuống không đồng bộ nhưng thực thi theo thứ tự!
Mọi thứ hỗ trợ thuộc tính không đồng bộ đều hỗ trợ việc tải tập lệnh theo cách này, ngoại trừ Safari 5.0 (5.1 thì không sao). Ngoài ra, tất cả phiên bản Firefox và Opera đều được hỗ trợ vì các phiên bản không hỗ trợ thuộc tính không đồng bộ vẫn thực thi các tập lệnh được thêm động theo thứ tự được thêm vào tài liệu.
Đó là cách nhanh nhất để tải tập lệnh phải không? Đúng không?
Nếu bạn đang linh động quyết định tập lệnh nào sẽ tải thì có, nếu không thì có thể là không. Với ví dụ trên, trình duyệt phải phân tích cú pháp và thực thi tập lệnh để khám phá những tập lệnh cần tải xuống. Thao tác này giúp ẩn tập lệnh của bạn khỏi trình quét tải trước. Trình duyệt sử dụng những trình quét này để khám phá tài nguyên trên những trang bạn có thể sẽ truy cập tiếp theo hoặc khám phá tài nguyên trang trong khi trình phân tích cú pháp bị một tài nguyên khác chặn.
Chúng ta có thể thêm khả năng tìm thấy lại bằng cách đặt phần này vào đầu tài liệu:
<link rel="subresource" href="//other-domain.com/1.js">
<link rel="subresource" href="2.js">
Thao tác này cho trình duyệt biết rằng trang cần 1.js và 2.js. link[rel=subresource]
tương tự như link[rel=prefetch]
, nhưng có ngữ nghĩa khác. Rất tiếc, tính năng này hiện chỉ được hỗ trợ trong Chrome. Bạn phải khai báo tập lệnh nào cần tải hai lần, một lần qua phần tử liên kết và một lần nữa trong tập lệnh của mình.
Sửa đổi: Ban đầu, tôi đã nói rằng các tệp này được trình quét tải trước thu thập, nhưng không phải vậy, mà là trình phân tích cú pháp thông thường. Tuy nhiên, trình quét tải trước có thể nhận được các tập lệnh này, nhưng chưa nhận được, trong khi các tập lệnh đi kèm với mã thực thi không bao giờ được tải trước. Cảm ơn Yoav Weiss đã sửa lỗi cho tôi trong phần bình luận.
Tôi thấy bài viết này gây buồn phiền
Tình hình đang khiến bạn trầm cảm và bạn sẽ cảm thấy tuyệt vọng. Không có cách khai báo nào không lặp lại nhưng nhanh chóng để tải tập lệnh xuống và không đồng bộ trong khi kiểm soát thứ tự thực thi. Với HTTP2/SPDY, bạn có thể giảm mức hao tổn yêu cầu đến mức việc phân phối tập lệnh trong nhiều tệp nhỏ có thể lưu vào bộ nhớ đệm riêng lẻ có thể là cách nhanh nhất. Hãy tưởng tượng:
<script src="dependencies.js"></script>
<script src="enhancement-1.js"></script>
<script src="enhancement-2.js"></script>
<script src="enhancement-3.js"></script>
…
<script src="enhancement-10.js"></script>
Mỗi tập lệnh tăng cường xử lý một thành phần trang cụ thể, nhưng yêu cầu các hàm tiện ích trong dependencies.js. Tốt nhất là chúng ta nên tải xuống tất cả theo cách không đồng bộ, sau đó thực thi các tập lệnh nâng cao sớm nhất có thể, theo thứ tự bất kỳ, nhưng sau phụ thuộc.js. Đó là tính năng cải tiến tăng dần! Rất tiếc, không có cách khai báo nào để đạt được điều này trừ phi bản thân các tập lệnh được sửa đổi để theo dõi trạng thái tải của phụ thuộc.js. Ngay cả async=false cũng không giải quyết được vấn đề này, vì việc thực thi enhancement-10.js sẽ chặn trên 1-9. Trên thực tế, chỉ có một trình duyệt có thể thực hiện việc này mà không cần hack…
IE có một ý tưởng!
IE tải tập lệnh theo cách khác với các trình duyệt khác.
var script = document.createElement('script');
script.src = 'whatever.js';
IE bắt đầu tải xuống "whatever.js" ngay lập tức, các trình duyệt khác sẽ không bắt đầu tải xuống cho đến khi tập lệnh được thêm vào tài liệu. IE cũng có một sự kiện "readystatechange" và thuộc tính "readystate" cho chúng ta biết tiến trình tải. Thao tác này thực sự hữu ích vì nó cho phép chúng ta kiểm soát việc tải và thực thi các tập lệnh một cách độc lập.
var script = document.createElement('script');
script.onreadystatechange = function() {
if (script.readyState == 'loaded') {
// Our script has download, but hasn't executed.
// It won't execute until we do:
document.body.appendChild(script);
}
};
script.src = 'whatever.js';
Chúng ta có thể xây dựng các mô hình phần phụ thuộc phức tạp bằng cách chọn thời điểm thêm tập lệnh vào tài liệu. IE đã hỗ trợ mô hình này kể từ phiên bản 6. Khá thú vị, nhưng vẫn gặp phải vấn đề về khả năng phát hiện trình tải trước giống như async=false
.
Đủ rồi! Làm cách nào để tải tập lệnh?
Ok ok. Nếu bạn muốn tải tập lệnh theo cách không chặn hiển thị, không liên quan đến lặp lại và có hỗ trợ trình duyệt tuyệt vời, sau đây là những gì tôi đề xuất:
<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>
Đó. Ở cuối phần tử body. Vâng, làm nhà phát triển web cũng giống như làm vua Sisyphus (bùng nổ! 100 điểm dành cho người sành điệu vì đã tham khảo thần thoại Hy Lạp!). Các hạn chế trong HTML và trình duyệt khiến chúng tôi không thể làm tốt hơn nhiều.
Tôi hy vọng các mô-đun JavaScript sẽ giải phóng chúng ta bằng cách cung cấp một cách khai báo không chặn để tải tập lệnh và cho phép kiểm soát thứ tự thực thi, mặc dù việc này đòi hỏi phải viết tập lệnh dưới dạng mô-đun.
Ôi, chắc chắn chúng ta có thể sử dụng một cách tốt hơn.
Để có điểm thưởng, nếu muốn cải thiện hiệu suất một cách mạnh mẽ và không ngại sự phức tạp và lặp lại, bạn có thể kết hợp một số thủ thuật ở trên.
Trước tiên, chúng ta thêm phần khai báo tài nguyên phụ cho trình tải trước:
<link rel="subresource" href="//other-domain.com/1.js">
<link rel="subresource" href="2.js">
Sau đó, cùng dòng trong phần đầu tài liệu, chúng ta tải tập lệnh bằng JavaScript, sử dụng async=false
, quay lại phương thức tải tập lệnh dựa trên trạng thái sẵn sàng của IE, quay lại phương thức trì hoãn.
var scripts = [
'1.js',
'2.js'
];
var src;
var script;
var pendingScripts = [];
var firstScript = document.scripts[0];
// Watch scripts load in IE
function stateChange() {
// Execute as many scripts in order as we can
var pendingScript;
while (pendingScripts[0] && pendingScripts[0].readyState == 'loaded') {
pendingScript = pendingScripts.shift();
// avoid future loading events from this script (eg, if src changes)
pendingScript.onreadystatechange = null;
// can't just appendChild, old IE bug if element isn't closed
firstScript.parentNode.insertBefore(pendingScript, firstScript);
}
}
// loop through our script urls
while (src = scripts.shift()) {
if ('async' in firstScript) { // modern browsers
script = document.createElement('script');
script.async = false;
script.src = src;
document.head.appendChild(script);
}
else if (firstScript.readyState) { // IE<10
// create a script and add it to our todo pile
script = document.createElement('script');
pendingScripts.push(script);
// listen for state changes
script.onreadystatechange = stateChange;
// must set src AFTER adding onreadystatechange listener
// else we'll miss the loaded event for cached scripts
script.src = src;
}
else { // fall back to defer
document.write('<script src="' + src + '" defer></'+'script>');
}
}
Sau một vài thủ thuật và việc rút gọn, kích thước của tập lệnh sẽ là 362 byte + URL của tập lệnh:
!function(e,t,r){function n(){for(;d[0]&&"loaded"==d[0][f];)c=d.shift(),c[o]=!i.parentNode.insertBefore(c,i)}for(var s,a,c,d=[],i=e.scripts[0],o="onreadystatechange",f="readyState";s=r.shift();)a=e.createElement(t),"async"in i?(a.async=!1,e.head.appendChild(a)):i[f]?(d.push(a),a[o]=n):e.write("<"+t+' src="'+s+'" defer></'+t+">"),a.src=s}(document,"script",[
"//other-domain.com/1.js",
"2.js"
])
Có đáng giá thêm byte so với một tập lệnh đơn giản không? Nếu đang sử dụng JavaScript để tải tập lệnh có điều kiện, như BBC, thì bạn cũng có thể hưởng lợi từ việc kích hoạt các tệp tải xuống đó sớm hơn. Nếu không, có thể bạn sẽ không cần sử dụng phương thức này. Hãy sử dụng phương thức đơn giản là kết thúc phần nội dung.
Phew, giờ tôi đã biết lý do phần tải tập lệnh WHATWG lại rộng lớn như vậy. Tôi cần uống chút gì đó.
Tài liệu tham khảo nhanh
Phần tử của tập lệnh thuần tuý
<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>
Thông số kỹ thuật cho biết: Tải xuống cùng nhau, thực thi theo thứ tự sau khi có bất kỳ CSS nào đang chờ xử lý, chặn quá trình kết xuất cho đến khi hoàn tất. Trình duyệt cho biết: Đúng rồi!
Hoãn
<script src="//other-domain.com/1.js" defer></script>
<script src="2.js" defer></script>
Thông số kỹ thuật cho biết: Tải xuống cùng nhau, thực thi theo thứ tự ngay trước DOMContentLoaded. Bỏ qua "trì hoãn" trên các tập lệnh không có "src". IE < 10 cho biết: Tôi có thể thực thi 2.js ở giữa quá trình thực thi 1.js. Điều đó không thú vị sao? Các trình duyệt màu đỏ cho biết: Tôi không biết điều "trì hoãn" này là gì. Tôi sẽ tải tập lệnh như thể không có ở đó. Các trình duyệt khác nói: Ok, nhưng tôi có thể không bỏ qua "defer" trên các tập lệnh không có "src".
Không đồng bộ
<script src="//other-domain.com/1.js" async></script>
<script src="2.js" async></script>
Thông số kỹ thuật cho biết: Tải xuống cùng nhau, thực thi theo thứ tự tải xuống bất kỳ. Các trình duyệt có màu đỏ cho biết: "Không đồng bộ" là gì? Tôi sẽ tải các tập lệnh như thể không có tập lệnh đó. Các trình duyệt khác sẽ cho biết: Vâng, ok.
Async false
[
'1.js',
'2.js'
].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
script.async = false;
document.head.appendChild(script);
});
Thông số kỹ thuật cho biết: Tải xuống cùng nhau, thực thi theo thứ tự ngay khi tất cả tải xuống. Firefox < 3.6, Opera nói: Tôi không biết "không đồng bộ" là gì, nhưng tình cờ tôi thực thi các tập lệnh được thêm qua JS theo thứ tự thêm. Safari 5.0 cho biết: Tôi hiểu "không đồng bộ", nhưng không hiểu cách đặt giá trị này thành "false" bằng JS. Tôi sẽ thực thi tập lệnh của bạn ngay khi tập lệnh đó được tải, theo thứ tự bất kỳ. IE < 10 cho biết: Không biết về "không đồng bộ", nhưng có một giải pháp sử dụng "onreadystatechange". Các trình duyệt khác có màu đỏ cho biết: Tôi không hiểu về "không đồng bộ" này, tôi sẽ thực thi tập lệnh của bạn ngay khi tập lệnh đó xuất hiện, theo thứ tự bất kỳ. Mọi thứ khác đều cho thấy: Tôi là bạn của bạn, chúng ta sẽ làm việc này theo đúng quy trình.