Объединение 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 (Направленный Ациклический Граф) — я просто не знаю правильного русскоязычного термина. Ах да, если у вас в зависимостях получились циклы — что-то где-то было разбито неправильно.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 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
                              И как кеширование поможет при первичной загруке?
                              См. примечание (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
                                              Хороший наглядный пример. Там на одной странице подключается 48 js-файлов и 12 css-файлов. Все, разумеется, кэшируются браузером и обновляются не так часто, но при каждой загрузке браузер вынужден делать 60 http-запросов, чтобы проверить, не изменился ли какой-нибудь файл.
                                              • 0
                                                Уй, зря вы это сказали. Если там при каждой загрузке запросы отправляются - это как раз пример "плохого" кэширования.
                                                • 0
                                                  Скорее всего кэширование работает нормально, меня смущает сам факт подключения 60 файлов без учета картинок.
                                                • 0
                                                  если настроены expires заголовки, за проверок вообще не будет, пока "срок годности" не выйдет.
                                        • –1
                                          и ещё. gzip вам и медленным клиентам в помощь.
                                          кеш и сжатие трафика, вот где сила!
                                          • 0
                                            Я про latenty на http запросах, вы мне про гзип. Не находите, что до того, как гзип сработает должен запрос прийти? Так вот в этом и есть тормоза, а не в скорости отдачи контента сервером.
                                            • 0
                                              latency в запросах CDN должен устранить.
                                              если нету его, то давайте зададимся вопросом, а сколько сил мы бросаем на решение столь второстепенной проблемы, вместо того, чтобы заняться чем-то действительно полезным?
                                              если мы говорим про какой-то конкретный случай, когда сайт на северном полюсе, а клиент на южном сидит через модем в 33600 Кб/с, и он единственный пользователь нашего сайта, тогда другое дело.
                                              • 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
                                                    Вот могу руку на отсечение дать, что преподаватель как-то обошел этот термин. Орграфы и деревья - естественно был. (Хотя я мог и просто забыть об этом за прошедшие пять лет).

                                                    Рассмотрим.

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