Опубликован: 16.02.2009 | Доступ: свободный | Студентов: 1437 / 138 | Оценка: 4.26 / 4.17 | Длительность: 16:10:00
ISBN: 978-5-9963-0024-2
Лекция 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, то решение будет совершенно одинаково (вычислили зависимости, склеили файлы, сжали, переименовали, сделали архив) для обоих типов файлов.