23 июля 2008 в 13:00

Объединение JS-файлов 2.0 (1/2)

В последнее время стало модно объединять все внешние 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-код, который используется на сайте за раз, даже если на текущей странице этот код не нужен.
(Продолжение следует...)
1 При правильно настроенном кэшировании (наличии 'Expires') эти запросы будут отправлены только при первой загрузке страницы. Тем не менее, встречают по одёжке.
2 Например, весь Prototype целиком, вместо отдельно взятых функций '$' и '$$', ага.
3 Вы не пользуетесь JS-фреймворком (пусть даже велосипедом собственного изобретения)?
4 На самом деле не дерево, а DAG (Направленный Ациклический Граф) — я просто не знаю правильного русскоязычного термина. Ах да, если у вас в зависимостях получились циклы — что-то где-то было разбито неправильно.
bkonst @bkonst
карма
47,5
рейтинг 0,0
Похожие публикации
Самое читаемое Разработка

Комментарии (70)

  • 0
    Интересно. Ушёл пробовать.
  • 0
    Спасибо. А я как-то и не задумывался о таких вещах.
    Просто мы используем jQuery в качестве фреймворка, а при работе с какими-то специфичными вещами, обычно формируем отдельный js файлик с необходимыми функциями...

    Думаю, стоит попробовать и такой подход тоже...
  • 0
    можно ещё и автоматическую сборку наворотить ;)
    семпл на пхп:

    $add_js = new AddJs();
    $add_js -> AddRoot('dom.js');
    $add_js -> AddRoot('pane.js');
    $add_js -> AddBranch('transition.js');
    $add_js -> Optimize = true;
    $add_js -> Gzip = true;
    $add_js -> GetJsFile();

    после чего скрипт делает всю рутину в стиле обхода дерева, выделения юников из массива..
    в конце он выплёвывает собранный воедино, оптимизированый по размеру и гзипнутый js файл :)
    имхо удобно..
    • 0
      В принципе, оно более-менее так и сделано.
    • 0
      Ну и еще ссылку на source было бы неплохо приложить.
      Идея неплохая.
  • 0
    Хм идея правильная, тоже задумался, что пора объединить. Вот тока думаю если конечный скрипт будет весить 100 кб и больше, то вы сделаете только хуже, ибо маленькие файлы быстрее загружаюся. Для таких целей и картинки разрезают на маленькие.
    Щас еще подумал, может маленькие клипарты, которые ставятся на бекграунд тоже объединить, где возможно :) пойду поэксперементирую.
    • 0
      Большой файл всегда будет загружаться быстрее, чем несколько его частей, за счет отсутствия накладных расходов на установку соединения. С картинками ситуация немного другая - если резать картинки "правильно", то каждый из кусков можно сжать лучше, чем картинку в целом.
      • +1
        Плюс к этому, если имеется сжатие gzip'ом, то большой файл сожмется лучше, чем куча маленьких.
  • 0
    Более-менее готовое решение: http://www.seanalyzer.ru/projects/jsproc… (не пробовал, но идея нравится).
    • 0
      Там щас совсем страшненькая, но рабочая, версия валяется. Щас есть отрефакторенная, но не доходят руки выложить.

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

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

      Вообще, в JsProcessor'е директива //#include(_once) не единственная вкусность. Есть еще //#define, с помощью которого можно не только определять константы (по типу С), а еще и брать переменные из php и из окружения (этакий шаблонизатор, не портящий js-код). А для больших файлов, которые, тем не менее, логически разбивать некуда (методы для работы со строками, например) есть директива //#label, позволяющая отмечать участки кода и подгружать затем только нужный участок.

      И все это работает на реальном сайте, напичканном js-скриптами, и проверено временем. Вот только бы еще //#if реализовать для условной сборки.
      • 0
        Ага, я про остальные вкусности тоже читал. Сейчас столкнулся с необходимостью такой фичи как получение данных в js их какого-то "более внутреннего" источника в приложении, нежели переменные окружения.

        Например, в приложении есть конфигурационный файл наподобие Zend_Config, который должен быть доступен и на сервере (в PHP), и на клианте (в JS). Хотелось бы, чтобы при динамической сборке скрипта можно было вставлять в него такие вот данные.

        Идея загружать эти данные асинхронно при необходимости в виде json, например, не очень нравится. Может быть и на этот счет есть соображения?
        • 0
          В php
          $jsprocessor->assign('var', 'value');

          В js
          //#define FROM_PHP $var
          alert(FROM_PHP);

          JavaScript в результате
          alert('value');
          • 0
            Да, только не понятно, как определить, что для этого js-файла (там где используется //#define FROM_PHP $var) процессор должен выполнить assign в эту $var, а в остальных — нет. Или нужно этот assign писать везде?
            • 0
              Хм, честно говоря, не понял вопроса.
  • +1
    В свое время долго думал над этим.
    В итоге сейчас пользую jQuery. И гружу отдельные плагины к нему, если они нужны на данной странице. Считаю, что с js у меня на проектах все более-менее хорошо.
  • 0
    имхо, сама идея загрузки всего js сразу не совсем удачна и своевременна. все-таки не все еще обладают толстым каналом, чтоб грузить такой проект без особого дискомфорта. знаю нескольких людей, которые именно из-за этого отказались от gmail. как один из путей решения представляется все-таки разбить код на несколько модулей, а с самого начала предлагать грузить скрипты только для наиболее посещаемых разделов.
    • 0
      Угу, это я собирался разобрать во второй части. Много за раз написать трудно.
  • 0
    не считаю это правильным...
    как сказали встерчают по одежке - и если у меня на главной странице подгрузится скрипт в 150 кб (а именно примерно столько жабаскрипта у нас подгружается в среднем) - то юзеры задолбаются ждать

    мы разбили скрипты которые подгружается только там где нужны - а на большое количество запросов - нам откровенно плевать - сервера выдержат :)

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

    JS кешируются. По этому, смысла в "объеденении JS" не вижу - только если мы хотим загрузить сервер лишней работой. А пользователь, если даже и получит меньшую скорость, то это проценты - миллисекунды и то, в первую загрузку страницы. (далее данные будут прокешированны)

    Я надеюсь, что автор во второй части приведет рабочий пример (с кешем и без) :)
    • 0
      Как выше пошутил Dargin, может и картинки объеденить? :)
      Странно, что этим никто не занимается.
      • 0
        Объединением картинок занимаются. Ключевое слово - CSS Sprites.

        Выигрыш во времени при объединении файлов уже много раз разбирался, но поскольку возникли вопросы - подброшу статистику.
    • 0
      Да, ещё одно замечание. Cервер, конечно же, этим каждый раз заниматься не будет. Это делается один раз после обновления какого либо из JS-файлов.
      • 0
        Согласен. На счёт статистики - ждём.
        Спасибо.
  • 0
    стоит один раз написать диспетчер вызовов и обращаться к внешним модулям только через него, например:

    component("myComponent").call("methodName", {arg1:"val1", arg2:"val2"});
    • 0
      Зачем?
  • 0
    да сколько можно уже придумывать подобные грабли?
    научитесь правильно кешировать файлы на стороне клиента.
    • 0
      И как кеширование поможет при первичной загруке?
      См. примечание (1).
      • 0
        да никак! если кеша нет, то его нужно заполнить.
        зато все остальные танцы с бубном отпадают после одного запроса.
        • +1
          Простите, но
          1) Описанная мной схема примитивна, как апельсин, и до "танцев с бубном" ей как до луны. Она не предназначена для использования вместо кеширования.
          2) Новые пользователи, по вашему, должны сосать лапу, пока страничка грузится в первый раз?
          2) Кеш, он такая странная штука, которая имеет свойство периодически опустошаться. При падении браузера например. Почему при этом должны сосать лапу обычные пользователи?

          Кроме того, я специально в примечании упомянул кеширование; таком образом, ваше предположение, что я не умею правильно кешировать файлы, мягко говоря, необоснованы. Хотелось бы услышать ваши извинения. "Я был чрезмерно горячен" или "Я был невнимателен" сойдёт.
          • 0
            возможно, "Я был чрезмерно горячен", прощу прощения. "Я был невнимателен" и ни в коем случае не хотел сказать, что вы не умеете кешировать.
            просто создавая дополнительные зависимости в своём проекте, вы обрекаете программистов на новые ошибки.
      • 0
        если выполнить ваше первое примечание, то ничего другого вообще не нужно.
    • 0
      :) а если у нас 99% пользователей заходят один-единственный раз? :)
      • 0
        мы сами себе создаём проблему - усложняем систему, объединив файлы. для меня, как для разработчика важна простота компонентов. мне не надо сидеть и дебажить скрипт, который включает все мои четыре файла. если пользователей много, значит сайту нужен CDN и нечего головы ломать, заставлять бедный апачик отдавать статику.
    • 0
      При обновлении проекта кеш принудительно очищается и все файлы надо тянуть заново. При частых обновлениях файлы будут перетягиваться заново часто. HTTP-запросы на канале с большой latency съедают львиную долю времени загрузки. ADSL соединение может быть 5mbit, а страница с несколькими десятками файлов будет грузиться десятки секунд. Объединение рулит в этом случае.
      • 0
        при частых обновлениях, советую научиться разбивать файлы таким образом, чтобы количество новых файлов было минимальным. частые обновление - знак того, что код не стабилен. тестируйте больше перед продакшеном.
        • 0
          Нет, частые обновления это признак того, что проект живёт и развивается.
          • 0
            вот вам примеры живых и развивающихся проектов с нечастыми обновлениями: Prototype, script.aculo.us, jQuery.
            а если вы обновляете какую-то свою библиотеку c вашего сайта каждый день - поверьте, у вас проблемы.
            • 0
              А что собирать надо только библиотеки? Вот вам пример живого, развивающегося сервиса: http://wow.ya.ru

              И не один день, а раз в неделю, этого достаточно, чтобы задуматься о сборке.
              • 0
                по вашему, я должен каждую неделю тянуть заново 10-ть в 1-м вместо пары-тройки файлов?
                • 0
                  Для сброса кеша используется foo.js?build=N.

                  Как вы опеределите в автоматическом режиме, что эта пара-тройка файлов изменилась в этом билде, в отличие от того, что сейчас в продакшене? Проще всего проставить новый build=N для всех файлов.
                  • 0
                    у меня в имени файла есть его версия.
                    меняется файл, меняется и его имя. значит клиент запросит новый файл.
                    • 0
                      Вы это руками делаете или в автоматическом режиме?
                      • 0
                        да, вручную - в два удара: имя файла и в одном php-файле его новое имя.
                        • 0
                          А теперь представьте, что файлов у вас сотни и разработчиков десяток.
                          • 0
                            это не многое меняет. каждый занимается своим файлом и заботится о том, чтобы он был правильно прописан.
                            • 0
                              Всё ясно. Теоретик.
                              • 0
                                вам виднее, видимо :)
                                ни один нормальный tech leader не даст вам делать 100 файлов на проекте, если это не ExtJS какой-нить :)
                                • 0
                                  Я и говорю, теоретик.
                                  • 0
                                    этот теоретик однажды работал в стартапе, где каждый из 6-ти программеров писал свои собственные обработчики для AJAX, когда уже существовали кроссбраузерные JS-библиотеки.
                                    поверьте, у меня есть опыт.
                                    • 0
                                      В огороде бузина, а в Киеве дядя.

                                      Как соотносится ваш страшный опыт работы с программистами, не умеющими использовать существующие инструменты, с неиспользованием инструментов вообще?
                                      • 0
                                        buena suerte, mi amigo
              • 0
                Хороший наглядный пример. Там на одной странице подключается 48 js-файлов и 12 css-файлов. Все, разумеется, кэшируются браузером и обновляются не так часто, но при каждой загрузке браузер вынужден делать 60 http-запросов, чтобы проверить, не изменился ли какой-нибудь файл.
                • 0
                  Уй, зря вы это сказали. Если там при каждой загрузке запросы отправляются - это как раз пример "плохого" кэширования.
                  • 0
                    Скорее всего кэширование работает нормально, меня смущает сам факт подключения 60 файлов без учета картинок.
                • 0
                  если настроены expires заголовки, за проверок вообще не будет, пока "срок годности" не выйдет.
      • –1
        и ещё. gzip вам и медленным клиентам в помощь.
        кеш и сжатие трафика, вот где сила!
        • 0
          Я про latenty на http запросах, вы мне про гзип. Не находите, что до того, как гзип сработает должен запрос прийти? Так вот в этом и есть тормоза, а не в скорости отдачи контента сервером.
          • 0
            latency в запросах CDN должен устранить.
            если нету его, то давайте зададимся вопросом, а сколько сил мы бросаем на решение столь второстепенной проблемы, вместо того, чтобы заняться чем-то действительно полезным?
            если мы говорим про какой-то конкретный случай, когда сайт на северном полюсе, а клиент на южном сидит через модем в 33600 Кб/с, и он единственный пользователь нашего сайта, тогда другое дело.
            • 0
              Мы решаем конкретные проблемы конкретных пользователей.
              • 0
                а тоже ценю каждого своего пользователя. но гонясь за всеми, вы не угодите всем.
  • 0
    • 0
      Немного не то. "Фишка", которую я пытаюсь продвинуть - не само по себе слияние, а учёт зависимостей при слиянии.
  • 0
    ИМХО большинство пользователей сначала попадают на заглавную страницу. Сделав заглавную в обычном дизайне, но без JS (или практически), можно показать пользователю "часть" сайта намного быстрее, чем если заглавная подгружает скрипты. Сплешскрины в начале скорее всего и были наделены такой идеей, которая затем была забыта дизайнерами.
  • 0
    Похожий подход я использую в своих проектах, отличие в том, что у меня в описании зависимости всего 2 уровня и этого достаточно т.к. результирующий список строится в несколько проходов, потом полученный скрипт пропускается через упаковщик скрипта, а затем упаковывается gzipом и помещается в кэш на сервере и затем отдается всем, кто желает получить такой же набор скриптов.
    На практике сталкивался с тем, что для некоторых скриптов важен порядок помещения в единый файл скриптов, поэтому зависимости и порядок описываю отдельным файлом.
    Так же оставил возможность простым переключением использовать скрипты классическим способом (например при отладке). Таким же образом обрабатываю и CSS файлы. Сейчас решаю вопрос с версионностью наборов скриптов. Если интересно, попробую найти время и описать поподробнее.
  • –1
    Статья стала последней каплей. Отодвинул все дела, засучил рукава да и объединил все js файлы в один, в своём проекте.

    Спасибо!
    • 0
      в js framework-е YUI есть компонент yuiloader
      оттуда можно почерпнуть много идей по теме подгрузки зависимостей
      на его основе в своём проекте делал автоматическое подключение нужных скриптов и css-файлов
      • 0
        немного не туда коммент написал *confused*
  • 0
    Идея неплохая, лично я пользуюсь малоизвестным но чень удобным js фреймворком сделанным именно по такому принципу: каждая функция реализована в виде пары — js файл с кодом + xml файл с зависимостями. Также есть отдельная программа которая собирает в единственный файл только необходимые функции
    Если заинтересовало ищите здесь http://cross-browser.com/
  • 0
    DAG - ориентированный ациклический граф; на лекциях по дискретной математике спать просто вредно :)

    и кстати, почему бы не рассмотреть "системы модулей" в существующих JS-фреймворках?
    • 0
      Вот могу руку на отсечение дать, что преподаватель как-то обошел этот термин. Орграфы и деревья - естественно был. (Хотя я мог и просто забыть об этом за прошедшие пять лет).

      Рассмотрим.

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.