Олег Марков
Тег 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 (в начале, середине, конце);
- внутри некоторых других элементов (но это редко нужно и может быть плохой практикой).
На практике чаще всего используют два варианта:
- В head с атрибутами defer или async.
- Перед закрывающим тегом body, чтобы не блокировать отображение контента.
Об этом мы подробно поговорим дальше, когда будем разбирать порядок загрузки.
Варианты использования: встроенный и внешний скрипт
У тега script есть два основных способа работы:
- Встроенный (inline) код.
- Подключение внешнего файла.
Встроенный 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 важен в двух случаях:
- Поддержка модулей (ES Modules):
<script type="module" src="main.js"></script>Здесь мы явно говорим браузеру, что файл — модуль, и он должен:
- поддерживать import / export;
- загружать зависимости;
- выполнять код в отдельной области видимости.
- Нестандартный тип, который браузер не должен выполнять (часто для шаблонов):
<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>Поведение:
- Браузер парсит HTML.
- Доходит до тега script.
- Останавливает парсинг.
- Загружает script.js.
- Выполняет его.
- Продолжает парсинг документа.
Если таких скриптов несколько, каждый из них блокирует разбор 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 до уровня Middle — бесплатно!
Html — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Все гайды по Html
Лучшие курсы по теме

HTML и CSS
Антон Ларичев
TypeScript с нуля
Антон Ларичев