<
  • Главная
Статьи

Глибоке занурення в каламутні води завантаження скриптів

  1. Вступ
  2. Як я підключав скрипт в перший раз
  3. Спасибі, IE! (І ніякого сарказму)
  4. От спасибі, IE! (І тепер це сарказм)
  5. 1.js
  6. HTML5 поспішає на допомогу
  7. Я знаю, що врятує нас! JavaScript-бібліотека!
  8. DOM поспішає на допомогу
  9. Це ж найшвидший спосіб завантажувати скрипти, правда? ПРАВДА?
  10. У мене розвивається депресія від цієї статті.
  11. У IE є ідея!
  12. Коротше! Скажіть вже, як мені завантажувати скрипти!
  13. Ммм, тобто нічого кращого зараз немає, так?
  14. Швидка довідка
  15. Defer
  16. Async
  17. Async false

# Глибоке занурення в каламутні води завантаження скриптів

Вступ

У цій статті я навчу вас, як завантажувати і виконувати JavaScript всередині браузера.

Ні, стривайте, поверніться! Я знаю, як це звучить, занадто банально і просто. Однак, слід пам'ятати, що весь процес відбувається всередині браузера, де теоретично прості речі швидко ховаються в вирі з багів і фич. Причина цієї плутанини - величезна кількість коду для підтримки зворотної сумісності. Якщо ви знаєте ці нюанси поведінки браузерів, то зможете вибрати найшвидший і найменш трудомісткий спосіб завантажувати скрипти. Якщо ви поспішаєте, можете відразу переходити до розділу швидкої довідки.

Для початку, ось як в специфікації визначаються різні способи, якими можна завантажити і виконати код:

WHATWG про завантаження скриптів

Як і всі специфікації WHATWG (Web Hypertext Application Technology Working Group, прим. Ред.), З першого погляду ця найбільше нагадує наслідки потрапляння кластерної бомби в фабрику з виробництва гри «Ерудит», але коли ви прочитаєте її в п'ятий раз і втре кров з очей, все стає насправді досить цікаво:

Як я підключав скрипт в перший раз

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

О, свята простота! Отже, в цьому прикладі браузер завантажить обидва скрипта паралельно і виконає їх якомога швидше, зберігши при цьому їх порядок. Скрипт 2.js не стане виконуватися, поки не виконається (або поки не видасть помилку) 1.js. 1.js, в свою чергу, не почне виконуватися, поки не буде виконано попередній скрипт (або не оброблена таблиця стилів).

На жаль, поки все це відбувається, браузер блокує подальший рендеринг сторінки. Відбувається це через який ми отримали в спадок з епохи «Веб 1.0» DOM API. API дозволяють додавати дані до вмісту, яке в даний момент прожевивает браузер, такі, наприклад, як document.write. Сучасні браузери будуть продовжувати сканувати і парсити документ в фоновому режимі, а також починати завантаження зовнішнього вмісту (JS, CSS, зображення і т.п.), яке потрібно документу, але рендеринг сторінки все одно буде блокований.

Саме з цієї причини кращі фахівці з продуктивності радять ставити елементи script в самому кінці документа - в такому випадку виконання скрипта блокує якомога менше контенту. На жаль, це означає, що браузер не побачить ваш скрипт до тих пір, поки не завантажить весь HTML, а до цього моменту він вже почав завантажувати інший контент: CSS, зображення, блоки iframe. Сучасні браузери досить розумні і дають JavaScript-файлів пріоритет в завантаженні вище, ніж картинками, але можна зробити ще краще.

Спасибі, IE! (І ніякого сарказму)

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

Microsoft звернула увагу на ці проблеми з продуктивністю, і, в Internet Explorer 4 був введений атрибут defer. Означає він, більш-менш, таке: «Я обіцяю не вставляти палиці в колеса парсеру і не буду використовувати такі речі, як document.write. Якщо я порушу цю обіцянку, можете карати мене на ваш розсуд ». Цей атрибут увійшов в специфікацію HTML4 і з'явився в інших браузерах.

В наведеному вище прикладі трапиться наступне: браузер завантажить обидва скрипта паралельно і виконає їх прямо перед тим, як спрацює подія DOMContentLoaded, в правильному порядку.

Атрибут defer, як вибухнула кластерна бомба на фабриці з виробництва вовни, раптово, перетворив все в моторошно заплутаний клубок. Разом, з атрибутами src і defer, тегами script і динамічно додаються скриптами, всього у нас виходило шість способів додати скрипт в документ. Не дивно, що розробники браузерів не змогли домовитися про те, в якому порядку вони повинні виконуватись. На сайті, присвяченому Хакама Mozilla, є відмінна стаття з описом проблеми , В тому вигляді, в якому вона була в 2009 році.

WHATWG стандартизувала поведінку, оголосивши, що defer не повинен мати ніякого ефекту на динамічно додані скрипти або скрипти без src. В інших випадках скрипти повинні виконуватися після того, як закінчився парсинг документа, в тому порядку, в якому вони були додані.

От спасибі, IE! (І тепер це сарказм)

Бог дав, Бог взяв. На жаль, в сімействі IE з версії 4 по 9 присутня дуже неприємний баг, який може змусити скрипти виконуватися в самому несподіваному порядку .

Ось що відбувається:

1.js

console .log ( '1'); document .getElementsByTagName ( 'p') [0] .innerHTML = 'Змінюємо який-небудь контент'; console .log ( '2');

2.js

console .log ( '3');

Якщо на сторінці присутній хоча б один абзац, то ми можемо очікувати, що порядок записів в лог буде йти так - [1, 2, 3], а в IE9 і нижче виходить так - [1, 3, 2]. Деякі маніпуляції з DOM змушують IE призупинити виконання поточного скрипта і виконати інші, які очікують виконання скрипти, перед тим, як продовжити.

Навіть в реалізаціях без цього бага (IE10 і інших браузерах) виконання скрипта буде відкладено до того моменту, як браузер завантажить і розпарсити весь документ. Не так уже й погано, якщо ви в будь-якому випадку збираєтеся чекати виконання DOMContentLoaded. Але якщо ви хочете дійсно агресивно підійти до оптимізації продуктивності, то підготовку коду до обробки подій можна почати раніше.

HTML5 поспішає на допомогу

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

У HTML5 з'явився новий атрибут, async. Його використання має на увазі, що ви не будете користуватися document.write, і браузер не чекатиме, поки документ повністю розпарсити, перед тим, як почати виконання зазначеного атрибутом скрипта. Браузер завантажить обидва скрипта паралельно і виконає їх як тільки зможе.

На жаль, в силу того факту, що скрипти будуть завантажуватися наскільки можливо швидко, може вийти так, що 2.js завантажиться перед 1.js. Загалом то немає проблем, якщо наші скрипти незалежні один від одного (1.js - скрипт аналітики, і не має нічого спільного зі скриптом 2.js). Але якщо, наприклад, 1.js - CDN-копія jQuery, від якої залежить виконання 2.js, тоді ваша сторінка покриється помилками, як вибухнула кластерна бомба в ...
в ... ой, навіть не можу придумати метафору.

Я знаю, що врятує нас! JavaScript-бібліотека!

Ідеальна ситуація виглядала б так: у нас є набір скриптів, які завантажуються негайно, не блокуючи рендеринг, і виконуються як тільки можливо, в тому порядку, в якому вони були додані. Дуже шкода, що HTML ненавидить вас і не дасть вам таке провернути.

Цю проблему намагалися кілька разів вирішити на рівні JavaScript. Одні рішення пропонували вам внести зміни в ваш JavaScript-код, обернувши його в колбек, який бібліотека викличе в правильному порядку (наприклад, RequireJS ). Інші використовують XHR для паралельної завантаження, а потім виконують через eval () в правильному порядку. Але цей метод не працює для скриптів знаходяться на інших доменах, якщо у них немає CORS-заголовка (А у браузера немає відповідної підтримки).

Деякі, як покійний LabJS, взагалі використовували супер-магічні хакі. Загальний принцип цих хаков виглядав так: змусити браузер завантажити ресурс так, що по закінченню завантаження буде викликано подія, але, при цьому, не виконувати ресурс.

У LabJS скрипт додавався з неправильним MIME-типом, наприклад: <script type = "script / cache" src = "...">. Після того, як всі скрипти скачати, вони додавалися знову, але, вже з правильним типом, в надії, що браузер завантажить їх з кешу і виконає відразу і по порядку. Це спиралося на поширене, але не відповідає специфікації поведінку. І все зламалося, коли в HTML5 було оголошено, що браузери не повинні завантажувати скрипти з невідомим типом.

У цих прийомів є парочка цілком чітких проблем в сфері продуктивності. Наприклад, доведеться почекати, поки завантажиться і виповниться код JavaScript- бібліотеки перед тим, як хоч якийсь з скриптів, який нею управляється, почне завантажуватися. Крім того, як ми збираємося завантажувати завантажувач скрипта? Як ми будемо завантажувати скрипт, який говорить завантажувачу скриптів, що йому загрожують? Хто буде зберігати «Хранителів»? Чому я голий? Це все дуже складні питання.

DOM поспішає на допомогу

Насправді, відповідь знаходиться всередині специфікації HTML5, хоча він і захований в самому низу секції, присвяченій завантаженні скриптів.

Ось він:

Атрибут async контролює, чи буде елемент виконуватися асинхронно. Якщо на елементі встановлено прапор force-async, то при читанні атрибут async повинен повертати true. При записи прапор force-async повинен бути знятий.

А тепер, давайте переведемо це на людську мову:

[ '//Other-domain.com/1.js', '2.js'] .forEach (function (src) {var script = document .createElement ( 'script'); script.src = src; document .head .appendChild (script);});

Скрипти, які створюються і додаються до документа динамічно, асинхронні за замовчуванням, не блокують рендеринг і виконуються, як тільки завантажені. Але, це так само означає, що вони можуть виконатися в неправильному порядку. Однак, ми можемо явно вказати на них, як на НЕ-асинхронні:

[ '//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);});

Це дає нашим скриптам поведінку, якого не можна досягти тільки маніпуляціями з HTML. Скрипти явно не-асинхронні, вони додаються в чергу виконання (та сама чергу, в яку вони додаються в нашому найпершому прикладі, де був тільки HTML). Однак через те, що вони створюються динамічно, вони виконуються незалежно від парсинга документа, так що рендеринг не блокується, поки вони завантажуються (не плутайте НЕ-асинхронну завантаження скриптів з синхронним XHR, в якому немає нічого хорошого).

Наведений скрипт потрібно включати прямо в заголовку сторінки, щоб завантаження скриптів починалася як можна швидше, не заважаючи прогресивному рендерингу, і виконувалася тому порядку, який ми встановили. 2.js легко можете завантажити перед 1.js, але він не буде запущений, поки 1.js НЕ скочується і не виконається (або поки не відбудеться помилка на тому чи іншому етапі). Ура! Асинхронна завантаження і виконання по порядку!

Завантажувати скрипти таким чином можна у всіх браузерах, які підтримують атрибут async , За винятком Safari 5.0 (в 5.1 все окей). Крім того, цей метод підтримують всі версії Firefox і Opera, оскільки ті версії, які не підтримують атрибут async, все одно виконують динамічно додані скрипти в тому порядку, в якому вони додані в документ.

Це ж найшвидший спосіб завантажувати скрипти, правда? ПРАВДА?

Ну, якщо ви динамічно вирішуєте, які скрипти завантажувати, тоді так, в іншому випадку, мабуть, немає. З прикладом вище браузер повинен розпарсити і виконати скрипт для того, щоб визначити, які скрипти завантажувати. Це означає, що ваші скрипти залишаються прихованими від сканерів предзагрузкі. Браузери використовують ці сканери для того, щоб знайти на сторінці ті ресурси, які браузеру, швидше за все, скоро знадобляться. Або, знайти ресурси сторінки, поки браузер блокований завантаженням іншого ресурсу.

Можна додати видимість цих скриптів для браузера, поставивши в заголовку документа:

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

Ці директиви говорять браузеру, що сторінці потрібні 1.js і 2.js, і їх видно модулю предзагрузкі. link [rel = subresource] дуже схожий на link [rel = prefetch], але у нього трохи інша семантика . На жаль, зараз він підтримується тільки в Chrome, і вам потрібно двічі декларувати, які скрипти завантажувати: один раз в посиланнях, один раз у вашому скрипті.

У мене розвивається депресія від цієї статті.

Розумію, дійсно, все дуже погано, і те, що у вас депресія, це нормально. Поки немає способу, без повторів коду, декларувати швидку і асинхронну завантаження скриптів, контролюючи при цьому порядок їх виконання.

З HTTP2 / SPDY можна зменшити надмірність запиту до такої міри, що найшвидшим способом завантажити JavaScript буде передача скриптів окремими маленькими файлами (кожен з яких може індивідуально кешуватися).

Уявіть таку ситуацію:

<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>

Кожен скрипт поліпшення працює з конкретним компонентом на сторінці, але йому потрібні сервісні функції з dependencies.js. В ідеалі, ми все завантажили асинхронно, потім, як можна швидше завантажили скрипти enhancement, в будь-якому порядку, але після dependencies.js. Таке ось прогресивне поліпшення!

На жаль, ніякого декларативного способу домогтися такої ситуації немає, якщо тільки самі скрипти не візьмуться відстежувати стан завантаження dependencies.js. Навіть оголошення async = false не вирішує цю проблему, оскільки виконання enhancement-10.js буде блокуватися, поки не виконаються скрипти з 1 по 9. Існує тільки один браузер, в якому це можливо зробити без хаков.

У IE є ідея!

IE завантажує скрипти не так, як інші браузери.

var script = document .createElement ( 'script'); script.src = 'whatever.js';

IE починає завантажувати whatever.js відразу, в той час як інші браузери не починають завантажувати файл, поки скрипт не додано до домену. У IE також є подія readystatechange і властивість readystate, які дозволяють нам відстежити процес завантаження. Насправді, це досить корисно, оскільки дозволяє нам незалежно контролювати завантаження і виконання скриптів.

var script = document .createElement ( 'script'); script.onreadystatechange = function () {if (script.readyState == 'loaded') {document .body.appendChild (script); }}; script.src = 'whatever.js';

Вибираючи, коли додати скрипти до документа, можна будувати досить складні моделі залежності. IE підтримує цю модель починаючи з 6 версії. Досить цікаво, але тут є така ж проблема, що і з async = false - наші скрипти невидно для модуля предзагрузкі.

Коротше! Скажіть вже, як мені завантажувати скрипти!

Добре Добре. Отже, ви хочете завантажувати скрипти так, щоб вони не блокували рендеринг, не змушували б вас писати повторюється код, і мати відмінну кросбраузерності підтримку? Ось що я можу запропонувати:

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

Так, саме так. І в кінці елемента body. Розумієте, буття веб-розробника чимось схоже на буття Сізіфа (бум! +100 очок до хіпстерства за посилання на давньогрецьку міфологію). Обмеження в HTML і браузерах не дають нам зробити щось, що буде значно краще.

Я дуже сподіваюся, що JavaScript-модулі врятують нас, надавши декларативний і неблокірующій спосіб завантажувати скрипти зі збереженням контролю над порядком їх виконання, хоча, для цього доведеться писати скрипти як модулі.

Ммм, тобто нічого кращого зараз немає, так?

Ну насправді, якщо ви хочете заробити додатковий плюсик в карму, агресивно поліпшивши продуктивність сторінки, і деяка складність і повторюваність вас не бентежать, то можна поєднати дещо з прийомів вище.

По-перше, ми додаємо визначення subresource, для тих браузерів, які займаються предзагрузкі:

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

Після цього відразу в заголовку документа ми завантажуємо наші скрипти через JavaScript за допомогою async = false, з фолбеком на завантаження скриптів через IE-подія readystate, ну, і про всяк випадок, якщо це не спрацює, то з фолбеком на defer.

var scripts = [ '1.js', '2.js']; var src; var script; var pendingScripts = []; var firstScript = document .scripts [0]; function stateChange () {var pendingScript; while (pendingScripts [0] && pendingScripts [0] .readyState == 'loaded') {pendingScript = pendingScripts.shift (); pendingScript.onreadystatechange = null; firstScript.parentNode.insertBefore (pendingScript, firstScript); }} While (src = scripts.shift ()) {if ( 'async' in firstScript) {script = document .createElement ( 'script'); script.async = false; script.src = src; document .head.appendChild (script); } Else if (firstScript.readyState) {script = document .createElement ( 'script'); pendingScripts.push (script); script.onreadystatechange = stateChange; script.src = src; } Else {document .write ( '<script src = "' + src + '" defer> </' + 'script>'); }}

Пара оптимізацій, мініфікація і, вуаля: 362 байти + посилання на ваші скрипти:

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

Чи варто все це зайвих байтів в порівнянні зі звичайним включенням скрипта? Якщо ви вже використовуєте JavaScript для умовної завантаження скриптів ( як BBC ), То почати їх завантаження раніше виглядає цілком виправданим. В інших випадках, швидше за все - ні, просто пишіть скрипти в кінець body.

Уууф, тепер я розумію, чому секція специфікації WHATWG по завантаженню скриптів була такою величезною. Піду накачени стопарик.

Швидка довідка

Прості елементи script

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

Специфікація: Скачивать разом, виконувати по порядку після завантаження CSS, блокувати рендеринг до завершення.

Браузери: Так, сер!

Defer

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

Специфікація: Скачивать разом, виконувати по порядку безпосередньо перед подією DOMContentLoaded. Ігнорувати defer для скриптів, у яких немає src.

IE <10: Ну, може бути, я виконаю 2.js десь посередині виконання 1.js. Це ж смішно, погодьтеся?

Браузери в червоному : Поняття не маю, що це ще за defer такий, буду виконувати скрипти так, як якщо б його не було.

Інші браузери: Окей, але взагалі-то, я можу і не ігнорувати defer на скриптах, у яких немає src.

Async

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

Специфікація: Скачивать разом, виконувати в тому порядку, в якому вони будуть завантажені.

Браузери в червоному : Що це за async? Буду завантажувати скрипти, як якщо б його не було.

Решта браузери: Оу, ну окей.

Async false

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

Специфікація: Скачивать разом, виконувати по порядку тоді, коли все буде завантажено.

Firefox <3.6, Opera: Поняття не маю, що це за async, але так вже вийшло, що я виконую скрипти, які додаються через JS, в тому порядку, в якому вони були додані.

Safari 5.0: Так, я начебто знаю, що таке async, але не розумію, як можна встановлювати його значення в false через JS. Знаєте що? Я виконаю ваші скрипти в такому порядку, в якому вони завантажаться, а там вже як піде.

IE <10: Нічого не знаю про async, але мене можна переконати за допомогою onreadystatechange.

Решта браузери в червоному : Поняття не маю, що це за async. Буду виконувати ваші скрипти в міру їх надходження, в тому порядку, в якому вони завантажаться.

Всі інші: Я твій друг, зроблю все за інструкцією!

ПРАВДА?
Ммм, тобто нічого кращого зараз немає, так?
Крім того, як ми збираємося завантажувати завантажувач скрипта?
Як ми будемо завантажувати скрипт, який говорить завантажувачу скриптів, що йому загрожують?
Хто буде зберігати «Хранителів»?
Чому я голий?
Це ж найшвидший спосіб завантажувати скрипти, правда?
ПРАВДА?
Отже, ви хочете завантажувати скрипти так, щоб вони не блокували рендеринг, не змушували б вас писати повторюється код, і мати відмінну кросбраузерності підтримку?
Ммм, тобто нічого кращого зараз немає, так?


Новости
  • Виртуальный хостинг

    Виртуальный хостинг. Возможности сервера распределяются в равной мере между всеми... 
    Читать полностью

  • Редизайн сайта

    Редизайн сайта – это полное либо частичное обновление дизайна существующего сайта.... 
    Читать полностью

  • Консалтинг, услуги контент-менеджера

    Сопровождение любых интернет ресурсов;- Знание HTML и CSS- Поиск и обновление контента;-... 
    Читать полностью

  • Трафик из соцсетей

    Сравнительно дешевый способ по сравнению с поисковым и контекстным видами раскрутки... 
    Читать полностью

  • Поисковая оптимизация

    Поисковая оптимизация (англ. search engine optimization, SEO) — поднятие позиций сайта в результатах... 
    Читать полностью