Оптимизация HTML5 приложений

This post is also available in: Английский

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

Фронтэнд веб проекта состоит из 3х основных частей: HTML, CSS, JavaScript. Ниже мы представим как способы оптимизации каждого из этих составляющих приложения, так и общие советы по ускорению фронтэнда.

Общие советы

Уменьшайте число HTTP запросов

Помните, что каждый HTTP запрос влечёт за собой ожидания подключения и установления соединения между фронтом и бэкэндом.  На изображении приведена информация по временным интервалам запроса google.com:

Как видно,  получение данных заняло менее 32%. Все остальное время происходило соединение  с сервером и подготовка им информации для отправки. Таким образом большое количество запросов небольшого объема данных могут привести к существенной деградации производительности.  Уменьшив число HTTP запросов мы сведем к минимуму время подключения и ожидания данных.

Объединяйте контент

По указанной выше причине объединяйте скрипты и css-стили в общие файлы. Изображения же компонуйте в спрайты и не забывайте про их оптимизацию.  При расположении на них картинок отдавайте предпочтение увеличению спрайта по горизонтали, а не по вертикали – как правило в таком случае вы выиграете в размере полученного файла.

Для минификации существуют различные инструменты. Самым популярным является YUI Compressor (http://developer.yahoo.com/yui/compressor/). Также стоит упомянуть Google Closure Compiler (https://developers.google.com/closure/compiler/). Эта утилита не просто минифицирует код. Она анализирует его, оптимизирует и выкидывает ненужные участки, существенно увеличивая быстродействия. Но помните и об обратной стороне медали: минифицированный код, а уж тем более скомпилированные, может исполняться с ошибками и труднее в отладке. Поэтому тщательно проверяйте работу приложения. Автоматизировать данный  процесс помогут Unit-тесты, о которых пойдет речь в одной из наших будущих статей.

Не масштабируйте изображения

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

По возможности используйте GET запросы

Причина, по которой стоит отдавать преимущество GET запросам перед POST – особенность второго, заключающееся к отправке сначала заголовков запроса, и лишь затем данных. В GET все отправляется в одном TCP пакете.

Управляйте временем кэширования

Старайтесь  управлять кэшированием контента в Ваших проектах. Каждый год дизайн вэб-приложений становится все сложнее и сложнее. Это означает увеличение числа скриптов, стилей и страниц. Не заставляйте пользователя постоянно грузить один и тот же контент. Для статичного содержимого используйте заголовок “Expires”, для динамичного — “Cache-Control”.

CSS и JS – во внешние файлы

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

Уменьшайте и сжимайте ваши стили и скрипты.

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

Разделяйте контент по суб-доменам

Разнесение, скажем, скриптов на домен sub1.site.com, а стилей на sub2.site.com позволит браузеру производить их загрузку одновременно.  Обычно браузеры могут поддерживать до 10 одновременных загрузок, так что вы в состоянии также распределить изображения по суб-доменам и увеличить интенсивность их загрузки пользователем. Помимо этого помните о передачи Cookie при запросах. При использовании субдоменов мы избавляемся от ненужных передач cookie.

Избегайте редиректов в ваших AJAX запросах

Старайтесь избегать их, т.к. они заставляют пользователей дольше ждать так важную для них информацию.  Помните, что очень часто к редиректу приводит отсутствие закрывающего слэша в конце URL. Например запрос к  www.site.com/directory приведет к редиректу на URL www.site.com/directory/.

Предзагрука компонентов – это хорошо

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

Постзагрузка – тоже хорошо

Примеров компонентов, загрузку которых стоит производить в последнюю очередь – js-скрипты. Дело в том, что когда браузер встречает на странице тег script, он прекращает рендеринг содержимого до тех пор, пока не будет исполнен скрипт.

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

Отдельные версии для мобильных устройств

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

Ускоряем HTML разметку

Уменьшайте число элементов в DOM-дереве и оптимизируйте их

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

Закрывайте теги

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

Несколько слов о CSS

Не дублируйте свойства стилей

Если у вас есть несколько элементов с похожим описанием (например с синей рамкой в 1 пиксель), то не стоит описывать эту рамку для каждого элемента. Присвойте им все м единый дополнительный класс, рисующий рамку. Например:

.bordered {
    border: 1px solid blue;
}

Используйте краткую запись  стилей

Многие свойства CSS имеют как полную, так и краткую запись (например margin, padding, border, background).  При описании стилей используйте краткую форму.  Это позволит уменьшить размер ваших файлов стилей. Например:

.style { 	/* Плохо */
	margin-top: 5px;
        margin-left: 20px;
}

.style {		/* Хорошо */
	margin: 5px 0 0 20px;
}

Откажитесь от Filter, Expressions

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

Анимация

Современные веб-технологии предоставляют возможность добавить на страницу анимацию. 2 основных способа – анимация посредство JavaScript и CSS3 Transitions.

Для JS-анимации популярные библиотеки (в том числе JQuery) предоставляют функции для удобного управления перемещением и трансформацией объектов. Однако, даже простейшая анимация посредством JS заключается в наборе последовательных смен состояний отображения объекта (его стилей CSS). Какждая такая смена влечет к перестроению DOM дерева, а значит провоцирует серьезную нагрузку на браузер. А если будут производится сразу несколько трансформаций, то о плавности анимации можно забыть. Мобильные же устройства в приницпе не справляются с такими задачами и вместо красивых перемещений будет получен эффект слайд-шоу.

CSS анимация – гораздо менее ресурсоемкий процесс. В этой статье представлен пример демонстрирующий ее преимущества в быстродействии. Тем не менее на embed-устройствах ее плавность все равно может не быть идеальной. Поэтому стоит задуматься о необходимости анимированных трансформаций для мобильной версии вашего сайта.

Ускорение анимации

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

  • Google Chrome 13
  • Mozilla Firefox 4
  • Microsoft Internet Explorer 9
  • Opera 11
  • Apple Safari 5

Замер скорости анимации

Для измерения FPS мы рекомендуем использовать небольшую JS – библиотеку Stats.js.  Она выведет на экран небольшой виджет, в котором вы сможете наблюдать за скорость анимации на вашем сайте.  Также вы можете воспользоваться букмарклетом, добавляющим подобный монитор на любой сайт. Для этого добавьте указанную ниже ссылку в закладки, а затем на нужном сайте перейдите по ней.

FPS Monitor

Средства и инструменты измерения скорости загрузки страницы

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

Также можно замерить скорость загрузки страницы и скорость построения DOM. Для этого стоит использовать события window.onload и $(document).ready(). window.onload отработает когда прогрузятся весь контент, изображения и скрипты. $(document).ready – когда будет построен DOM. Измерить скорость чистого ренедеринга, к сожалению,не получится.

Пример:

beforeload = (new Date()).getTime();
function pageLoadingTime() {
    afterload = (new Date()).getTime();
    secondes = (afterload-beforeload)/1000;
    console.log('Your Page Load took  ' + secondes + ' seconde(s).');
}

function domLoadingTime() {
    afterload = (new Date()).getTime();
    secondes = (afterload-beforeload)/1000;
    console.log('Your DOM Load took  ' + secondes + ' seconde(s).');
}

window.onload = pageLoadingTime;
$(document).ready(domLoadingTime);

Оптимизация JavaScript

Расположение скриптов  на странице

Подключение JS-файлов имеет большее значение, чем можно представить. Особенность заключается в том, что встречая тег <script> браузер сначала грузит файл с кодом, а заетм исполняет его. Во время исполнения браузер останавливает все остальные действия: будь то загрузка контента или рендеринг страницы.

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

Оптимизация кода

Доступ к данным

В JavaScript у функциии всегда есть доступ к объектам, находящимся вне ее. К примеру:

var name = “Some Name”;
var someFunc = function() {
	return name;
}
someFunc(); // Возвращает “Some Name”;

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

Пример:

var arr1 = [],
    arr3 = [];

var func1 = function() {
    for (var i = 0; i<100000; i++) {
        arr1.push(i);
    }
}

var func2 = function() {
    var arr2 = [];

    for (var i = 0; i<100000; i++) {
        arr2.push(i);
    }
}

var func3 = function() {
    var arr4 = arr3;

    for (var i = 0; i<100000; i++) {
        arr4.push(i);
    } 

    arr3 = arr4;
}

var startTime = (new Date()).getTime();
func1();
console.log('Outer Arr push time: ' + ((new Date()).getTime()- startTime));

startTime = (new Date()).getTime();
func2();
console.log('Inner Arr push time: ' + ((new Date()).getTime()- startTime));

startTime = (new Date()).getTime();
func3();
console.log('Combo Arr push time: ' + ((new Date()).getTime()- startTime));

Результат работы кода:

Outer Arr push time: 65
Inner Arr push time: 11
Combo Arr push time: 12

Как видно скорость добавления элемента во внешний массив в 5,5 раз медленнее, чем в локальный. В тоже время длительность выполнения операций присваивания содержимого внешнего массива внутреннему и обратно занимает всего 1 мс и мало сказывается на быстродействии кода.

Меморизация

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

var result = 1,
    resultArr = [0, 1];

var factorial = function(num) {
    if (resultArr[num]) {
        return resultArr[num];
    } else {
        var k = resultArr.length - 1;
        result = resultArr[k];

        for (var i = k; i <= num; i++) {
            console.log(i);
            result = result * i;
            resultArr[i] = result ;
        }

        return resultArr;
    }
}

Теперь вычисление какого-либо факториала рассчитывает сразу и факториалы меньших чисел. В том же случае, если требуемый факториал больше, то его вычисление начинает не с 1, а с максимального числа из тех, факториалы которых уже известны.

Оптимизация условных операторов

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

Работа с DOM

Добавление элементов

Процедуры манипуляции с DOM страницы являются очень ресурсоемкими для браузера. Для примера приведем процедуру добавления на страницу множества <span> элементов в цикле. Браузер будут перестраивать DOM каждый раз при добавлении на страницу нового элемента. Оптимизировать этот процесс можно накопив все множество <span> внутри какой-то переменной и затем за один раз добавить в DOM. Если с каким-то элементом на странице нужно произовдить набор операций, то не стоит постоянно выбирать его селектором. Достаточно записать его в переменную и обращаться уже к ней.

Пример:

$('body').html('

');

var startTime = (new Date()).getTime();
for (var i = 0; i< 1000; i++) {
    $('.someClass').append(''+i+'');
}
console.log('DOM append 1 by 1 time: ' +
   ((new Date()).getTime()- startTime));

$('.someClass').html('');

startTime = (new Date()).getTime();
var el = $('.someClass');
for (var i = 1000; i< 2000; i++) {
    el.append(''+i+'');
}
$('.someClass2').append(string);
console.log('DOM append 1 by 1 without reselecting target el time: ' +
    ((new Date()).getTime()- startTime));

startTime = (new Date()).getTime();
var string = '';
for (var i = 1000; i< 2000; i++) {
    string += ''+i+'';
}

$('.someClass2').append(string);
console.log('DOM append all together time: ' +
    ((new Date()).getTime() - startTime));

Результат работы кода:

DOM append 1 by 1 time: 958
DOM append 1 by 1 without reselecting target el time: 163
DOM append all together time: 60

Отказ от повторных селектов одного и того же элемента приводит к 16ти кратному увеличению производительности, в тоже время выливка всего набора элементов за 1 раз ускоряет код почти в 6 раз.

Оптимизация селекторов

Использование селекторов JQuery – устоявшаяся практика при разработке веб-приложений. Типовой проект может содержать сотни селекторов. Выборку элементов также можно оптимизировать. В том случае, если мы знаем внутри какого блока локализованы нужные нам элементы, то следует уточнить выборку:

startTime = (new Date()).getTime();

for (var i = 0; i< 1000; i++) {
    $('.cl');
}    

console.log('Simple select of els time: ' +
    ((new Date()).getTime() - startTime));

startTime = (new Date()).getTime();

for (var i = 0; i< 1000; i++) {
    $('someClass2 .cl_0');
}

console.log('Target select of els time: ' +
    ((new Date()).getTime()- startTime));

Результат работы кода:

Simple select of els time: 553
Target select of els time: 180

Как видно из результатов теста уточнения селекторов увеличивает скорость выборки в 3 раза.

Делегирование событий

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

  1. Создать единое событие нажатия на меню (оно всплывет)
  2. При нажатии определять целевой объект
  3. Описывать в конструкции if..else обработчики для объектов
  4. Останавливать «всплытие»

Пример:

$(‘#menu’).click(function(e) {
    e = e || window.event;
    var target = e.target || e.srcElement;

    if (target.nodeName === ‘menuItemName’ ) {
        alert(‘!!!’);
        e.preventDefault();
        e.stopPropagation();
    }
});

Обратная сторона медали

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

Инструменты для профилирования JS

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

  • YUI Profiler – JS-библиотека для профилирования
  • Firebug – популярный и мощный плагин для Firefox. Помимо профилирования предоставляет еще массу других возможностей и инструментов для разработчика.
  • WebKit Web Inspector  — аналог Firebug для Safari/Chrome
  • IE Developer Tools – профайлер для браузера от Microsoft

Полезные ссылки