В последнее время стало модно объединять все внешние JavaScript-файлы вашего сайта в один большой, загружаемый один раз и навсегда. Это, скажем прямо, хорошо — браузер не делает сто миллионов запросов на сервер для отображения одной страницы 1, скорость загрузки повышается, пользователи счастливы, разработчики отдыхают.
Как всегда, в бочке меда есть ложка дегтя — в объединенный файл в этом случае попадает много того, что при первом запросе можно было бы и не загружать.2 (Здесь должна была быть ссылка на хабратопик с соответствующим обсуждением. Успешно потеряна.) Чаще всего для борьбы с этим предлагают выкидывать ненужные части руками... Лично у меня перспектива каждый раз перелопачивать несколько десятков (а то и сотен 3) килобайт JavaScript кода вызывает острое нежелание работать — а у вас?
Описание простейшего алгоритма разрешения зависимости между модулями.
Предложение первое: разобрать используемый вами фреймворк на составные части. JSON — отдельно, AJAX — отдельно, работа с DOM — отдельно, формы — отдельно. После этого задача «выкидывания ненужного» превращается в задачу «собери только нужное». Несомненный плюс — результат сборки стал меньше. Несомненный минус — если что-то из «нужного» забыто, все перестает работать.
Предложение второе: сохранить информацию о зависимостях между составными частями. (Формы используют функции DOM, JSON — AJAX и так далее.) На этом шаге забыть что-то нужное становится заметно труднее, а сборка превращается из увлекательной головоломки «...@#$, почему все перестало работать...» в рутинную и нудную операцию.
Предложение третье: сохранить информацию о том, какие именно модули нужны сайту в целом. Используется ли AJAX? Если ли формы? Какие-то необычные элементы управления?
Предложение четвертое: подумать, и заставить работать машину.
С формальной точки зрения, после того, как первый и второй шаг выполнены, у нас появляется дерево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 .... код
Выделить подобные метки из текстового файла не составляет труда.
Естественно, чтобы получить полное дерево зависимостей, надо будет пройтись по всем доступных файлам — но полное дерево обычно не нужно.
Затратив один раз кучу времени на формирование модулей и зависимостей между ними, мы экономим время каждый раз, когда хотим уменьшить объем загружаемого клиентов внешнего файла. Приятно. Но все-таки часть проблемы осталась — пользователь загружает весь JS-код, который используется на сайте за раз, даже если на текущей странице этот код не нужен.
Итак, мы оставили нового пользователя наедине с единственным JS-файлом, не включающем ничего лишнего. Стал ли при этом пользователь счастливее? Ничуть. Наоборот, в среднем пользователь5 стал более несчастным, чем раньше, а причина этому — увеличившееся время загрузки страницы.
... вы находитесь на воздушном шаре! ...
Время загрузки ресурса через HTTP складывается из следующих основных элементов:
В свою очередь, время загрузки страницы будет состоять из времени загрузки HTML-кода и всех внешних ресурсов: изображений, CSS и JS файлов. Основная проблема в том, что CSS и JS-файлы грузятся последовательно7. В этом случае общение с сервером выглядит так:
— запросили страницу — получили 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. Если ресурсы не объединять, получаем следующее:
Если мы слили воедино абсолютно все JS-модули сайта, получаем:
Результатом становится увеличение времени загрузки самой первой страницы, на которую попадает пользователь. При типовых значениях скорости/пинка мы начинаем проигрывать уже при дополнительном объеме загрузки в 23 килобайта.
Если мы объединили только модули, необходимые для текущей страницы, получаем следующее:
Каждая отдельно взятая страница при пустом кеше будет загружаться быстрее, но все они вместе — медленнее, чем в исходном случае.
Получаем, что слепое использование модного сейчас объединения ресурсов часто только ухудшает жизнь пользователя.
— можно ли проглотить бильярдный шар?
— можно, но, как правило, не нужно.
Конечно же, выход из сложившегося положения есть8. В большинстве случаев для получения реального выигрыша достаточно выделить «ядро» — набор модулей, используемых на всех (или по крайней мере на часто загружаемых) страницах сайта. Например, в нашем примере достаточно выделить в ядро ресурсы A и B, чтобы получить преимущество:
Вдумчивый читатель сейчас возмутится и спросит: «А что, если ядра нет? Или ядро получается слишком маленьким?». Спешу заверить — это легко5 решается вручную выделением 23 независимых групп со своими собственными ядрами. При желании задачу разбиения можно формализовать и получить точное машинное решение — но это обычно не нужно; руководствуясь простейшим правилом — чем больше ядро, тем лучше, можно добиться вполне приличного результата.
Как говорят на Хабре: «Каков же месседж этой статьи?» Ограничимся жалкими тремя пунктами:
1 При правильно настроенном кэшировании (наличии 'Expires') эти запросы будут отправлены только при первой загрузке страницы. Тем не менее, встречают по одёжке.
2 Например, весь Prototype целиком, вместо отдельно взятых функций '$' и '$$', ага.
3 Вы не пользуетесь JS-фреймворком (пусть даже велосипедом собственного изобретения)?
4 На самом деле не дерево, а DAG (Направленный Ациклический Граф) — я просто не знаю правильного русскоязычного термина. Ах да, если у вас в зависимостях получились циклы — что-то где-то было разбито неправильно.
5 с пустым кэшем, конечно.
6 подразумевается, что большая часть запросов — это GET-запросы с разумной длиной URL. Длина такого запроса примерно постоянна, и составляет ~450–600 байт.
7 по крайней мере, пока они находятся на одном хосте.
8 и, что приятно видеть из комментов, используется.
9 «У нас есть такие приборы — но мы вам про них не расскажем». Придется поверить на слово.