← JavaScript/async и defer скрипты#153 из 383← ПредыдущийСледующий →+20 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

async и defer скрипты

Представь: твой лендинг грузится 4 секунды — пользователь видит белый экран. Причина: скрипт аналитики в <head> без атрибутов блокирует весь HTML. Два атрибута — async и defer — полностью меняют поведение загрузки скриптов и напрямую влияют на Core Web Vitals и конверсию.

Что решает этот механизм

По умолчанию <script> в <head> останавливает парсинг HTML до полной загрузки и выполнения скрипта. На медленном 3G это может добавить 2-3 секунды к First Contentful Paint. defer и async позволяют загружать скрипты параллельно с парсингом HTML.

На основе предыдущих уроков

  • жизненный цикл страницы — defer влияет на момент DOMContentLoaded, async — нет
  • Promise, async/await — принцип параллельной загрузки аналогичен Promise.all
  • Обычный скрипт (блокирующий)

    <!-- Браузер СТОПОРИТ парсинг HTML, скачивает, выполняет, потом продолжает -->
    <head>
      <script src="heavy-library.js"></script>  <!-- блокирует! -->
    </head>
    <body>
      <!-- Эта часть не начнёт рендериться до завершения скрипта -->
    </body>

    Диаграмма обычного скрипта:

    HTML: ████████ СТОП ████████
    скрипт:         ░░░░ ████
                    ^load ^exec

    defer — загрузка параллельно, выполнение после HTML

    <script defer src="app.js"></script>
    <script defer src="utils.js"></script>
  • Скрипт скачивается параллельно с парсингом HTML
  • Выполняется после полного парсинга HTML
  • Порядок выполнения сохраняется: utils.js выполнится после app.js
  • DOMContentLoaded ждёт выполнения всех defer-скриптов
  • Диаграмма defer:

    HTML: ████████████████████ DOMContentLoaded
    скрипт:  ░░░░░░░░░░░       ████
             ^начало загрузки   ^выполнение после HTML

    async — загрузка параллельно, выполнение сразу

    <script async src="analytics.js"></script>
    <script async src="ads.js"></script>
  • Скрипт скачивается параллельно с парсингом HTML
  • Выполняется сразу как загрузился, прерывая парсинг HTML
  • Порядок НЕ гарантирован: какой загрузился первым, тот и выполнится
  • DOMContentLoaded НЕ ждёт async-скриптов
  • Диаграмма async:

    HTML: ████████ СТОП ████████████████
    скрипт:  ░░░░░ ████
                   ^загрузился — СРАЗУ выполнился

    Сравнение

    | | Обычный | defer | async |

    |---|---|---|---|

    | Блокирует HTML | Да | Нет | Частично |

    | Параллельная загрузка | Нет | Да | Да |

    | Порядок выполнения | Порядок в HTML | Порядок в HTML | Первый загрузился — первый выполнился |

    | Ждёт DOM | Не нужно | Да | Нет |

    | DOMContentLoaded | До него | После него | Независимо |

    Когда что использовать

    <!-- Сторонняя аналитика, метрики, рекламные скрипты -->
    <!-- Не зависят от DOM, не зависят друг от друга -->
    <script async src="https://analytics.example.com/tracker.js"></script>
    
    <!-- Основной код приложения, библиотеки -->
    <!-- Зависят от DOM, порядок важен -->
    <script defer src="vendor/jquery.js"></script>
    <script defer src="app.js"></script>  <!-- выполнится ПОСЛЕ jquery.js -->
    
    <!-- Критичный инлайновый код -->
    <script>
      // Инлайн-скрипты игнорируют async/defer
      const config = { theme: 'dark' }
    </script>

    DOMContentLoaded и load

    // DOMContentLoaded — DOM готов, картинки ещё грузятся
    // Срабатывает ПОСЛЕ defer-скриптов
    document.addEventListener('DOMContentLoaded', () => {
      console.log('DOM готов, можно работать с элементами')
    })
    
    // load — всё готово: DOM, картинки, стили, шрифты
    window.addEventListener('load', () => {
      console.log('Всё загружено включая картинки')
    })

    Типичные ошибки

    1. Использовать async для скриптов, которые зависят друг от друга

    <!-- ПЛОХО — async не гарантирует порядок! -->
    <script async src="jquery.js"></script>
    <script async src="app.js"></script>
    <!-- app.js может выполниться ДО jquery.js — TypeError: $ is not defined -->
    
    <!-- ХОРОШО — defer сохраняет порядок -->
    <script defer src="jquery.js"></script>
    <script defer src="app.js"></script>
    <!-- jquery.js всегда выполнится первым -->

    2. Добавлять async/defer к инлайновым скриптам — атрибуты игнорируются

    <!-- ПЛОХО — async/defer не работают для инлайн-скриптов -->
    <script async>
      const config = { theme: 'dark' }  // выполняется синхронно несмотря на async
    </script>
    
    <!-- async/defer работают ТОЛЬКО для внешних файлов с src -->
    <script defer src="app.js"></script>

    3. Размещать defer-скрипты в body перед закрывающим тегом — это старый паттерн

    <!-- УСТАРЕЛО — скрипты в конце body были хаком до появления defer -->
    <body>
      ...контент...
      <script src="app.js"></script>  <!-- блокирует только конец страницы -->
    </body>
    
    <!-- СОВРЕМЕННЫЙ ПОДХОД — defer в head, HTML парсится полностью параллельно -->
    <head>
      <script defer src="app.js"></script>
    </head>

    В реальных проектах

  • Google Analytics, Яндекс.Метрика: добавляются через async — независимы от DOM, не должны блокировать контент
  • React/Vue/Angular приложения: бандл main.js подключается через defer — DOM нужен, порядок важен
  • Core Web Vitals: отсутствие defer/async на тяжёлых скриптах — одна из частых причин плохого LCP и FID в Lighthouse
  • Примеры

    Симуляция порядка загрузки async vs defer через Promise и setTimeout — наглядная демонстрация разницы

    // Симуляция порядка загрузки скриптов через Promise + setTimeout
    // Числа в setTimeout имитируют время загрузки скриптов с сети
    
    function delay(ms) {
      return new Promise(resolve => setTimeout(resolve, ms))
    }
    
    // --- Симуляция обычного (блокирующего) скрипта ---
    async function simulateNormalScript() {
      const log = []
      log.push('Начало парсинга HTML...')
    
      // Блокирует парсинг
      log.push('[СТОП] Загружаем script.js (300мс)...')
      await delay(300)
      log.push('[ВЫПОЛНЕНИЕ] script.js запущен')
      log.push('Продолжаем парсинг HTML...')
      log.push('HTML полностью разобран')
      log.push('DOMContentLoaded!')
    
      return log
    }
    
    // --- Симуляция defer ---
    async function simulateDeferScripts() {
      const log = []
      log.push('Начало парсинга HTML...')
    
      // Параллельная загрузка
      const deferLoads = [
        delay(200).then(() => ({ name: 'vendor.js',  order: 1 })),
        delay(300).then(() => ({ name: 'app.js',     order: 2 })),
        delay(100).then(() => ({ name: 'utils.js',   order: 0 })),
      ]
    
      // HTML парсится одновременно
      await delay(250)
      log.push('HTML полностью разобран (defer скрипты ещё грузятся)')
    
      // Ждём ВСЕ defer-скрипты, выполняем В ПОРЯДКЕ order
      const loaded = await Promise.all(deferLoads)
      const sorted = loaded.sort((a, b) => a.order - b.order)
      sorted.forEach(s => log.push(`[ВЫПОЛНЕНИЕ defer] ${s.name}`))
      log.push('DOMContentLoaded! (после всех defer)')
    
      return log
    }
    
    // --- Симуляция async ---
    async function simulateAsyncScripts() {
      const log = []
      log.push('Начало парсинга HTML...')
    
      // Каждый загружается и выполняется как только готов (без порядка)
      const asyncScripts = [
        delay(250).then(() => 'analytics.js'),   // загружается 250мс
        delay(100).then(() => 'tracker.js'),     // загружается 100мс (первым!)
        delay(400).then(() => 'ads.js'),         // загружается 400мс (последним)
      ]
    
      // HTML парсится параллельно
      const htmlParsing = delay(350).then(() => {
        log.push('HTML полностью разобран')
        log.push('DOMContentLoaded! (async скрипты ещё могут выполняться)')
      })
    
      // Выполняем по мере загрузки (порядок НЕ гарантирован)
      asyncScripts.forEach(p => {
        p.then(name => log.push(`[ВЫПОЛНЕНИЕ async] ${name} — загрузился!`))
      })
    
      await Promise.all([...asyncScripts, htmlParsing])
      return log
    }
    
    // --- Запуск всех симуляций ---
    console.log('=== Обычный скрипт (блокирующий) ===')
    simulateNormalScript().then(log => {
      log.forEach(line => console.log(line))
    
      console.log('\n=== defer скрипты ===')
      return simulateDeferScripts()
    }).then(log => {
      log.forEach(line => console.log(line))
    
      console.log('\n=== async скрипты ===')
      return simulateAsyncScripts()
    }).then(log => {
      log.forEach(line => console.log(line))
    
      console.log('\n=== Итог: порядок выполнения ===')
      console.log('Обычный: HTML СТОП → скрипт → HTML продолжение')
      console.log('defer:   HTML + загрузка параллельно → выполнение в порядке → DOMContentLoaded')
      console.log('async:   HTML + загрузка параллельно → выполнение по готовности (порядок случайный)')
    })

    async и defer скрипты

    Представь: твой лендинг грузится 4 секунды — пользователь видит белый экран. Причина: скрипт аналитики в <head> без атрибутов блокирует весь HTML. Два атрибута — async и defer — полностью меняют поведение загрузки скриптов и напрямую влияют на Core Web Vitals и конверсию.

    Что решает этот механизм

    По умолчанию <script> в <head> останавливает парсинг HTML до полной загрузки и выполнения скрипта. На медленном 3G это может добавить 2-3 секунды к First Contentful Paint. defer и async позволяют загружать скрипты параллельно с парсингом HTML.

    На основе предыдущих уроков

  • жизненный цикл страницы — defer влияет на момент DOMContentLoaded, async — нет
  • Promise, async/await — принцип параллельной загрузки аналогичен Promise.all
  • Обычный скрипт (блокирующий)

    <!-- Браузер СТОПОРИТ парсинг HTML, скачивает, выполняет, потом продолжает -->
    <head>
      <script src="heavy-library.js"></script>  <!-- блокирует! -->
    </head>
    <body>
      <!-- Эта часть не начнёт рендериться до завершения скрипта -->
    </body>

    Диаграмма обычного скрипта:

    HTML: ████████ СТОП ████████
    скрипт:         ░░░░ ████
                    ^load ^exec

    defer — загрузка параллельно, выполнение после HTML

    <script defer src="app.js"></script>
    <script defer src="utils.js"></script>
  • Скрипт скачивается параллельно с парсингом HTML
  • Выполняется после полного парсинга HTML
  • Порядок выполнения сохраняется: utils.js выполнится после app.js
  • DOMContentLoaded ждёт выполнения всех defer-скриптов
  • Диаграмма defer:

    HTML: ████████████████████ DOMContentLoaded
    скрипт:  ░░░░░░░░░░░       ████
             ^начало загрузки   ^выполнение после HTML

    async — загрузка параллельно, выполнение сразу

    <script async src="analytics.js"></script>
    <script async src="ads.js"></script>
  • Скрипт скачивается параллельно с парсингом HTML
  • Выполняется сразу как загрузился, прерывая парсинг HTML
  • Порядок НЕ гарантирован: какой загрузился первым, тот и выполнится
  • DOMContentLoaded НЕ ждёт async-скриптов
  • Диаграмма async:

    HTML: ████████ СТОП ████████████████
    скрипт:  ░░░░░ ████
                   ^загрузился — СРАЗУ выполнился

    Сравнение

    | | Обычный | defer | async |

    |---|---|---|---|

    | Блокирует HTML | Да | Нет | Частично |

    | Параллельная загрузка | Нет | Да | Да |

    | Порядок выполнения | Порядок в HTML | Порядок в HTML | Первый загрузился — первый выполнился |

    | Ждёт DOM | Не нужно | Да | Нет |

    | DOMContentLoaded | До него | После него | Независимо |

    Когда что использовать

    <!-- Сторонняя аналитика, метрики, рекламные скрипты -->
    <!-- Не зависят от DOM, не зависят друг от друга -->
    <script async src="https://analytics.example.com/tracker.js"></script>
    
    <!-- Основной код приложения, библиотеки -->
    <!-- Зависят от DOM, порядок важен -->
    <script defer src="vendor/jquery.js"></script>
    <script defer src="app.js"></script>  <!-- выполнится ПОСЛЕ jquery.js -->
    
    <!-- Критичный инлайновый код -->
    <script>
      // Инлайн-скрипты игнорируют async/defer
      const config = { theme: 'dark' }
    </script>

    DOMContentLoaded и load

    // DOMContentLoaded — DOM готов, картинки ещё грузятся
    // Срабатывает ПОСЛЕ defer-скриптов
    document.addEventListener('DOMContentLoaded', () => {
      console.log('DOM готов, можно работать с элементами')
    })
    
    // load — всё готово: DOM, картинки, стили, шрифты
    window.addEventListener('load', () => {
      console.log('Всё загружено включая картинки')
    })

    Типичные ошибки

    1. Использовать async для скриптов, которые зависят друг от друга

    <!-- ПЛОХО — async не гарантирует порядок! -->
    <script async src="jquery.js"></script>
    <script async src="app.js"></script>
    <!-- app.js может выполниться ДО jquery.js — TypeError: $ is not defined -->
    
    <!-- ХОРОШО — defer сохраняет порядок -->
    <script defer src="jquery.js"></script>
    <script defer src="app.js"></script>
    <!-- jquery.js всегда выполнится первым -->

    2. Добавлять async/defer к инлайновым скриптам — атрибуты игнорируются

    <!-- ПЛОХО — async/defer не работают для инлайн-скриптов -->
    <script async>
      const config = { theme: 'dark' }  // выполняется синхронно несмотря на async
    </script>
    
    <!-- async/defer работают ТОЛЬКО для внешних файлов с src -->
    <script defer src="app.js"></script>

    3. Размещать defer-скрипты в body перед закрывающим тегом — это старый паттерн

    <!-- УСТАРЕЛО — скрипты в конце body были хаком до появления defer -->
    <body>
      ...контент...
      <script src="app.js"></script>  <!-- блокирует только конец страницы -->
    </body>
    
    <!-- СОВРЕМЕННЫЙ ПОДХОД — defer в head, HTML парсится полностью параллельно -->
    <head>
      <script defer src="app.js"></script>
    </head>

    В реальных проектах

  • Google Analytics, Яндекс.Метрика: добавляются через async — независимы от DOM, не должны блокировать контент
  • React/Vue/Angular приложения: бандл main.js подключается через defer — DOM нужен, порядок важен
  • Core Web Vitals: отсутствие defer/async на тяжёлых скриптах — одна из частых причин плохого LCP и FID в Lighthouse
  • Примеры

    Симуляция порядка загрузки async vs defer через Promise и setTimeout — наглядная демонстрация разницы

    // Симуляция порядка загрузки скриптов через Promise + setTimeout
    // Числа в setTimeout имитируют время загрузки скриптов с сети
    
    function delay(ms) {
      return new Promise(resolve => setTimeout(resolve, ms))
    }
    
    // --- Симуляция обычного (блокирующего) скрипта ---
    async function simulateNormalScript() {
      const log = []
      log.push('Начало парсинга HTML...')
    
      // Блокирует парсинг
      log.push('[СТОП] Загружаем script.js (300мс)...')
      await delay(300)
      log.push('[ВЫПОЛНЕНИЕ] script.js запущен')
      log.push('Продолжаем парсинг HTML...')
      log.push('HTML полностью разобран')
      log.push('DOMContentLoaded!')
    
      return log
    }
    
    // --- Симуляция defer ---
    async function simulateDeferScripts() {
      const log = []
      log.push('Начало парсинга HTML...')
    
      // Параллельная загрузка
      const deferLoads = [
        delay(200).then(() => ({ name: 'vendor.js',  order: 1 })),
        delay(300).then(() => ({ name: 'app.js',     order: 2 })),
        delay(100).then(() => ({ name: 'utils.js',   order: 0 })),
      ]
    
      // HTML парсится одновременно
      await delay(250)
      log.push('HTML полностью разобран (defer скрипты ещё грузятся)')
    
      // Ждём ВСЕ defer-скрипты, выполняем В ПОРЯДКЕ order
      const loaded = await Promise.all(deferLoads)
      const sorted = loaded.sort((a, b) => a.order - b.order)
      sorted.forEach(s => log.push(`[ВЫПОЛНЕНИЕ defer] ${s.name}`))
      log.push('DOMContentLoaded! (после всех defer)')
    
      return log
    }
    
    // --- Симуляция async ---
    async function simulateAsyncScripts() {
      const log = []
      log.push('Начало парсинга HTML...')
    
      // Каждый загружается и выполняется как только готов (без порядка)
      const asyncScripts = [
        delay(250).then(() => 'analytics.js'),   // загружается 250мс
        delay(100).then(() => 'tracker.js'),     // загружается 100мс (первым!)
        delay(400).then(() => 'ads.js'),         // загружается 400мс (последним)
      ]
    
      // HTML парсится параллельно
      const htmlParsing = delay(350).then(() => {
        log.push('HTML полностью разобран')
        log.push('DOMContentLoaded! (async скрипты ещё могут выполняться)')
      })
    
      // Выполняем по мере загрузки (порядок НЕ гарантирован)
      asyncScripts.forEach(p => {
        p.then(name => log.push(`[ВЫПОЛНЕНИЕ async] ${name} — загрузился!`))
      })
    
      await Promise.all([...asyncScripts, htmlParsing])
      return log
    }
    
    // --- Запуск всех симуляций ---
    console.log('=== Обычный скрипт (блокирующий) ===')
    simulateNormalScript().then(log => {
      log.forEach(line => console.log(line))
    
      console.log('\n=== defer скрипты ===')
      return simulateDeferScripts()
    }).then(log => {
      log.forEach(line => console.log(line))
    
      console.log('\n=== async скрипты ===')
      return simulateAsyncScripts()
    }).then(log => {
      log.forEach(line => console.log(line))
    
      console.log('\n=== Итог: порядок выполнения ===')
      console.log('Обычный: HTML СТОП → скрипт → HTML продолжение')
      console.log('defer:   HTML + загрузка параллельно → выполнение в порядке → DOMContentLoaded')
      console.log('async:   HTML + загрузка параллельно → выполнение по готовности (порядок случайный)')
    })

    Задание

    Напиши функцию simulateLoadOrder(scripts) которая принимает массив скриптов вида [{ name, loadTime, type }] где type — "normal", "defer" или "async". Функция должна вернуть Promise, который резолвится в массив строк — порядок событий (загрузка, выполнение, DOMContentLoaded). Для normal: каждый блокирует HTML. Для defer: все грузятся параллельно, выполняются в исходном порядке после HTML. Для async: каждый выполняется как только загрузился.

    Подсказка

    delay(s.loadTime) для загрузки. deferLoads — Promise.all, потом sort по idx. asyncLoads — каждый выполняется сразу в .then(). HTML_PARSE_TIME = 200мс симулирует парсинг.

    Загружаем среду выполнения...
    Загружаем AI-помощника...