иконка discount

Скидка 15% по промокоду

кибер понедельник до 01.12иконка discount
CYBER2025
логотип PurpleSchool
логотип PurpleSchool

Тег script в HTML - как правильно подключать и исполнять JavaScript

Автор

Олег Марков

Введение

Тег script в HTML отвечает за подключение и выполнение JavaScript в браузере. Через него вы добавляете интерактивность, обработку событий, работу с DOM, отправку запросов и многое другое. Без тега script современный веб почти невозможен.

Смотрите, я покажу вам, как устроен этот тег, какие у него есть режимы работы и атрибуты, как правильно подключать внешние файлы и писать встроенный код, а также как избежать типичных ошибок с блокировкой загрузки страницы и конфликтами скриптов.

Наша цель — чтобы вы после прочтения могли уверенно использовать тег script в типичных сценариях разработки и понимали, что именно происходит «под капотом» во время загрузки и выполнения кода.

Что такое тег script и где его можно размещать

Тег script — это элемент HTML, который указывает браузеру, что внутри него или по ссылке в атрибуте src находится исполняемый код (чаще всего JavaScript).

Базовый пример:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <!-- Встроенный скрипт в шапке документа -->
  <script>
    // Этот код выполнится при загрузке страницы
    console.log('Страница загружается');
  </script>
</head>
<body>
  <h1>Пример использования тега script</h1>

  <!-- Подключение внешнего файла со скриптом -->
  <script src="script.js"></script>
</body>
</html>

Комментарии в коде показывают, в каких местах расположен скрипт и что он делает.

Где можно размещать script

Тег script можно разместить почти в любом месте HTML-документа:

  • в head;
  • в body (в начале, середине, конце);
  • внутри некоторых других элементов (но это редко нужно и может быть плохой практикой).

На практике чаще всего используют два варианта:

  1. В head с атрибутами defer или async.
  2. Перед закрывающим тегом body, чтобы не блокировать отображение контента.

Об этом мы подробно поговорим дальше, когда будем разбирать порядок загрузки.

Варианты использования: встроенный и внешний скрипт

У тега script есть два основных способа работы:

  1. Встроенный (inline) код.
  2. Подключение внешнего файла.

Встроенный JavaScript

Встроенный код пишется между открывающим и закрывающим тегом script.

<script>
  // Объявляем переменную
  const userName = 'Иван';

  // Выводим сообщение в консоль
  console.log('Пользователь зашел на страницу -', userName);

  // Находим элемент по идентификатору и меняем текст
  // Важно - этот код сработает только после того, как элемент уже есть в DOM
  document.getElementById('welcome').textContent = 'Здравствуйте, ' + userName;
</script>

<p id="welcome">Загрузка...</p>

Обратите внимание, как этот фрагмент кода меняет содержимое элемента с id welcome. Для этого элемент должен быть в документе на момент выполнения скрипта. Если разместите script выше этого абзаца, вы получите ошибку, потому что document.getElementById не найдет элемент.

Встраивание удобно для небольших фрагментов кода, когда:

  • нужно добавить пару строк логики;
  • нет смысла создавать отдельный файл;
  • скрипт уникален для этой страницы.

Но у такого подхода есть минусы:

  • код нельзя кэшировать отдельно;
  • сложнее поддерживать и переиспользовать;
  • смешиваются структура (HTML) и логика (JS).

Внешний JavaScript-файл

Чаще всего скрипты подключают как внешние файлы через атрибут src:

<script src="scripts/main.js"></script>

Внешний файл:

// Файл scripts/main.js

// Инициализируем обработчик при загрузке DOM
document.addEventListener('DOMContentLoaded', function () {
  // Находим кнопку по селектору
  const button = document.querySelector('#send');

  // Вешаем обработчик клика
  button.addEventListener('click', function () {
    // Показываем сообщение пользователю
    alert('Форма отправлена');
  });
});

Комментарии показывают, когда выполняется код и что делает каждая строка.

Преимущества внешних файлов:

  • кэширование в браузере;
  • один файл можно подключать на нескольких страницах;
  • код проще организовывать по модулям;
  • HTML остается чище.

Важный момент — тег script не должен одновременно содержать src и код внутри. Браузеры будут игнорировать содержимое между тегами, если указан src.

<!-- Такой код нежелателен - внутренний скрипт будет проигнорирован -->
<script src="main.js">
  console.log('Этот код не выполнится'); // Комментарий к нерабочему примеру
</script>

Основные атрибуты тега script

Теперь давайте подробно разберем атрибуты, которые чаще всего используются с тегом script.

Атрибут src

src указывает путь к внешнему JavaScript-файлу:

<!-- Подключение локального файла -->
<script src="/js/app.js"></script>

<!-- Подключение с CDN -->
<script src="https://cdn.example.com/lib.min.js"></script>
  • Если путь начинается с http или https — скрипт берется с удаленного сервера.
  • Если путь начинается со слэша — считаем от корня сайта.
  • Относительные пути считаются относительно текущей страницы.

Атрибут type

Исторически type указывал MIME-тип:

<script type="text/javascript">
  // Старый, но все еще валидный синтаксис
</script>

Сейчас для классического JavaScript можно вообще не писать type — браузеры по умолчанию считают, что это обычный JS.

Но type важен в двух случаях:

  1. Поддержка модулей (ES Modules):
<script type="module" src="main.js"></script>

Здесь мы явно говорим браузеру, что файл — модуль, и он должен:

  • поддерживать import / export;
  • загружать зависимости;
  • выполнять код в отдельной области видимости.
  1. Нестандартный тип, который браузер не должен выполнять (часто для шаблонов):
<script type="text/template" id="user-template">
  <!-- Здесь может быть HTML-шаблон -->
  <div class="user">
    <span class="name"></span>
  </div>
</script>

В этом случае вы дальше можете через JavaScript получить содержимое этого скрипта и использовать как шаблон. Браузер его не исполнит, потому что тип ему неизвестен.

Атрибут async

async управляет тем, как загружается и выполняется внешний скрипт.

Когда вы пишете:

<script src="script.js" async></script>

Браузер делает следующее:

  • начинает загружать скрипт параллельно с парсингом HTML;
  • как только файлы загружены, останавливает парсинг документа и выполняет скрипт;
  • после выполнения продолжает парсинг.

Важные особенности:

  • Порядок выполнения async-скриптов может отличаться от порядка в HTML — они выполняются по мере загрузки.
  • async имеет смысл только для внешних скриптов с src.
  • Такой режим подходит для независимых аналитик, виджетов, рекламы, которые не зависят от остального кода.

Смотрите, пример:

<!-- Этот скрипт может выполниться позже, чем второй -->
<script src="a.js" async></script>

<!-- Этот тоже async и может выполниться раньше первого -->
<script src="b.js" async></script>

Если a.js зависит от b.js, async использовать нельзя — порядок работы будет непредсказуемым.

Атрибут defer

defer тоже относится к внешним скриптам и говорит браузеру:

<script src="script.js" defer></script>
  • загружай файл параллельно с HTML;
  • не останавливай парсинг документа;
  • выполни скрипт только после того, как HTML полностью разобран;
  • выполняй такие скрипты в том порядке, в каком они указаны в документе.

Это очень удобный режим, когда вам нужно гарантировать, что DOM уже есть, а скрипты не должны блокировать отображение страницы.

Пример:

<head>
  <script src="lib.js" defer></script>
  <script src="main.js" defer></script>
</head>

Здесь lib.js точно выполнится перед main.js, даже если загрузится позже, потому что браузер учитывает порядок в документе. В то же время контент страницы будет отображаться без задержек.

Атрибут crossorigin

crossorigin используется при загрузке скриптов с другого домена, когда вам важна корректная работа CORS и сохранение информации об ошибках в консоли.

Основные значения:

  • anonymous — без передачи куки и других учетных данных;
  • use-credentials — с передачей куки.

Пример:

<script
  src="https://cdn.example.com/app.js"
  crossorigin="anonymous">
</script>

Комментарии здесь не требуются, так как атрибут самодостаточен, но вы можете помнить, что без него некоторые сообщения об ошибках могут быть скрыты.

Атрибут integrity (Subresource Integrity)

integrity позволяет браузеру проверить, что загруженный файл не был подменен. Чаще всего используется с CDN.

<script
  src="https://cdn.example.com/library.min.js"
  integrity="sha384-oFgjf9L3..."
  crossorigin="anonymous">
</script>
  • Значение integrity — это хеш содержимого файла.
  • Если файл на сервере изменится, браузер откажется его выполнять.

Это повышает безопасность, особенно при использовании сторонних библиотек.

Атрибут nomodule

nomodule используется для поддержки старых браузеров, которые не понимают type="module".

Давайте разберемся на примере:

<!-- Модульный скрипт для современных браузеров -->
<script type="module" src="main.esm.js"></script>

<!-- Резервный скрипт для старых браузеров -->
<script nomodule src="main.legacy.js"></script>
  • Современные браузеры выполнят первый скрипт и проигнорируют второй.
  • Старые браузеры не поймут type="module", проигнорируют его и выполнят скрипт с nomodule.

Так можно без сложной логики разделить код для разных поколений браузеров.

Порядок загрузки и выполнения скриптов

Теперь самое важное для практики — как браузер загружает и выполняет скрипты в разных режимах.

Классический script без async и defer

<script src="script.js"></script>

Поведение:

  1. Браузер парсит HTML.
  2. Доходит до тега script.
  3. Останавливает парсинг.
  4. Загружает script.js.
  5. Выполняет его.
  6. Продолжает парсинг документа.

Если таких скриптов несколько, каждый из них блокирует разбор HTML. Поэтому размещение тяжелых скриптов вверху страницы без defer и async может заметно замедлять отображение.

async против defer: наглядное сравнение

Давайте посмотрим на три разных варианта:

<!-- 1. Обычный скрипт -->
<script src="a.js"></script>

<!-- 2. Скрипт с async -->
<script src="b.js" async></script>

<!-- 3. Скрипт с defer -->
<script src="c.js" defer></script>

Условно:

  • a.js: блокирует разбор, выполняется сразу по загрузке.
  • b.js: загружается параллельно, выполняется по готовности, порядок относительно других async-скриптов не гарантирован.
  • c.js: загружается параллельно, выполняется только когда HTML разобран полностью, соблюдается порядок нескольких defer-скриптов.

На практике:

  • Для критически важного кода, который должен выполниться строго по порядку и может зависеть от предыдущих скриптов, лучше использовать defer.
  • Для независимого кода, не зависящего от DOM и других скриптов, можно использовать async.

Влияние на DOM и события загрузки

Если вы пишете обработчики, связанные с загрузкой документа, важно понимать различия:

  • Классические скрипты внизу body часто уже имеют доступ ко всем элементам.
  • Скрипты с defer выполняются после того, как DOM готов, до события DOMContentLoaded.
  • Модульные скрипты (type="module") ведут себя ближе к defer — они тоже ждут разбор DOM.

Например:

<script defer src="main.js"></script>
// Файл main.js

// Этот код может сразу безопасно работать с DOM
const title = document.querySelector('h1');

// Проверяем, что элемент найден
if (title) {
  // Меняем текст заголовка
  title.textContent = 'Страница загружена';
}

// Можем также подписаться на события, если нужно
window.addEventListener('load', function () {
  // Код здесь выполнится после полной загрузки всех ресурсов
  console.log('Страница полностью загружена');
});

Комментарии в примере показывают, что с defer DOM уже доступен без дополнительных ожиданий.

Модульный JavaScript и script type="module"

Современный JavaScript активно использует модули. Браузеры поддерживают их напрямую через type="module".

Основные особенности script type="module"

<script type="module" src="main.js"></script>

В этом режиме:

  • поддерживаются import и export;
  • код выполняется в строгом режиме;
  • каждая модульная область имеет собственную область видимости;
  • один и тот же модуль грузится и выполняется только один раз (кешируется).

Например, у вас есть модуль utils.js:

// Файл utils.js

// Экспортируем функцию
export function sum(a, b) {
  // Складываем два числа
  return a + b;
}

И главный модуль main.js:

// Файл main.js

// Импортируем функцию sum из utils.js
import { sum } from './utils.js';

// Вычисляем сумму и выводим результат
const result = sum(2, 3);
console.log('Результат сложения -', result);

HTML:

<script type="module" src="main.js"></script>

Теперь вы увидите в консоли результат работы функции sum. Комментарии в коде помогают понять, как именно осуществляется импорт и экспорт.

Модульные скрипты часто рассматриваются как замена громоздким сборщикам на маленьких проектах.

Динамический импорт в модулях

Еще одна полезная возможность — динамический импорт:

// Файл main.js

// Функция, которая при необходимости подгружает модуль
async function loadHeavyModule() {
  // Динамически импортируем модуль
  const module = await import('./heavy.js');

  // Вызываем функцию из загруженного модуля
  module.runHeavyTask();
}

// Запускаем загрузку по событию, например по клику
document.getElementById('start').addEventListener('click', loadHeavyModule);

Так вы можете отложить загрузку тяжелых частей приложения до момента, когда они реально понадобятся.

Взаимодействие с DOM: когда и как лучше выполнять код

Тег script тесно связан с DOM, потому что чаще всего скрипт нужен именно для работы с разметкой. Важно понимать, когда DOM доступен.

DOMContentLoaded и load

Давайте посмотрим на два ключевых события:

  • DOMContentLoaded — HTML разобран, дерево DOM готово, стили могут еще догружаться.
  • load — загружены все ресурсы (картинки, стили, фреймы).

Пример:

<script>
  // Подписываемся на событие готовности DOM
  document.addEventListener('DOMContentLoaded', function () {
    // Этот код выполнится, когда DOM уже доступен
    const elem = document.querySelector('#info');
    if (elem) {
      elem.textContent = 'DOM готов';
    }
  });

  // Подписываемся на полную загрузку страницы
  window.addEventListener('load', function () {
    // Этот код выполнится после загрузки всех ресурсов
    console.log('Все ресурсы загружены');
  });
</script>

<div id="info">Ожидание...</div>

Комментарии в этом примере показывают, что именно можно делать на разных этапах загрузки.

Скрипты внизу body

Классический подход — ставить script в конце body:

<body>
  <!-- Весь HTML-контент страницы -->

  <!-- Скрипт, который выполнится, когда DOM уже сформирован -->
  <script src="main.js"></script>
</body>

В файле main.js вы можете сразу работать с элементами, так как к моменту его выполнения браузер уже построил DOM.

Однако в современных проектах чаще используют:

  • скрипты в head с defer;
  • или модульные скрипты.

Оба варианта упрощают структуру и позволяют лучше управлять зависимостями.

Обеспечение производительности при работе со script

Когда на странице много скриптов, важно не «заблокировать» пользователю отображение контента. Давайте разберем основные практики.

Минимизация блокирующих скриптов

Правило: по возможности не используйте синхронные script в head без defer или async.

Плохой пример:

<head>
  <!-- Этот скрипт блокирует отрисовку -->
  <script src="heavy.js"></script>
</head>

Лучше:

<head>
  <!-- Скрипт загружается параллельно и не блокирует HTML -->
  <script src="heavy.js" defer></script>
</head>

Разделение кода

Часто логика приложения делится:

  • «критический» код, который нужен сразу;
  • второстепенный код, который можно загрузить позже.

Для второстепенного кода хорошо подходят:

  • динамический импорт в модулях;
  • lazy loading по событиям (как мы смотрели выше).

Кэширование внешних файлов

Если вы подключаете общий app.js на всех страницах, браузер кэширует его. Поэтому вынесение кода во внешние файлы почти всегда выгодно.

Серверная настройка заголовков Cache-Control и ETag помогает максимально использовать этот кэш, но на стороне HTML вам важно просто подключать один и тот же файл, а не генерировать уникальное имя при каждом запросе без необходимости.

Безопасность при использовании тега script

Тег script — основной носитель потенциальных XSS-уязвимостей (межсайтовых скриптовых атак). Давайте кратко разберем ключевые моменты.

Никогда не вставляйте непроверенные данные в script

Опасный пример:

<script>
  // userName берется напрямую из ввода пользователя на сервере
  var userName = "{{userInput}}"; // Комментарий - так можно внедрить произвольный код
</script>

Если userInput содержит вредоносный фрагмент, он окажется внутри JS-кода и выполнится.

Нужно:

  • экранировать специальные символы;
  • по возможности не генерировать код на основе пользовательских данных;
  • использовать безопасные способы передачи данных, например через data-атрибуты или JSON, который корректно сериализован.

Content Security Policy и запрет inline-скриптов

В современных проектах все чаще включают Content Security Policy (CSP) и запрещают встроенные скрипты (без хеша или nonce). Это означает:

  • нельзя писать код напрямую в script без специальных меток;
  • нельзя использовать обработчики событий прямо в атрибутах (например onclick).

Вместо:

<button onclick="alert('Нажато')">Нажми</button>

Лучше:

<button id="alert-btn">Нажми</button>

<script src="main.js" defer></script>
// Файл main.js

// Находим кнопку по идентификатору
const button = document.getElementById('alert-btn');

if (button) {
  // Вешаем обработчик события через JS
  button.addEventListener('click', function () {
    alert('Нажато');
  });
}

Комментарии в этом коде показывают безопасный способ привязки логики.

Опасность eval и похожих конструкций

Функции, которые выполняют строки кода (eval, new Function и некоторые другие), также увеличивают риск XSS.

Лучше избегать их использования и работать с данными как с данными, а не как с кодом.

Типичные ошибки при работе с тегом script

Сейчас давайте посмотрим на набор проблем, с которыми часто сталкиваются начинающие разработчики.

Неправильный порядок загрузки зависимых скриптов

Пример:

<script src="jquery.plugin.js"></script>
<script src="jquery.js"></script>

Плагин зависит от jQuery, но подключен раньше. В результате в консоли:

  • ошибка вида jQuery is not defined.

Решение:

<script src="jquery.js"></script>
<script src="jquery.plugin.js"></script>

Или использовать сборку / модули, где зависимости описываются явно через import.

Невозможность найти элементы DOM

Ошибка вида Cannot read properties of null чаще всего возникает, когда:

  • вы пытаетесь получить элемент до того, как он появился в DOM;
  • селектор написан неправильно.

Например:

<script>
  const elem = document.getElementById('content');
  elem.textContent = 'Готово'; // elem может быть null, если элемента еще нет
</script>

<div id="content"></div>

Решение:

  • перенести script вниз;
  • использовать defer;
  • или подписаться на DOMContentLoaded.

Смешивание inline-кода и src

Как мы уже смотрели:

<script src="main.js">
  console.log('Этот код будет проигнорирован');
</script>

Весь внутренний код игнорируется, когда указан src. Это частая причина «пропавших» логов.

Нужно:

  • либо оставить только src;
  • либо убрать src и оставить код внутри.

Использование устаревшего атрибута language

Иногда можно встретить:

<script language="javascript">
  // Старый атрибут language уже давно не нужен
</script>

Сейчас этот атрибут не используется, его следует удалить. Для указания типа используется type, а в большинстве случаев он вообще не нужен.

Заключение

Тег script — это точка входа JavaScript в HTML-документ. От того, как вы его используете, зависит:

  • скорость загрузки страницы;
  • корректность работы скриптов;
  • отсутствие ошибок с порядком выполнения и доступом к DOM;
  • безопасность приложения.

Вы рассмотрели:

  • два способа использования — встроенный код и внешние файлы;
  • ключевые атрибуты — src, type, async, defer, crossorigin, integrity, nomodule;
  • различия в порядке загрузки и выполнения;
  • особенности модульных скриптов;
  • влияние скриптов на DOM и жизненный цикл страницы;
  • основные практики по производительности и безопасности;
  • типичные ошибки и способы их устранения.

Теперь у вас есть цельная картина, как именно работает тег script и какие инструменты он предоставляет для управления поведением JavaScript в браузере.

Частозадаваемые технические вопросы по теме статьи и ответы на них

Можно ли использовать одновременно async и defer у одного тега script

Нет, браузер будет рассматривать только один из них. По спецификации, если указаны оба, поведение совпадает с async. Поэтому выбирайте только тот режим, который вам нужен — обычно либо async, либо defer.

Как подгрузить скрипт динамически после загрузки страницы

Вы можете создать элемент script через JavaScript и добавить его в DOM:

// Создаем новый тег script
const script = document.createElement('script');

// Указываем источник
script.src = 'extra.js';

// При необходимости указываем async
script.async = true;

// Добавляем на страницу, после чего скрипт начнет загружаться
document.body.appendChild(script);

Такой подход удобен, если нужно загружать код только по требованию.

Как запретить выполнение inline-скриптов, но оставить внешние файлы

Используйте заголовок Content-Security-Policy на сервере. Простейший вариант:

Content-Security-Policy: script-src 'self'
  • 'self' разрешает запускать скрипты только из того же домена.
  • Отсутствие 'unsafe-inline' запрещает inline-скрипты и обработчики в атрибутах. После этого весь JavaScript нужно вынести во внешние файлы и подключать через src.

Как правильно подключать несколько модульных скриптов, если у них общие зависимости

Обычно вы подключаете только один «входной» модуль:

<script type="module" src="main.js"></script>

А внутри main.js импортируете все остальные:

import './init-ui.js';
import './analytics.js';
import './forms.js';

Браузер сам подгрузит зависимости, закеширует модули и выполнит их один раз. Не нужно подключать каждый модуль отдельным тегом.

Как работать с legacy-браузерами, которые не поддерживают type="module"

Используйте связку module / nomodule:

<!-- Современный код -->
<script type="module" src="app.esm.js"></script>

<!-- Транспилированный код для старых браузеров -->
<script nomodule src="app.legacy.js"></script>
  • Современные браузеры выполнят только модульный скрипт.
  • Старые проигнорируют type="module" и выполнят скрипт с nomodule. Для этого обычно используют сборщики (Babel, Webpack, Vite) с двумя целевыми сборками.
Стрелочка влевоСлоты компонентов в HTML slot - полное руководствоТег noscript в HTML - как и когда его использоватьСтрелочка вправо

Постройте личный план изучения Html до уровня Middle — бесплатно!

Html — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Все гайды по Html

Тег section в HTML - семантическая разметка структуры страницыТег nav в HTML - полное руководство по семантической навигацииТег main в HTML - подробное руководство по использованиюТег header в HTML - полное практическое руководствоТег footer в HTML - назначение семантика и практические примерыТег figure в HTML - как правильно оформлять иллюстрации и подписиТег figcaption в HTML - подробное руководство с примерамиТег aside в HTML - назначение правильная семантика и примеры
Текстовая область HTML textarea - практическое руководствоВыпадающий список HTML select - полное руководство для разработчиковОпция списка HTML option - как работает и как правильно использоватьАтрибут method в HTML - как правильно отправлять данные формыЗаголовок группы HTML legend - как правильно использовать и оформлятьТег input в HTML - типы атрибуты валидация и примерыТег формы form в HTMLГруппа полей HTML fieldsetАтрибут action в HTML - как правильно задавать адрес отправки формы
Открыть базу знаний

Лучшие курсы по теме

изображение курса

HTML и CSS

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.9
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий