Рисуем в вашем присутствии

Артемий Ломов

Браузеры отображают открываемые веб-страницы в потоковом режиме. Иными словами, элементы HTML-документа отрисовываются сразу же по мере их создания внутри DOM в процессе загрузки страницы. Взаимное расположение, размеры и прочие характеристики элементов, определяемые применяемыми стилевыми правилами, могут неоднократно изменяться впоследствии в ходе продолжения обработки загружаемого HTML-документа.

Наглядная агитация

Впрочем, лучше один раз увидеть, чем сто раз услышать. Для своего доклада «Быстрый удар точно в цель», посвященного CSS-селекторам и представленного, в частности, на встрече WSD в Екатеринбурге, автор этих строк подготовил наглядную демонстрацию искусственно замедленной загрузки веб-страницы.

Рассмотрим более детально, как браузер обрабатывает, к примеру, такой фрагмент кода:


<body>
	<div id="wrapper">
		<header id="intro">
			<h1>…</h1>
			<p>…</p>
		</header>

	</div>
</body>

Браузер сразу же, вне зависимости от наличия или отсутствия в коде разметки соответствующих тегов, создает внутри DOM элементы html и body, немедленно применяя относящиеся к ним стилевые правила. Например, если в CSS-коде фигурирует конструкция наподобие body {background: black;}, фон страницы окрасится черным еще до того, как будет загружена первая порция осмысленного контента (при условии, что файл таблицы стилей уже загружен или восстановлен из кэша).

Получив открывающий тег <div id="wrapper">, браузер создает соответствующий элемент внутри DOM и, опять же, немедленно применяет к этому элементу относящиеся к нему стилевые правила.

Аналогичное происходит в моменты получения открывающих тегов <header id="intro"> и <h1>.

Получив содержимое элемента <h1>…</h1> (имеющее при отображении ненулевые ширину и высоту) браузер вынужден перерисовать этот элемент и все элементы, являющиеся по отношению к нему предками, с учетом изменившихся размеров элемента <h1>…</h1>. То же самое происходит по мере получения содержимого элемента <p>…</p>.

Пересчет и перерисовка

В ходе первичного отображения HTML-документа имеют место множественные процессы пересчета (reflow) и перерисовки (repaint) элементов. Пересчет — это рекурсивный обход (как правило, начиная с изменившегося элемента) ветви DOM-дерева, осуществляющийся с целью вычисления размеров элементов и их положения относительно родительского элемента. В том случае, если вычисленные размеры или положение элемента изменились, производится его перерисовка.

Пересчет и возможная перерисовка тех или иных элементов осуществляются не только в ходе первоначальной загрузки веб-страницы, но и в ряде других ситуаций, к которым относятся, в частности:

  • изменение размеров окна браузера или шрифта пользователем;
  • динамические изменения внутри DOM посредством скрипта;
  • динамические изменения стилей элемента посредством скрипта;
  • периодическое срабатывание по таймеру (это характерно, в частности, для браузера Opera).

Требовательность к ресурсам

Казалось бы, описанный сценарий развертывания процесса отображения веб-страниц при первоначальной загрузке жутко избыточен с точки зрения ресурсозатрат. Неужели не проще применить стилевые правила уже после того, как дерево DOM полностью построено, один раз и навсегда отрисовав все элементы?.. Это ведь получилось бы сделать намного быстрее!

Ответ на данный вопрос не столь однозначен.

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

Даже CSS-правила не все браузеры применяют честно. Так, скажем, Opera и браузеры на основе движка Webkit (Chrome, Safari…) игнорируют селектор E:last-child до тех пор, пока родительский элемент (по отношению к тем, для которых проверяется соответствие данному селектору) не будет загружен целиком. На этот случай у меня тоже есть наглядная демка замедленной загрузки: сравните процесс отрисовки меню навигации в одном из упомянутых браузеров и, к примеру, в Firefox, обращая внимание на скругленные уголки в нижней части меню.

В теории браузеры могли бы применять CSS-правила отложенно, только после полной загрузки документа и его последующего синтаксического анализа, при обработке XHTML-страниц (речь идет, конечно, об «истинном» XHTML). На практике же распространенные в современном мире браузеры начинают отображать XHTML-документы, не дожидаясь их полной загрузки, осуществляя вывод сообщений об ошибках синтаксиса только по мере обнаружения этих самых ошибок. (Автор из спортивного интереса ставил соответствующие эксперименты.)

Несмотря на то, что постепенная, потоковая отрисовка элементов веб-страниц по мере их загрузки, похоже, существенно проигрывает гипотетическому отображению документов «одним махом» в плане экономии ресурсов, у нее есть по крайней мере одно очень важное преимущество, лежащее отнюдь не в технологической плоскости.

Удобство для пользователя — превыше всего

Речь идет о принципах проектирования пользовательских интерфейсов. Возможно, в случае рендеринга страницы только после получения всей необходимой информации с момента, грубо говоря, щелчка по ссылке до момента полного завершения отрисовки всех элементов страницы пройдет ощутимо меньше времени, чем в случае потокового отображения. Проблема в том, что в течение большей части этого времени браузер практически никак не будет проявлять себя, демонстрируя в лучшем случае некий индикатор прогресса — сколько там процентов от объема нужных файлов загружено с сервера.

Другое дело, когда происходят все эти пресловутые бесконечные пересчеты и перерисовки. Задолго до полной загрузки всех необходимых объектов с сервера пользователь может наблюдать в окошке браузера какие-то части страницы, начинать читать первые порции контента. Видимость состояния системы, последовательное проявление на экране разных областей страницы «успокаивает» пользователя, дает ему понять, что процесс загрузки исправно продвигается.

Принято считать, что время отклика, укладывающееся в 1⁄10 секунды, свойственно интерфейсам, призванным работать в режиме реального времени. Такой ответ расценивается пользователем как мгновенный. Предельная продолжительность промежутка времени, в течение которого ход мыслей пользователя не прерывается, несмотря на уже заметную задержку — 1 секунда. Наконец, максимальное время, на протяжении которого внимание пользователя может быть сосредоточено на диалоге с интерфейсом — 10 секунд. Когда время отклика интерфейса превышает это значение, пользователь, как правило, отвлекается и переключается на другие задачи, пока дожидается ответа.

В данном контексте примечателен доклад Егора Дыдыкина «Новая главная портала Mail.Ru», прозвучавший на апрельской конференции РИТ++. Егор поделился весьма занятными цифрами. По его словам, старая версия главной страницы Mail.Ru загружалась в течение 7 секунд и, будучи сверстанной таблицами, в некоторых браузерах не подавала признаков жизни бо́льшую часть этого времени. В новой же версии портальная навигация (верхняя горизонтальная навигационная область) показывается уже через 195 миллисекунд после начала загрузки страницы, левый вертикальный блок — еще через 180 миллисекунд, форма поиска — еще через 40 и так далее. Общее время загрузки составляет около полутора секунд. Очевидно, все это делает новую версию главной страницы Mail.Ru более привлекательной для пользователя по сравнению со старой.

Веб-страница — не PDF-документ

Как мне представляется, множественные процессы пересчета и перерисовки хотя и требуют ресурсов, но явно не космических. Особенно в сравнении с разными другими штуками на уровне интерфейса операционных систем и приложений — взять хотя бы пресловутый интерфейс Aero с полупрозрачными окошками и анимационными эффектами, реализованный в Windows 7. Вряд ли сто́ит жертвовать очевидной выгодой с точки зрения пользовательских ощущений в пользу нескольких сотен миллисекунд (ну пусть даже единиц секунд на каких-то сверхсложных страницах) экономии времени в условиях, когда пользователь не будет иметь возможности наблюдать вживую процесс загрузки.

Ладно бы, веб-страницы рендерились бы один раз и навсегда, будучи чем-то наподобие PDF-документов. Но этого так или иначе нет и не будет — пересчет и перерисовку все равно придется осуществлять при изменении размеров окна браузера пользователем, при изменении им размера шрифта, при динамических изменениях в DOM или стилях при помощи JavaScript (особенно в современной ситуации, когда что бы то ни было может подгружаться с сервера асинхронно в любой момент). Нет смысла отказываться на этапе первоначальной загрузки страницы от вполне работающего механизма, который все равно будет использоваться в дальнейшем.

Возможно, производителям браузеров имеет смысл как-то специально оптимизировать обработку ресурсоемких CSS-селекторов, не производить ее по обычному сценарию (в рамках которого селекторы обрабатываются справа налево) для каждого элемента. Но тут есть определенная опасность: когда браузеры пойдут каждый своим путем, кто в лес, кто по дрова — энтропия возрастет, снизится предсказуемость эффективности тех или иных решений. (Упомянутое выше «читерство» со стороны Webkit-браузеров и Opera вряд ли можно считать правильным. Решения такого порядка характерны скорее для JavaScript, нежели для честного CSS.)