Опубликован: 16.02.2009 | Уровень: специалист | Доступ: платный
Лекция 4:

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

В теории

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

— dom.js
 — array.map.js
	— array.js
  — sprinf.js
— calendar.js
  — date.js
   — mycoolcombobox.js
	— dom.js
		— array.map.js
			— array.js
		— sprinf.js
— animated.pane.js
 — pane.js
	— dom.js
		— array.map.js
			— array.js
		— sprinf.js
			— animation.js
	— transition.js
... и так далее ...

Дальше мы выбираем непосредственно нужные сайту вершины. Пусть это будут dom.js и animated.pane.js . Теперь это дело техники — обойти получившийся набор деревьев в глубину:

— array.js
— array.map.js
— sprinf.js
— dom.js
— array.js
— array.map.js
— sprinf.js
— dom.js
— pane.js
— transition.js
— animation.js
— animated.pane.js

...удалить повторяющиеся элементы:

— array.js
— array.map.js
— sprinf.js
— dom.js
— pane.js
— transition.js
— animation.js
— animated.pane.js

и слить соответствующие модули воедино.

На практике

Хранить информацию о зависимостях можно, например, следующим образом (добавляя в "модули" служебные комментарии):

// #REQUIRE: array.map.js
// #REQUIRE: sprintf.js
....
Код

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

К чему мы пришли?

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

Итак, мы оставили нового пользователя наедине с единственным JavaScript-файлом, не включающим ничего лишнего. Стал ли при этом пользователь счастливее? Ничуть. Наоборот, в среднем пользователь стал более несчастным, чем раньше, а причина этому — увеличившееся время загрузки страницы.

Немного из теории HTTP-запросов

Время загрузки ресурса через HTTP-соединение складывается из следующих основных элементов:

  1. время отсылки запроса на сервер T1— для большинства запросов величина практически постоянная;
  2. время формирования ответа сервера— для статических ресурсов, которые мы сейчас и рассматриваем, пренебрежимо мало;
  3. время получения ответа сервера T2, которое, в свою очередь, состоит из постоянной для сервера сетевой задержки L и времени получения ответа R, прямо пропорционального размеру ресурса.

Итак, время загрузки страницы будет состоять из времени загрузки HTML-кода и всех внешних ресурсов: изображений, CSS- и JavaScript-файлов. Основная проблема в том, что CSS и JavaScript-файлы загружаются последовательно (разработчики браузеров уже работают над решением этой проблемы в последних версиях, однако пока еще 99% пользователей страдают от последовательной загрузки). В этом случае общение с сервером выглядит так:

— запросили страницу
— получили HTML 
— запросили ресурс A: T1
— получили ресурс A: L + R(A)
— запросили ресурс B: T1
— получили ресурс B: L + R(B)
— запросили ресурс C: T1
— получили ресурс C: L + R(C)

Общие временные затраты при этом составят 3(T1+L) + R(A+B+C).

Объединяя файлы, мы уменьшаем количество запросов на сервер:

— запросили страницу
— получили HTML 
— запросили ресурс A+B+C: T1
— получили ресурс A+B+C: L + R(A + B + C)

Очевидна экономия в 2(T1 + L).

Для 20 ресурсов эта экономия составит уже 19(T1 + L). Если взять достаточно типичные сейчас для домашнего/офисного Интернета значения скорости в 256 Кбит/с и пинга ~20–30 мс, получим экономию в 950 мс— одну секунду загрузки страницы. У людей же, пользующихся мобильным или спутниковым интернетом с пингом более 300 мс, разница времен загрузки страниц составит 6-7 секунд.

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

Суровая реальность

Пусть у нашего сайта есть три страницы — P1, P2 и P3, поочередно запрашиваемые новым пользователем. P1 использует ресурсы A, B и C, P2— A, С и D, а P3— A, С, E и F. Если ресурсы не объединять, получаем следующее:

  • P1— тратим время на загрузку A, B и C.
  • P2— тратим время на загрузку только D.
  • P3— тратим время на загрузку E и F.

Если мы слили воедино абсолютно все JavaScript-модули сайта, получаем:

  • P1— тратим время на загрузку (A+B+C+D+E+F).
  • P2— внешние ресурсы не требуются.
  • P3— внешние ресурсы не требуются.

Результатом становится увеличение времени загрузки самой первой страницы, на которую попадает пользователь. При типовых значениях скорости/пинга мы начинаем проигрывать уже при дополнительном объеме загрузки в 23Кб.

Если мы объединили только модули, необходимые для текущей страницы, получаем следующее:

  • P1— тратим время на загрузку (A+B+C).
  • P2— тратим время на загрузку (A+C+D).
  • P3— тратим время на загрузку (A+С+E+F).

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

Возможное решение

Конечно же, выход из сложившегося положения есть. В большинстве случаев для получения реального выигрыша достаточно выделить "ядро"— набор модулей, используемых на всех (или, по крайней мере, на часто загружаемых) страницах сайта. Например, в нашем примере достаточно выделить в ядро ресурсы A и B, чтобы получить преимущество:

  • P1— тратим время на загрузку (A + B) и C.
  • P2— тратим время на загрузку D.
  • P3— тратим время на загрузку (E + F).

Вдумчивый читатель сейчас возмутится и спросит: "А что, если ядра нет? Или ядро получается слишком маленьким?". Ответ: это легко решается вручную выделением 2-3 независимых групп со своими собственными ядрами. При желании задачу разбиения можно формализовать и получить точное машинное решение— но это обычно не нужно; руководствуясь простейшим правилом— чем больше ядро, тем лучше, — можно добиться вполне приличного результата.

Реализация на PHP

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

В качестве рабочего примера можно привести следующую функцию:

function cache_js(){
 $arrNewJS=array();
 $strHash='';
 $strGzipContent='';
 $intLastModified=0;

//проходимся по списку файлов
 foreach ((array)$this->scripts as $file){
	if (substr($file,0,5)=='http:') continue;
	if ($file[0]=='/') $strFilename=sys_root.$file;
	else $strFilename=sys_root.'app/front/view/'.$file;
	$strHash.=$file;

//читаем содержимое в одну строку
	$strGzipContent.=file_get_contents($strFilename);
	$intLastModified=$intLastModified<filemtime($strFilename) ?
		filemtime($strFilename) : $intLastModified;
}
 $strGzipHash=md5($strHash);
 $strGzipFile=sys_root.'app/front/view/js/bin/'.$strGzipHash.'.gz';

//проверяем, надо ли перезаписать gz-файл
 if (file_exists($strGzipFile) && $intLastModified>filemtime($strGzipFile) 
 || !file_exists($strGzipFile)){
 	if (!file_exists($strGzipFile)) touch($strGzipFile);

//используем функции встроенной в php библиотеки zlib для архивации
 	$gz = gzopen($strGzipFile,'w9');
 	gzputs ($gz, $strGzipContent);
 	gzclose($gz);
	}

//перезаписываем список на один файл
 $arrNewJS[]='js/bin/'.$strGzipHash.'.gz';
 $this->scripts=$arrNewJS;
}

Для CSS основные теоретические моменты описаны выше, а реализация даже несколько проще. Если использовать YUI Compressor, то решение будет совершенно одинаково (вычислили зависимости, склеили файлы, сжали, переименовали, сделали архив) для обоих типов файлов.

Дарья Билялова
Дарья Билялова
Россия
Елена Петрушевская
Елена Петрушевская
Россия, г. Нижневартовск