Пользователь
0,5
рейтинг
12 октября 2011 в 20:00

Разработка → Объединение javascript файлов из песочницы

Эпоха тёплого лампового WEB 1.0 давно прошла, и в последнее время мы имеем дело со страницами, которые кишат так называемой динамичностью. Динамичность может быть обеспечена при помощи JavaScript, VbScript и плагинами вроде Java, Flash, Silverlight. В этой статье я хочу затронуть одну из возможных оптимизаций web-сайта — объединение всех javascript файлов в один.

Зачем?


Основных причин две:
  1. Повышение скорости загрузки страницы.
  2. Снижение нагрузки на сервер.

Начнём с «повышения скорости загрузки». Зачастую web-сайт просто пестрит скриптами и их общее число может перевалить за 50. Впрочем, это уже будет «клинический случай». Но хотя бы 15-30 встречается регулярно. На каждый чихскрипт браузер посылает запрос и, в зависимости от ответа, либо грузит его полностью, либо забирает из кеша. 15 запросов это много. На это нужно время. Да, все современные браузеры загружают файлы параллельно, но это не повод их так эксплуатировать. В моём случае скорость загрузки страницы возросла в несколько раз.

Касательно снижения нагрузки на сервер — всё сложнее. Снижение числа запросов в любом случае улучшает ситуацию, но вот насколько — я сказать затрудняюсь, т.к. я не админ. Я полагаю, что для снижения нагрузки можно найти массу более простых и действенных решений. Возможно, это, так называемая, экономия спичек на фоне пожара. Но в качестве побочного эффекта — сгодится.

Как?


Полагаю, что основных способов всего два:
  • Обернуть все файлы анонимными функциями, которые нужно будет вызывать единожды, по мере необходимости. Либо писать модульный код, где каждый файл может содержать 1 или несколько модулей, которые сами по себе не запускаются.
  • Весь код каждого файла поместить в строку, которую eval-ить по первому требованию.

В первом случае мы избавляемся от ненавистного многими eval-а, но заставляем браузер компилировать неиспользуемый код. Во втором случае мы используем eval, заставляя браузер компилировать часть уже скомпилированного файла. Выглядит это всё примерно так:

// анонимные функции.
window.__js = {
	'engine': function(){ /* код */ },
	'some': function(){ /* код */ }
};

// строки
window.__js = {
	'engine': '/* код */',
	'some': '/* код */'
};

Но у меня не было выбора, т.к. используемый движок кишит кодом «сомнительного качества». Такой код я не могу обернуть в анонимную функцию, потому, что:

// файл 1
function some(){
	...
}

// файл 2
some();

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

function(){
	function some(){
		...
	};
}

Вовсе не приводит к window.some !== undefined; Функция some определяется в области видимости (scope) анонимной функции, а вовсе не window, как это было бы, если бы она была определена в отдельном файле. Решение этой проблемы нашлось в jQuery. Дело в том, что выполнить javascript-код в глобальной области видимости можно используя:

( window.execScript || function( data ) {
	window[ "eval" ].call( window, data );
} )( data );

В зависимости от браузера мы вызываем либо execScript, либо запускаем привычный нам eval, задавая ему this равным window. Такой подход используется в jQuery начиная с версии 1.6. В более ранних версиях создавался тег script, в который помещался нужный код, и этот скрипт прикреплялся к документу.

Сжатие и обфускация


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

Компоновка


Самая простая часть задачи. Алгоритм прост, как валенок:
  • Пробегаем по списку файлов (можно воспользоваться маской, к примеру: js/*.js).
  • Запоминаем дату изменения файла.
  • Сверяем её с датой создания уже сжатого файла, если таковой имеется.
  • Если файл обновлён, или же сжатой копии нет вовсе — сжимаем и сохраняем в отдельном каталоге (либо используем префиксы, например: min_#{file_name}.
  • Пробегаем по списку сжатых файлов, поочерёдно добавляя их содержимое в массив.
  • Сохраняем результат в «итоговый единый javascript-файл»

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

Отладка


Жизнь программиста была бы прекрасна, если бы не многочисленные баги, которые имеют привычку появляться не вовремя и хорошо прятаться. Тут наша с вами затея терпит крах по всем фронтам. Наш код нечитаем, firebug на нём виснет, и ошибки указывают невесть куда. К тому же большинство переменных имеют вид a, b, c. На помощь к нам приходит Сhrome. Дело в том, что он умеет «де-обфусцировать» код до вполне читабельного состояния (контекстное меню во вкладке Scripts). Например:

function oa(a) {
	var b = 1, c = 0, d;
	if (!D(a)) {
	    b = 0;
	    for (d = a[s] - 1; d >= 0; d--) c = a.charCodeAt(d), b = (b << 6 & 268435455) + c + (c << 14), c = b & 266338304, b = c != 0 ? b ^ c >> 21 : b;
	}
	return b;
}

Результат весьма далёк от оригинала, но такое уже можно хотя бы прочитать. К сожалению есть некоторые проблемы с постановкой точек останова и их срабатыванием. Но на безрыбье и рак рыба. Жить можно.

Финальный штрих


Если в конец кода, который будет пропущен через eval добавить конструкцию /* //@ sourceURL=#{name}*/, chrome покажет нам заданный #{name} в списке скриптов. К сожалению, в Firefox этот механизм у меня не заработал. Комментарий-обёртка мне понадобился для IE.



Использоване


Локально работать с «единым файлом» чертовски неудобно, поэтому можно написать примерно такой велосипед:

window.__js_ready = {};
		
function __include( name )
{
	// проверяем не выполнялся ли он ранее
	if( !__js_ready[ name ] )
	{
		__js_ready[ name ] = true;
		
		// выполняем скрипт
		if( ONLINE ) // "продакшн"
		{
			( window.execScript || function( data ) {
				window[ "eval" ].call( window, __js[ name ]  );
			} )( data );
		}
		else // локально
		{
			// добавляем скрипт любым понравившимся вам методом, например
			// через document.write
		}
	}
}	

// в html коде
<script type="text/javascript">	
	__include( 'engine' );
</script>
<script type="text/javascript">	
	// используем "engine"
</script>

Разумеется, вариантов реализации подключения скрипта может быть множество. Да и этот можно улучшить. Например, поставив движок сайта на «событийную основу». Т.е. выполнять какой-либо код только тогда, когда выполнился ряд условий, например: были загружены все требуемые модули.

Минусы


  • Необходимость компилирования «единого скрипта».
  • Трудность отладки.
  • Пользователь при первом запуске грузит сразу все скрипты сайта. Впрочем, минус надуманный в случае использования gzip.
Зубашев Степан @faiwer
карма
49,0
рейтинг 0,5
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

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

  • +4
    Трудность отладки не минус — нечего на продакшн-сервере изменения делать (научен горьким опытом)
    А в том же Yii можно настроить Script Mapping.
  • +1
    Мы используем такую штуку github.com/andrewdavey/cassette
    При компиляции в debug все скрипты идут отдельными файлами. А при компиляции в public — объединяются в один файл
    • 0
      Согласен — нужно по-любому делать механизм работы как со сжатой версией как и с несжатой. И желательно простой механизм переключения между ними. Иначе дебажить крайне неудобно.
  • +3
    Для вашего кейса я бы рекомендовал посмортеть StealJS. Если бы была возможность конвретировать код в модули, то тогда бы лучше подошел RequireJS.
  • 0
    обфускация не панацея, любой eval меняем на document.write и весь чистый js как на ладони.
    • 0
      обфускация мною и не рассматривается как панацея, не зря же я расписал про чудо по чудо опцию по «антиобфускации» в chrome :) Но она помогает слегка сэкономить трафик, привести js файл к одной строке (для удобства), и помешать другим использовать ваши наработки как свои. Обфускация скорее бонус, нежели суть.
  • +1
    использую Google Closure Compiler.
    Для debug'a есть ряд удобных ключей.
  • 0
    а у меня есть bitbucket.org/wkornewald/django-mediagenerator/src и github.com/miracle2k/webassets. второе, кстати, прикручивается не только к джанго, но и вообще к чему угодно, если не лень.
  • 0
    Сейчас уже не найду ссылку, но у какого-то зарубежного спеца по clienside'у читал, что уже не модно (читай эффективно) складывать все скрипты в один файл, а правильно подгружать нужные для разных страниц. Не зря же столько загрузчиков скриптов появилось. Впрочем, не знаю, насколько его мнение истина, т.к. он автор одного из загрузчиков :)
    • 0
      таки нашел ссылку. Не совсем точно сказал, как оказалось. Правильно — объединять скрипты, используемые вместе, в один, а уникальные для страниц грузить через асинхронные загрузчики.
      • +2
        Я думаю это очевидно, что таких групп файлов должно быть несколько в сложном приложении.
    • 0
      Тоже неплохой вариант, но специфический :) Пример применения — движок 10-15 файлов и 1-2 файла js-ки для текущего модуля. С другой стороны единый файл мне симпатизирует ещё тем, что если сервер тупит, то пользователю достаточно всего 1 раз загрузить страницу вашего сайта, и все остальные будут загружаться гораздо быстрее.
      • 0
        А что, если пользователю не нужны другие страницы? Охота, чтобы каждая страница загружалась быстро, независимо от того записан файл в кэш браузера или нет! имхо
        • 0
          > Охота, чтобы каждая страница загружалась быстро

          В случае единого скрипта даже первая страница загрузится быстрее, чем если js-ок будет 15-30 штук. В дальнейшем скорость значительно увеличится (в завис. от скорости клиента и размера файла). Живой пример, на момент отдельных js-ок сайт загружался от 30сек до 1мин 40сек, после объединения около ~20сек. Я полагаю, что даже в случае нормального быстрого сервера, не страдающего большой нагрузкой, прирост ( даже в случае первой загрузки ) страницы будет ощутим.
        • 0
          Хм, пардон. Я думал что отвечаю не вам. Если речь идёт о «базовые скрипты в одном файле» + скрипты текущего модуля, то ситуация получается специфичная для конкретного сайта. К примеру, если 80% кода занимает базовый файл, а 20% кода отдельные модули, то скорее всего, лучше поместить весь оставшийся код в основной файл, если хотя бы 50 на 50, полагаю, следует провести замеры производительности :)
  • +1
    Какое-то у вас неправильное представление о вебдванольности.
    Объединив, скажем, все js-файлы в один 200-килобайтный файл и подключив его в секции head, вы заставите браузер приостанавливать рендеринг страницы до тех пор, пока этот файл не будет загружен и распарсен. Учитывая, что на странице кроме этого обычно есть еще и картинки, то даже на канале 1мбит/с будет ощутимая задержка рендеринга (я даже не говорю про относительно маломощные клиенты типа телефонов и планшетов), когда ваш пользователь видит пустую, неотрендеренную страницу.

    Суровая реальность такова, что если скриптов много, то их нужно разбивать на модули и подгружать эти модули асинхронно, по мере того, как появляется потребность в модуле на странице.
    • –1
      Никто не мешает загружать js файл уже по окончанию рендеринга страницы. Парсинг и выполнение js не существенно будут отличаться в случае 30 файлов, вместо 1го. Итоговая загрузка страницы почти всегда будет быстрее в случае 1го файла (в некоторых случаях раз в 10 быстрее).

      > Суровая реальность такова, что если скриптов много, то их нужно разбивать на модули и подгружать эти модули асинхронно, по мере того, как появляется потребность в модуле на странице.

      У нас почти так и было (только синхронно). + у нас сильно тормозил и тупил сервак (не справлялся с нагрузкой), а это приводило к тому, что треть js-файлов отдавалось с 503, и либо отдельные части сайта не функционировали, либо он не работал полностью (в случае если модуль из главных). После объединения — сайт стал загружаться значительно быстрее (до 10 раз). В случае нормального сервера такого контраста, конечно, не будет, но выгода должна быть весьма существенной.

      В новых сайтах я применяю DataURi и объединение js, это уменьшает количество загружаемых файлов (не учитывая контентных картинок) до 4-6 файлов (вместо 70, к примеру). Конечно тут есть альтернативы, но мне данный способ показался значительно удобнее и в разработке (не люблю спрайты), и в публикации.

      В общем «суровые реальности» у нас с вами разные.
  • 0
    Самым мощным сейчас является Google Compiler. Вот полная дока на русском к нему от уважаемого Ильи Кантора javascript.ru/optimize/google-closure-compiler
  • 0
    При сборке проекта средствами Maven можно воспользоваться плагином maven-minify, который позволяет объединять и минифицировать js и css файлы, и при этом обладает широкими возможностями конфигурации.

    В целом же я позволю себе не согласиться с автором статьи в его подходе к организации единого файла. Использование eval я считаю неоправданным, если можно обойтись без него. В данном случае (если это позволяют сроки реализации проекта), разумнее было бы разбить код на модули, или хотя бы выделить под него свое пространство имен.

    Обратитесь к книге Стояна Стефанова JavaScript Patterns, там хорошо описаны шаблоны модуляризации кода.
    • 0
      > если это позволяют сроки реализации проекта

      В этом как раз и заключается проблема, ибо в противном случае лучше было бы переписать весь плохой код, соблюдая модульность :)
  • НЛО прилетело и опубликовало эту надпись здесь
  • 0
    Стоит испонить eval(src + "\n//@ sourceURL=" + name);
    И что в хроме, что в ФФ вы получите именованный скрипт.
    Не надо было его в /*… */ оборачивать и все
    • 0
      Так я изначально и пробовал, но не срабатывало. Погуглив я нашёл массу тем с подобной проблемой на англоязычных форумах, и ни 1го ответа. Только предположение, что мол виноват многострочный js-код. А в комментарии я поместил эту конструкцию уже позже, столкнувшись с неадекватным поведением IE :)
  • +1
    Ну вообще-то еще есть такая штука как Google Minify — она объединяет и минифицирует и сжимает в gzip файлы «налету» при первом обращении к ним, и кеширует сжатый итоговый файл на диск и в дальнейшем отдает только его, пока какой-либо из исходных файлов не изменится.
    В итоге вы работаете с отдельными неминифицированными и несжатыми файлами, а в браузер отдается один объединенный файл, минифицированный и сжатый.
    У Google Minify есть плагины под некоторые распространенные CMS — например WordPress, Zend Framework, Yii, HostCMS, с остальными системами Minify можно использовать в чистом виде, с помощью входящего в комплект билдера
  • 0
    в качестве дополнительного плюса можно добавить еще и решение проблемы с зависимостями
  • 0
    Есть же мягкий и пушистый minify

    Вкратце: делаем ссылку вида /minify/?f=скрипт1.js, скрипт2.js, скрипт3.js, скрипт4.js… и вставляем ее в HEAD. Minify сам смотрит изменения файлов, кэширует и выдает весь код в виде одного сжатого файла. А разработчик продолжает работать с отдельными файлами как и раньше. Поддерживает кэширование на стороне клиента и прочие прелести. В качестве бонуса делает то же самое и с CSS.
    • 0
      Упс, не заметил комментарий тов. jameskotov, пардон
      • 0
        В комментариях приведено множество других решений, в том числе и лучших, чем предложенный мною. Мне же было интересно написать это самому :)

        По поводу minify, у меня возник вопрос — поддерживается ли раздача файла как статику? Без выполнения проверяющих скриптов?
        • 0
          Эт понятно )

          Сразу как статику — нет, в любом случае вызывается .php. Впрочем, это можно достаточно просто прикрутить самому — расковырять где у него лежит файл кэша и прописать на него редирект в .htaccess (если не существует — вызов .php). Но это, имхо, уже излишне. Учти что Minify умеет кэшировать на стороне клиента, так что обычно вся загрузка всех .js пользователю уложится в один запрос, в ответ на который придет «304 Not modified» заголовок и все. В моем случае — ~0.09 сек.
          • 0
            В целом отличный вариант :) минимум геморроя. Но подойдёт не всем, пример:
            1. Распределение js на несколько серверов. В зависимости от реализации могут возникнуть сложности.
            2. В моём случае js-ки подключаются в шаблонах, а не в head-е, и для применения minify пришлось бы перелопатить уйму кода :)
            • 0
              1. Все что на других доменах однозначно нужно или копировать к себе или подключать отдельно — через Minify будет плохо.
              2. Может, как вариант, выловить и объединить самые отъявленные .js?
              • 0
                ИМХО, при этом теряется вся прелесть подхода (отсутствие геммороя, и необходимость что-то менять). К прошлому посту ещё два варианта:

                1. ajax-сайт. Пилю себе на локалхосте stand-alone блог, с рассчётом на full ajax. js-ки дёргаются на событийной основе. Из вариантов: либо грузить всё сразу, либо всё по отдельности. Ну или основное сразу, остальное по отдельности. Лично мне симпатизирает только первый вариант (+- 30 Мб, ИМХО, ерунда).

                2. большое количество комбинаций файлов (завалы кешей на все случаи жизни, низкая скорость отдачи ранее невостребованной комбинации).
                • 0
                  1. Я очень слабо себе представляю блог, которому действительно необходимо было бы что-то подгружать событийно — проще и правильнее все подгрузить все сразу. Но для абстрактной задачи я бы посоветовал прогонять через Minify исходно загружаемые вещи (фреймворк + общий js-код, к примеру), а дальнейшее дергать как отдельные файлы — они будут раздаваться как статика и уж никак не повлияют на время загрузки страницы. Для красоты картины на боевом сервере их можно прогнать через любой JS-компрессор, но это уже мелочи.

                  2. Иногда лучше пристрелить, чтоб не мучалось. Если в проекте такой бардак, то врядли стоит тратить ощутимое время на допиливание частностей.
                  • 0
                    1. Согласен это излишне, мне нужно просто для самообучения :) Касательно предложенного вами способа я уже писал выше товарищу Sytrus-у. Это уже следующий уровень оптимизации, который далеко не всегда и не везде нужен.Лично я бы просто не стал заморачиваться.

                    2. Почему же бардак? имхо, просто очень крупный проект может иметь по такой схеме несколько сотен комбинаций без всякого бардака. Поэтому придётся собирать «базовый комплект», а склеивать уже дополнительные. Соответственно проще сделать единый файл.

                    На мой взгляд, все эти премудрости излишняя растрата времени. Либо выбрать простой вариант с minify, либо один из вариантов посложнее:
                    1. «базовый комплект» скриптов + отдельно загружаемые частные скрипты страницы
                    2. все скрипты сразу

                    Правильный выбор зависит от ситуации.
      • 0
        Хм, только что дошло: получается пользователь регулярно будет грузить одни и теже файлы в разных их комбинациях? К примеру:
        1. первая страница: main.js, jquery.js
        2. вторая страница: main.js, jquery.js, some.js

        Minify хитро всё учтёт и загрузит только some.js или всё заного? И что будет если в комбинации, наоборот, будет не хватать какого либо файла?
        • 0
          Это будут отдельные запросы, которые будут отдельно кэшироваться и отдельно загружаться. Именно поэтому правильнее гонять через Minify один и тот же набор файлов для каждой страницы.

          Есть два варианта:
          а) На всех страницах запрашивать через Minify jquery.js и main.js, а там где нужно отдельным файлом some.js
          б) На всех страницах запрашивать через Minify все файлы.
          Можно комбинировать оба варианта, загружая для конкретной страницы несколько файлов через minify (исключая общие для всех страниц, которые были загружены выше по коду).

          С точки зрения быстродействия и удобства работы в абстрактной задаче второй вариант предпочтительнее — меньше головной боли разработчику, быстрее (в общем виде) работает, больше порядка в коде.
          • 0
            «один и тот же набор файлов для каждой страницы» = для всех страниц сайта. Пятница, однако.
          • 0
            Солидарен. Примерно тоже самое отписал комментом выше :)

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