JavaScript позволяет нам изменять практически каждый аспект страницы: контент, стиль и ее реакцию на взаимодействие с пользователем. Однако JavaScript также может блокировать построение DOM и задерживать отображение страницы. Чтобы обеспечить оптимальную производительность, сделайте свой JavaScript асинхронным и исключите весь ненужный JavaScript из критического пути рендеринга.
Краткое содержание
- JavaScript может запрашивать и изменять DOM и CSSOM.
- Блоки выполнения JavaScript в CSSOM.
- JavaScript блокирует построение DOM, если оно явно не объявлено как асинхронное.
JavaScript — это динамический язык, который работает в браузере и позволяет нам изменять практически каждый аспект поведения страницы: мы можем изменять контент, добавляя и удаляя элементы из дерева DOM; мы можем изменить свойства CSSOM каждого элемента; мы можем обрабатывать ввод пользователя; и многое другое. Чтобы проиллюстрировать это, давайте дополним наш предыдущий пример «Hello World» простым встроенным скриптом:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
<title>Critical Path: Script</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script>
var span = document.getElementsByTagName('span')[0];
span.textContent = 'interactive'; // change DOM text content
span.style.display = 'inline'; // change CSSOM property
// create a new element, style it, and append it to the DOM
var loadTime = document.createElement('div');
loadTime.textContent = 'You loaded this page on: ' + new Date();
loadTime.style.color = 'blue';
document.body.appendChild(loadTime);
</script>
</body>
</html>
JavaScript позволяет нам проникнуть в DOM и получить ссылку на скрытый узел диапазона; узел может быть не виден в дереве рендеринга, но он все еще присутствует в DOM. Затем, когда у нас есть ссылка, мы можем изменить ее текст (через .textContent) и даже переопределить расчетное свойство стиля отображения с «none» на «inline». Теперь на нашей странице отображается « Привет, интерактивные студенты! ».
JavaScript также позволяет нам создавать, стилизовать, добавлять и удалять новые элементы в DOM. Технически вся наша страница может представлять собой один большой файл JavaScript, который создает и стилизует элементы один за другим. Хотя это и сработает, на практике использовать HTML и CSS гораздо проще. Во второй части нашей функции JavaScript мы создаем новый элемент div, устанавливаем его текстовое содержимое, стилизуем его и добавляем к телу.
При этом мы изменили содержимое и стиль CSS существующего узла DOM и добавили в документ совершенно новый узел. Наша страница не получит никаких наград за дизайн, но она иллюстрирует мощь и гибкость, которые предоставляет нам JavaScript.
Однако, хотя JavaScript предоставляет нам много возможностей, он создает множество дополнительных ограничений на то, как и когда отображается страница.
Во-первых, обратите внимание, что в приведенном выше примере наш встроенный скрипт находится внизу страницы. Почему? Что ж, вы должны попробовать это сами, но если мы переместим скрипт выше элемента span , вы заметите, что скрипт дает сбой и жалуется, что не может найти ссылку ни на один элемент span в документе; то есть getElementsByTagName('span') возвращает значение null . Это демонстрирует важное свойство: наш скрипт выполняется именно в том месте, где он вставлен в документ. Когда анализатор HTML обнаруживает тег сценария, он приостанавливает процесс построения DOM и передает управление движку JavaScript; после завершения работы движка JavaScript браузер продолжает с того места, где остановился, и возобновляет построение DOM.
Другими словами, наш блок сценария не может найти какие-либо элементы на странице позже, потому что они еще не обработаны! Или, говоря немного иначе: выполнение нашего встроенного скрипта блокирует построение DOM, что также задерживает первоначальный рендеринг.
Еще одним тонким свойством внедрения скриптов на нашу страницу является то, что они могут читать и изменять не только свойства DOM, но и свойства CSSOM. Фактически, именно это мы и делаем в нашем примере, когда меняем свойство display элемента span с none на inline. Конечный результат? Теперь у нас есть состояние гонки.
Что, если браузер не завершил загрузку и создание CSSOM, когда мы хотим запустить наш скрипт? Ответ прост и не очень хорош для производительности: браузер откладывает выполнение скрипта и построение DOM до тех пор, пока не завершится загрузка и построение CSSOM.
Короче говоря, JavaScript вводит множество новых зависимостей между DOM, CSSOM и выполнением JavaScript. Это может привести к значительным задержкам браузера при обработке и отображении страницы на экране:
- Расположение сценария в документе имеет важное значение.
- Когда браузер встречает тег скрипта, построение DOM приостанавливается до тех пор, пока скрипт не завершит выполнение.
- JavaScript может запрашивать и изменять DOM и CSSOM.
- Выполнение JavaScript приостанавливается до тех пор, пока CSSOM не будет готов.
В значительной степени «оптимизация критического пути рендеринга» относится к пониманию и оптимизации графа зависимостей между HTML, CSS и JavaScript.
Блокировка парсера и асинхронный JavaScript
По умолчанию выполнение JavaScript является «блокировкой синтаксического анализатора»: когда браузер встречает сценарий в документе, он должен приостановить построение DOM, передать управление среде выполнения JavaScript и позволить сценарию выполниться, прежде чем продолжить построение DOM. Мы видели это в действии со встроенным скриптом в нашем предыдущем примере. Фактически, встроенные скрипты всегда блокируют парсер, если вы не напишете дополнительный код для отсрочки их выполнения.
А как насчет сценариев, включенных через тег сценария? Давайте возьмем наш предыдущий пример и вынесем код в отдельный файл:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
<title>Critical Path: Script External</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script src="app.js"></script>
</body>
</html>
приложение.js
var span = document.getElementsByTagName('span')[0];
span.textContent = 'interactive'; // change DOM text content
span.style.display = 'inline'; // change CSSOM property
// create a new element, style it, and append it to the DOM
var loadTime = document.createElement('div');
loadTime.textContent = 'You loaded this page on: ' + new Date();
loadTime.style.color = 'blue';
document.body.appendChild(loadTime);
Независимо от того, используем ли мы тег <script> или встроенный фрагмент JavaScript, вы ожидаете, что оба будут вести себя одинаково. В обоих случаях браузер приостанавливает и выполняет сценарий, прежде чем сможет обработать оставшуюся часть документа. Однако в случае внешнего файла JavaScript браузер должен сделать паузу, чтобы дождаться получения сценария с диска, из кэша или удаленного сервера, что может добавить десятки-тысячи миллисекунд задержки к критическому пути рендеринга.
По умолчанию весь JavaScript блокирует парсер. Поскольку браузер не знает, что скрипт планирует сделать на странице, он предполагает наихудший сценарий и блокирует синтаксический анализатор. Сигнал браузеру о том, что сценарий не нужно выполнять именно в том месте, где он указан, позволяет браузеру продолжить создание DOM и позволить сценарию выполниться, когда он будет готов; например, после того, как файл получен из кэша или удаленного сервера.
Для этого мы помечаем наш скрипт как async :
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
<title>Critical Path: Script Async</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script src="app.js" async></script>
</body>
</html>
Добавление ключевого слова async в тег скрипта сообщает браузеру не блокировать построение DOM, пока он ожидает доступности скрипта, что может значительно повысить производительность.