Управление скоростью реакции сайта

Николай рассказал нам о том, как улучшить время загрузки данных, но мы помним о том, что у скорости страниц есть еще минимум две составляющих: это время инициализации новой страницы и время реакции элементов на странице. Под инициализацией я здесь понимаю отрисовку html/css и инициализацию скриптов. Рассмотрим сначала отрисовку. В этой области я не большой специалист, и не очень много могу сказать. Во-первых, очевидно, что простые страницы быстрее — все мы помним тесты производительности браузеров, основанные на отрисовке тысяч позиционированных элементов. Во-вторых, нельзя злоупотреблять expressions — некоторые из них выполняются даже при прокрутке страницы, и ощутимо нагружают процессор. Конечно, без них бывает трудно бороться с особенностями всенародно любимого браузера, но чем меньше expressions, тем лучше для скорости. Вообще больше всего на этом фронте оптимизируют те, кто делает браузеры, а не страницы. Однако и у нас есть простой и хороший способ значительно ускорить отрисовку: достаточно подключать javascript после основного контента. Рассмотрим это подробнее.

У подключения javascript есть одна серьезная проблема — это возможность вызвать в коде document.write. Браузер обязан вставить созданный таким образом html сразу после тега script. Чем это плохо?

Вот что делает браузер, если при разборе html-кода он обнаруживает, например, тег img: файл картинки ставится в общую очередь на загрузку, а браузер продолжает разбирать следующие теги. Но если в коде встречается тег script, браузер останавливается и ждет загрузки скрипта. Если бы он этого не делал, он рисковал бы обнаружить document.write уже после парсинга следующих тегов, и часть страницы, если не всю её, приходилось бы парсить заново. Поэтому в худшем случае каждый тег script в коде грозит вам остановкой отрисовки страницы до тех пор, пока все файлы в очереди закачки не будут загружены, чтобы браузер мог выполнить javascript и пойти дальше.

Эта проблема известна давно, и производители пытались с ней бороться. Microsoft предложил атрибут defer для тега script. С помощью этого атрибута разработчик может обещать браузеру, что в этом конкретном скрипте document.write не будет использоваться. Однако поддержка его разными браузерами хромает. В интернете о нем разные мнения, но по моим ощущениям гуру советуют не полагаться на него.

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

В такой ситуации нам может помочь другая пока что малоизвестная, но очень рекомендуемая практика под названием progressive enhancement. На РИТе вы могли слышать затрагивающий ее доклад Алексея Сергеева. Если говорить коротко, она заключается в следующем: вся основная функциональность страницы должна работать даже с отключенным javascript, но если он включен, то реакция некоторых элементов просто улучшается. То есть, никакие важные функции не полагаются на наличие javascript.

Чем это помогает в нашей ситуации? У нас есть видимый элемент страницы, поведение которого еще не улучшено javascript. Однако у этого элемента есть действие по умолчанию, например, это ведущая на другую страницу ссылка, или отправляющая на сервер форму кнопка. Именно это стандартное действие и будет выполнено, если пользователь не дождался загрузки скриптов. Кстати, такой подход еще и значительно упрощает скриптование форм: часто бывает достаточно просто отправить форму аяксом на сервер.

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

На этом я закончу об отрисовке страницы. Перейдем к инициализации скриптов.

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

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

Самый распространенный случай — обрабатывайте побольше элементов при загрузке. Например, навесьте на все ссылки на странице обработчик. Или пройдитесь по текстовым узлам, и примените к каждому типографику. С первым неразрывно связан второй: увеличивайте количество reflow для страницы всеми средствами. Уместно будет динамически поставить после каждой ссылки иконку, как это делает сервис SnapShots.com. Еще один пример: создать про запас dom-объекты, которые могут пригодиться. Потом, при необходимости, они будут просто показаны, и ваш код будет гладким и шелковистым.

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

Около полутора лет назад появилась маленькая библиотека, скорее даже расширение prototype.js, под названием behavior.js. Она позволяла использовать несколько расширенные css-селекторы для назначения обработчиков элементам. То есть, делала почти то же самое, для чего сейчас часто используется jQuery. Идея была очень интересная и удобная. Однако, несмотря на всю красоту, эта библиотека практически не подходила для использования. Основная проблема была, как обычно, со всенародно любимым браузером: при загрузке каждой страницы он на несколько секунд практически подвешивал компьютер. Корень зла уходил в поиск элементов.

Какие методы обычно используются для этой задачи? Их можно выделить четыре: xpath, id, tagName и просто name. Призрак будущего — xpath — предназначен именно для этой задачи, и позволяет быстро выполнять самые сложные поисковые запросы по дереву dom. Как обычно, он не работает в IE. Следом за ним идет getElementById — очень быстрый способ получить единственный элемент на странице. К сожалению, даже с ним у IE проблемы, и этими вызовами тоже не стоит злоупотреблять. Но основная рабочая лошадка поиска элементов — это getElementsByTagName, и, как водится, именно она работает медленнее всего. В IE. На сложных страницах время работы этой функции может достигать 500 мс полной загрузки процессора. А если этих вызовов несколько, полсекунды нужно умножать на их количество. К сожалению, труд авторов библиотек не может ощутимо улучшить эту ситуацию. И последним в списке стоит часто незаслуженно забытый name. Метод getElementsByName показывает очень неплохие результаты во всенародно любимом браузере, и в некоторых ситуациях стоит использовать именно его. Однако есть лучший вариант.

Этот вариант — использование глобальных обработчиков. Напомню, что большинство событий «всплывает» вверх по дереву dom до самого корневого элемента. Именно там их можно обнаружить и обработать, используя свойства событий target и srcElement для выявления источника события.

Какие плюсы нам дает этот подход? Во-первых, мы избавляемся от необходимости искать элементы, чтобы прикрепить к ним обработчики событий, потому что document у нас всегда под рукой в глобальном объекте. Кроме того, мы обрабатываем только один элемент, а не десятки. Еще один плюс, который редко, но очень приятно проявляет себя — это автоматическая обработка даже динамически созданных элементов. Представьте, как приходилось бы действовать без использования этого подхода: каждый метод, создающий элементы, должен был бы знать обработчики, которые могут быть навешены на этот элемент или использовать сложные архитектурные решения. В случае же глобального обработчика все происходит автомагически.

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

Используя такую тактику, мы добиваемся того, что при загрузке страницы происходит действительно минимум действий: на document навешивается несколько обработчиков, и всё.

Теоретически, можно улучшить behavior.js или jQuery таким образом, чтобы они использовали именно глобальные обработчики, но пока что я не знаю о таких проектах.

На этом разговор об инициализации можно закончить, и перейти к скорости реакции элементов страницы. Наиболее распространенные события для обработки — это hover и click. Начнем с click.

Есть очень простой способ ускорить реакцию на click на 100–200 миллисекунд. Для этого достаточно использовать вместо события click событие mousedown. Но, как обычно, выигрывая время, мы проигрываем что-то другое. В данном случае мы жертвуем возможностью для продвинутых пользователей изменить решение уже после нажатия кнопки — в случае использования click они могли бы отвести мышь в сторону. Вам решать, готовы ли вы на этой пойти ради скорости.

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

Еще один неожиданный прием для ускорения реакции на события можно назвать «забудьте все, чему вас учили». Я узнал его из презентации разработчика Plaxo.com по оптимизации javascript, и полагаюсь в этом на его слова. Прием заключается в использовании dom-0 обработчиков (то есть, onclick etc.) вместо addEventListener, и позволяет выиграть еще порядка 70 миллисекунд. Конечно, это экстремальный вариант для исключительных случаев.

Теперь поговорим о hover. Эта замечательная вещь встроена в css, и позволяет даже без javascript создавать очень интересные решения, от выпадающих меню до игр. Однако она не отличается особой гибкостью, и в этом ее проблема — она всегда срабатывает моментально.

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

Итак, вот примитивный пример меню. Я хочу заново открыть недавно созданный документ. После того, как я навел мышь на пункт «Последние» и увидел подменю, самым естественным движением для меня будет повести курсор мыши прямо к цели. Конечно, в простейшей реализации у меня это не получится как только курсор окажется над пунктом «Выход», подменю исчезнет. «Правильный» подход в таком случае — провести мышь по горизонтали до субменю, и только потом двигать его вниз. Посколько ширина основного меню может быть большой, а высота строчки маленькой, эта задача не из простых.

Как можно исправить эту проблему? Флэшеры любят красоваться плавно появляющимися и исчезающими блоками. Это красиво, но имеет свои минусы: javascript-реализация может чувствительно притормаживать, и для продвинутых пользователей это будет неудобно, потому что они хотят более быстрой реакции. Другой подход — увеличить время между системным hover и моментом, когда на него реагирует интерфейс. То есть, пользователь наводит мышь на субменю, а оно появляется, например, только через 200 миллисекунд. Если это действие было случайным, ничего не произойдет. Мы получим интерфейс, срабатывающий только при необходимости. И добиться этого очень просто. Настолько просто, что прямо сейчас я расскажу, как.

Я определил у dom-объектов метод delayedHover(). Внутри себя он использует более общий метод функций getDelayedHandlers(), который также определил я. Вначале рассмотрим более простой delayedHover(). Он получает три параметра: собственно dom-объект, функцию-обработчик и настройки задержек. Внутри него действительно всего несколько строк: мы вызываем getDelayedHandlers() у функции-обработчика, и назначаем полученные из него методы dom-объекту на mouseover/mouseout. Эти методы блокируют срабатывание друг друга. Например, если сразу после mouseover произойдет mouseout, то обработчик mouseover не выполнится. И наоборот.

Рассмотрим, как добиться такого поведения. Метод getDelayedHandlers() объявляет две симметричных функции. Каждая из них вначале сбрасывает таймаут другой, и после этого создает свой таймаут. По таймауту основная функция вызывается с параметром либо true, либо false.

А теперь — пример использования «медленного hover». Нам нужно показывать подсказку для какого-то элемента. Определим функцию, которая в зависимости от параметра либо показывает, либо прячет подсказку. После этого вызовем у кнопки метод delayedHover, и передадим ему параметрами эту функцию и временны.е настройки. Теперь подсказка будет появляться с задержкой 200 мс, и исчезать с задержкой 100 мс.

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

Вопросы?