Tìm hiểu sâu hơn về quá trình tải tập lệnh

Jake Archibald
Jake Archibald

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.

Không, chờ, quay lại! Tôi biết điều này nghe có vẻ nhàm chán và đơn giản, nhưng hãy nhớ rằng điều này đang xảy ra trong trình duyệt, nơi mà về mặt lý thuyết, điều này trở thành lỗ hổng bảo mật cũ. Việc biết được các điểm khác biệt này cho phép bạn chọn cách tải tập lệnh nhanh nhất, ít gây phiền toái nhất. Nếu bạn đang có một lịch trình dày đặc, hãy chuyển đến tài liệu tham khảo nhanh.

Đối với người mới bắt đầu, sau đây là cách thông số kỹ thuật xác định các cách tải xuống và thực thi một tập lệnh:

WhatWG khi tải tập lệnh
WhatWG khi tải tập lệnh

Giống như tất 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 cụm trong một nhà máy sản xuất vụn, nhưng một 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ị:

Tập lệnh đầu tiên của tôi bao gồm

<script src="//other-domain.com/1.js"></script>
<script src="2.js"></script>

Ahh, thật đơn giản và thú vị. Tại đây, trình duyệt sẽ tải song song cả hai tập lệnh xuống rồi thực thi các tập lệnh đó sớm nhất có thể, giúp giữ nguyên thứ tự của các tập lệnh đó. "2.js" sẽ không thực thi cho đến khi "1.js" đã được thực thi (hoặc không thực hiện được), "1.js" sẽ không thực thi cho đến khi tập lệnh hoặc biểu định kiểu trước đó đã được thực thi, v.v.

Rất tiếc, trình duyệt sẽ chặn thêm việc hiển thị trang trong khi tất cả điều này xảy 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 qua, 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 tải nội dung bên ngoài có thể cần (js, hình ảnh, css, v.v.), nhưng việc hiển thị vẫn bị chặn.

Đây là lý do tại sao tài liệu của bạn chứa các yếu tố tuyệt vời và tốt đẹp của kịch bản, vì tập lệnh sẽ chặn càng ít nội dung càng tốt. Rất tiếc, điều đó có nghĩa là trình duyệt không thấy tập lệnh của bạn cho đến khi trình duyệt 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, chẳng hạn 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.

Cảm ơn IE! (không, tôi không hề 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à đưa "trì hoãn" vào Internet Explorer 4. Về cơ bản, thông báo này thể hiện rằng "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 những nội dung như document.write. Nếu tôi phá vỡ lời hứa đó, bạn có thể tuỳ ý phạt tôi theo bất kỳ hình thức nào mà bạn thấy phù hợp”. Thuộc tính này đã chuyển thành HTML4 và xuất hiện trên các trình duyệt khác.

Trong ví dụ trên, trình duyệt sẽ tải song song cả hai tập lệnh xuống và thực thi các tập lệnh đó ngay trước khi DOMContentLoaded kích hoạt, giúp duy trì thứ tự của các tập lệnh đó.

Giống như một quả bom nổ trong nhà máy cừu, việc “trì hoãn” trở thành một mớ hỗn độn. Giữa các thuộc tính “src” và “defer”, cũng như thẻ tập lệnh so với các tập lệnh được thêm tự động, chúng ta có 6 mẫu thêm tập lệnh. Tất nhiên, các trình duyệt không thống nhất được thứ tự thực thi. Mozilla đã viết một bài viết hay về vấn đề này ngay từ năm 2009.

WhatWG đã nêu rõ hành vi này, tuyên bố "trì hoãn" không ảnh hưởng đến các tập lệnh được thêm tự độ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ự thêm các tập lệnh đó.

Cảm ơn IE! (Được rồi, giờ tôi đang châm biếm)

Cho đi, nhận lại. Rất tiếc, đã xảy ra 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. Dưới đâ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 trên trang, thứ tự dự kiến của các nhật ký là [1, 2, 3], mặc dù trong IE9 trở xuống, bạn sẽ nhận được [1, 3, 2]. Các hoạt động DOM cụ thể khiến IE tạm dừng việc thực thi tập lệnh hiện tại và thực thi các tập lệnh đang chờ xử lý khác trước khi tiếp tục.

Tuy nhiên, ngay cả trong các quá trình triển khai không bị lỗi, chẳng hạn như IE10 và các trình duyệt khác, việc thực thi tập lệnh 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 định đợi DOMContentLoaded. Tuy nhiên, nếu muốn thực sự tập trung vào hiệu suất, bạn có thể bắt đầu thêm trình nghe và tự khởi động sớm hơn...

HTML5 giải cứu

<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 "async" (không đồng bộ), giả định bạn sẽ không sử dụng document.write nhưng không đợi cho đến khi tài liệu đã phân tích cú pháp để thực thi. Trình duyệt sẽ tải song song cả hai tập lệnh xuống và thực thi các tập lệnh đó sớm nhất có thể.

Thật không may, vì "1.js" 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 tốt nếu chúng độc lập, có lẽ "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à bản sao CDN của jQuery mà "2.js" phụ thuộc vào, thì trang của bạn sẽ không có gì được co cụm trong lỗi ... này

Tôi biết thứ chúng ta cần, một thư viện JavaScript!

Bí quyết thành công là có một tập lệnh tải 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 tập lệnh. Rất tiếc, HTML ghét bạn và sẽ không cho phép bạn làm điều đó.

Vấn đề này đã được JavaScript giải quyết theo một số phiên bản. Một số lệnh yêu cầu bạn thay đổi JavaScript, gói mã này trong một lệnh gọi lại mà thư viện gọi theo đúng thứ tự (ví dụ: RequireJS). Những người khác sẽ sử dụng XHR để tải song song sau đó xuống eval() theo đúng thứ tự. Cách 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ó tiêu đề CORS và trình duyệt đã hỗ trợ tập lệnh này. Một số người thậm chí còn sử dụng cách tấn công siêu kỳ diệu như LabJS.

Các vụ tấn công này liên quan đến việc lừa trình duyệt tải tài nguyên xuống theo cách sẽ kích hoạt một sự kiện khi hoàn tất, nhưng tránh thực thi nó. Trong LabJS, tập lệnh sẽ được thêm với loại mime không chính xác, ví dụ: <script type="script/cache" src="...">. Sau khi tất cả tập lệnh được tải xuống, chúng sẽ được thêm lại với đúng loại, hy vọng trình duyệt sẽ lấy ngay các tập lệnh đó từ bộ nhớ đệm và thực thi chúng ngay lập tức, theo đúng 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à đã xảy ra lỗi khi trình duyệt được khai báo HTML5 không được tải xuống tập lệnh có loại không nhận dạng được. Đáng chú ý là 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 tự gặp vấn đề về hiệu suất, 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à trình tải này 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? Làm cách nào để tải tập lệnh để trình tải tập lệnh biết cần tải gì? Ai xem Watchmen? Tại sao tôi lại khoả thân? Đây toàn là những câu hỏi khó.

Về cơ bản, nếu bạn phải tải xuống một tệp tập lệnh bổ sung trước khi nghĩ đến việc tải xuống các tập lệnh khác, bạn đã thua cuộc chiến hiệu suất ngay ở đó.

DOM sẽ giải cứu

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âu lệnh đó thành "earthling":

[
  '//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 sẽ không đồng bộ theo mặc định, những tập lệnh này không chặn quá trình kết xuất và thực thi ngay khi tải xuống, tức 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 chúng là 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 tập lệnh của chúng ta kết hợp các hành vi mà HTML thuần tuý không thể thực hiện được. Bằng cách rõ ràng không đồng bộ, tập lệnh được thêm vào hàng đợi thực thi, chính hàng đợi mà chúng được thêm vào trong ví dụ đầu tiên về HTML thuần tuý. Tuy nhiên, bằng cách được tạo một cách linh động, chúng sẽ đượ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 việc kết xuất sẽ không bị chặn trong khi chúng được tải xuống (đừng nhầm lẫn việc tải tập lệnh không đồng bộ với việc đồng bộ hoá XHR, điều này chưa bao giờ là một điều tốt).

Tập lệnh ở trên phải được đưa vào cùng dòng trong phần đầu trang, xếp hàng đợi tải xuống tập lệnh càng sớm càng tốt mà không làm gián đoạn quá trình hiển thị liên tục và thực thi càng sớm càng tốt theo thứ tự bạn chỉ định. "2.js" có thể tải xuống miễn phí 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. Hurrah! tải xuống không đồng bộ nhưng thực thi có thứ tự!

Việc tải tập lệnh theo cách này được mọi thứ hỗ trợ thuộc tính không đồng bộ, ngoại trừ Safari 5.0 (5.1 là ổn). Ngoài ra, tất cả các phiên bản Firefox và Opera được hỗ trợ dưới dạng các phiên bản không hỗ trợ thuộc tính async (không đồng bộ) một cách thuận tiện thực thi các tập lệnh được thêm động theo thứ tự chúng được thêm vào tài liệu.

Đó là cách nhanh nhất để tải tập lệnh, đúng không? Đúng không?

Vâng, nếu bạn chủ động quyết định việc tập lệnh nào sẽ tải, có, nếu không, có 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á tập lệnh nào cần tải xuống. Thao tác này sẽ ẩn các tập lệnh của bạn khỏi việc tải trước trình quét. Trình duyệt sử dụng những trình quét này để khám phá tài nguyên trên các trang bạn có thể 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ị chặn bởi một tài nguyên khác.

Chúng ta có thể thêm lại tiềm năng được khám phá bằng cách đặt đoạn mã này ở đầu tài liệu:

<link rel="subresource" href="//other-domain.com/1.js">
<link rel="subresource" href="2.js">

Điều này cho trình duyệt biết 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 và bạn phải khai báo tập lệnh nào sẽ tải hai lần, một lần thông qua các phần tử liên kết và một lần nữa trong tập lệnh của mình.

Đính chính: Ban đầu tôi nói rằng những thông tin này do trình quét tải trước nhận, nhưng không phải, chúng được trình phân tích cú pháp thông thường nhận. Tuy nhiên, việc tải trước trình quét có thể nhận các trình quét này, nhưng vẫn chưa thể thực hiện, trong khi các tập lệnh được bao gồm trong mã thực thi không bao giờ được tải trước. Cảm ơn Yoav [Tên doanh nghiệp] đã 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 thất vọng

Tình hình thật không vui và có lẽ bạn sẽ cảm thấy chán nản. Không có cách khai báo nào mang tính lặp lại để tải tập lệnh xuống một cách nhanh chó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 tới mứ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 nâng cao xử lý một thành phần trang cụ thể, nhưng cần có các hàm hiệu dụng trong dependency.js. Tốt nhất là chúng ta nên tải xuống tất cả một cách không đồng bộ, sau đó thực thi các tập lệnh nâng cao càng sớm càng tốt, theo bất kỳ thứ tự nào, nhưng sau Parental.js. Đó là 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ừ khi các tập lệnh được sửa đổi để theo dõi trạng thái tải của dependency.js. Ngay cả async=false cũng không giải quyết được vấn đề này, vì việc thực thi additional-10.js sẽ chặn vào ngày 1-9. Trên thực tế, chỉ có một trình duyệt có thể làm được điều này mà không cần phải có mẹo...

IE có ý 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 bây giờ, các trình duyệt khác 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, "sẵn sàng" và thuộc tính "sẵn sàng", cho chúng tôi biết tiến trình tải. Điều 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 trang này vẫn gặp phải vấn đề tương tự về khả năng phát hiện trình tải trước như async=false.

Đủ rồi! Làm cách nào để tải tập lệnh?

Được rồi. Nếu bạn muốn tải tập lệnh theo cách không chặn hiển thị, không 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ử nội dung. Đúng vậy, việc trở thành một nhà phát triển web cũng giống như trở thành Vua Sisyphus (tuyệt vời! 100 điểm hipster để tham khảo thần thoại Hy Lạp!). Những giới hạn trong HTML và trình duyệt ngăn chúng tôi làm việc tốt hơn nhiều.

Tôi hy vọng các mô-đun JavaScript sẽ cứu chúng ta bằng cách cung cấp phương thức khai báo không chặn để tải tập lệnh và trao quyền kiểm soát thứ tự thực thi, mặc dù điều này yêu cầu phải viết tập lệnh dưới dạng mô-đun.

Ôi, phải có thứ gì đó tốt hơn chúng ta có thể dùng không?

Đủ công bằng, để kiếm thêm điểm thưởng, nếu bạn muốn thực sự năng động về hiệu suất và không bận tâm đến một chút 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 về tài nguyên phụ cho các trình tải trước:

<link rel="subresource" href="//other-domain.com/1.js">
<link rel="subresource" href="2.js">

Sau đó, 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 trở lại việ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 để 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>');
  }
}

Một vài thủ thuật và việc rút gọn sau này, đó là 362 byte + URL tập lệnh của bạn:

!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"
])

Tập lệnh này có đáng để thêm các byte so với một tập lệnh đơn giản không? Nếu đã sử dụng JavaScript để tải tập lệnh có điều kiện, như BBC cũng, 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ó lẽ bạn không nên sử dụng phương pháp xác định phần cuối đơn giản.

Ôi, giờ tôi đã biết tại sao phần tải tập lệnh whatWG rộng đến vậy. Tôi cần một cốc nước.

Tài liệu tham khảo nhanh

Phần tử 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 mọi CSS đang chờ xử lý, chặn hiển thị cho đến khi hoàn tất. Trình duyệt cho biết: Có!

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. Bạn thấy không thú vị sao? Trình duyệt có 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ể nó không có ở đó. Các trình duyệt khác cho biết: Vâng, nhưng tôi có thể sẽ không bỏ qua “trì hoãn” trên những 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 bất kỳ thứ tự tải xuống nào. Trình duyệt có màu đỏ cho biết: Có vấn đề gì "không đồng bộ"? Tôi sẽ tải tập lệnh như thể tập lệnh này không có ở đó. Các trình duyệt khác cho biết: Vâng.

Async false

[
  '1.js',
  '2.js'
].forEach(function(src) {
  var script = document.createElement('script');
  script.src = src;
  script.async = false;
  document.head.appendChild(script);
});

Quy cách cho biết: Tải xuống cùng nhau, thực thi theo thứ tự ngay khi tất cả tệp tải xuống. Firefox < 3.6, Opera cho biết: Tôi không hiểu điều "không đồng bộ" này là gì, nhưng đúng là tôi thực thi các tập lệnh được thêm qua JS theo thứ tự thêm chúng. Safari 5.0 cho biết: Tôi hiểu là "không đồng bộ", nhưng không hiểu việc đặt giá trị thành "false" đối với JS. Tôi sẽ thực thi tập lệnh của bạn ngay khi tập lệnh được tải xuống, theo bất kỳ thứ tự nào. IE < 10 cho biết: Không có ý tưởng nào về "không đồng bộ", nhưng có 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 điều "không đồng bộ" này, tôi sẽ thực thi tập lệnh của bạn ngay sau khi chúng hạ cánh, theo bất kỳ thứ tự nào. Mọi thứ khác nói: Tôi là bạn của bạn, chúng ta sẽ thực hiện việc này dựa trên cuốn sách.