CSS и JS в одном файле

Раз уж я затронул тему снижения числа коннектов к серверу при загрузке сайта, то было бы уместо вспомнить незаслуженно забытую технику — объединение JS и CSS в одном файле.

Насколько я знаю, первым эту технику исследовал некий ShivaP в статье «Combine CSS with JS and make it into a single download!» ещё четыре года назад.

Идея такова: в файле специальными ухищрениями помещается и код JavaScript, и код CSS, файл подключается два раза — один раз через тег SCRIPT, второй раз через тег LINK (как CSS), браузер грузит файл один раз, а второй раз берёт его уже из кеша.

Автор проверил свою идею на двух браузерах — IE и FireFox, на том и остановился.

Я проверил эту идею на гораздо бо́льшем числе браузеров (обнаружил проблемы на некоторых «Хромах» и исправил), а так же немного её видоизменил, разрешив использовать многострочный CSS без особых неудобств. Последнее нужно персонально мне, так как у меня нет возможности прогонять свой CSS через оптимизаторы и вытягивать таким образом код CSS в одну строку, а я хочу использовать эту технику у себя на сайте.

У техники есть ограничение, которое не портит много крови, но о нём необходимо знать — в тексте CSS и JS не должна встречаться конструкция «*/». Если в CSS достаточно просто выкусить все комментарии («*/» там, можно считать, нигде больше встретиться не может), то в JS за этим придётся следить, так как такая комбинация вполне может встретиться, в частности, в регулярных выражениях. Например: var regexp = /smth*/g;

В таких случаях звёздочку необходимо прятать (например, заменять её в регулярных выражениях на \x2A или разбивать строку: "smth*" + "/").

Как ShivaP, первый исследователь этой техники, решил проблему размещения JS и CSS в одном файле можно почитать в оринальной статье, я же поступил несколько иначе, но сходным образом. Конструкции размещаются так, чтобы JS оказывался в комментариях CSS и наоборот. Я похожими вещами уже занимался неоднократно.

Мой код выглядит следующим образом (я поставил рядом две картинки, чтобы можно было сравнить как один и тот же код воспринимается интерпретатором JavaScript и парсером CSS): JS и CSS (15.58КиБ) Как видите, принцип довольно несложен.

Теперь несколько слов о том как это всё подключается:
<link type="text/css" rel="stylesheet" href="core.jscss" />

<script type="text/javascript" src="core.jscss#2"></script>
Обратите внимание на «#2» после core.jscss, этот хак позволяет обойти странное поведение некоторых версий «Хрома» — без него браузер не делал попыток подгрузить файл ещё раз и JavaScript не выполнялся.

Немаловажно так же, что заголовок «content-type» должен быть выставлен в «*/*», чтобы избежать проблем с FireFox 2.

У себя на сайте я буду использовать скрипт наподобие нижеприведённого, чтобы разом разрешить все проблемы и выставить правильные заголовки.
<?
ob_start();
$dir     = 'media/';
$expires = 10 * 365 * 24 * 60 * 60; // 10 years

$files = explode(',', str_replace('..', '', $_SERVER['QUERY_STRING']));
$css = $js = array();

foreach ($files as $file) {
   switch (pathinfo($file,  PATHINFO_EXTENSION)) {
       case 'css':
          $css[] = $dir . $file;
          break;

       case 'js':
          $js[] = $dir . $file;
          break;
    }
}

$css = @array_map('file_get_contents', array_unique($css));
$js  = @array_map('file_get_contents', array_unique($js));

$css = implode($css);
$js  = implode($js, ';');

// removes /* comments */ from css
$css = preg_replace('!/\*.*?\*/!s', '', $css);

// removes only first /* comment */ from js
$js  = preg_replace('!^/\*.*?\*/!s', '', $js);

echo <<<JSCSS
// /*
$js
"*/{"/*"}
$css
*/
JSCSS;

$len  = ob_get_length();
$txt  = ob_get_clean();
$etag = '"' . sha1($etag) . '"';

header("Content-type: */*; charset=utf-8");
header("Expires: " . gmdate('D, d M Y H:i:s \G\M\T', $_SERVER['REQUEST_TIME'] + $expires));
header("Cache-control: max-age=$expires, public");
header("Content-length: " . $len);
header('ETag: ' . $etag);

if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
    list ($hash) = explode(';', $_SERVER['HTTP_IF_NONE_MATCH']);

    if ($hash == $etag) {
        header('HTTP/1.1 304 Not modified, thanx for yr question');
        exit;
    }
}

echo $txt;
Формат вызова простой: скрипту через запятую перечисляются файлы, которые нужно объединить и подключить (файлы берутся из папки «media», см. переменную «$dir»):

/util/js+css.php?core.js,core.css,another.css,another.js
19 апреля 2011 19:48

Виктор (инкогнито)
19 апреля 2011, 21:07

Евгений, спасибо за статью!

Вы считаете это готовым к использованию в боевых проектах?

bolk (bolknote.ru)
19 апреля 2011, 22:13, ответ предназначен Виктору

Пока не знаю, собираюсь попробовать на блоге и посмотреть не будет ли проблем.

bolk (bolknote.ru)
19 апреля 2011, 22:15

Кстати, есть страничка, где можно проверить как это работает под вашим браузером: http://bolknote.ru/files/exp-jscss/

reon (reon.livejournal.com)
19 апреля 2011, 23:55, ответ предназначен bolk (bolknote.ru):

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

bolk (bolknote.ru)
20 апреля 2011, 00:08, ответ предназначен reon (reon.livejournal.com):

C одной стороны идея неплохая, на один коннект меньше, но с другой стороны - концептуально неверно смешивать оформление и код.
Он и не смешан. Оформление в CSS, код в JavaScript. То, что это приходит в одном файле к клиенту, концептуально ничего не меняет.
А не проще все держать в отдельных файлах и подключать просто серверным инклюдом - тогда вообще один-единственный запрос будет, если конечно не считать картинок и прочих файлов с контентом?
Если сайт состоит из одной страницы, так и надо делать, в остальных случаях так делать нельзя. Вынесенный в отдельный файл CSS и JS снижает вес HTML-страницы. Когда клиент перейдёт на неё, ресурсы (JS и CSS) будут загружены из кеша.

astur (kozlov.am)
20 апреля 2011, 02:32

А что, если грузить честный js, который бы сам проставил css-свойства? Я понимаю, что минусы есть, но интересен именно твой взгляд.

http://orcinus.ru (инкогнито)
20 апреля 2011, 05:52

Интересно, но в некоторых проектах Яндекса (уже не помню где копался), огромные портянки ЦСС шли прямо в коде самой странички, не подключаясь извне.

И еще, кажется у тебя на сайте была статья о подключении дополнительной .js из подключаемого .js файла. Выглядело как обычный вывод строки. Ведь ни что не мешает написать скрипт который оформит два document.write, и модно даже многострочники оформлять, заменяя перевод строки на /n.

agonych (agonych.livejournal.com)
20 апреля 2011, 05:52, ответ предназначен astur (kozlov.am):

1. Вес такого файла в разы превысит выйгрыш от одного запроса
2. На слабых компьютерах такой код существенно увеличит время полной загрузки страницы
3. Головная боль с поддрежкой и разработкой.

http://orcinus.ru (инкогнито)
20 апреля 2011, 05:55

Мне кажется или у тебя в строчке "header('HTTP/1.1 304 Not modified, thanx for yr question');" есть опечатка.

bolk (bolknote.ru)
20 апреля 2011, 10:00, ответ предназначен http://orcinus.ru

Вроде всё верно написано, где опечатка?

bolk (bolknote.ru)
20 апреля 2011, 10:03, ответ предназначен http://orcinus.ru

Интересно, но в некоторых проектах Яндекса (уже не помню где копался), огромные портянки ЦСС шли прямо в коде самой странички, не подключаясь извне.
Я ничего про это сказать не могу, просто не знаю о таких случаях.
И еще, кажется у тебя на сайте была статья о подключении дополнительной .js из подключаемого .js файла. Выглядело как обычный вывод строки. Ведь ни что не мешает написать скрипт который оформит два document.write, и модно даже многострочники оформлять, заменяя перевод строки на /n.
Производительность такого решения ниже.

bolk (bolknote.ru)
20 апреля 2011, 10:05, ответ предназначен http://orcinus.ru

Производительность ниже, так как скрипт сначала придётся разместить в DOM, потом проинтерпретировать. Кроме того, это начисто убивает возможность использования defer при необходимости.

Чистяков Денис (инкогнито)
20 апреля 2011, 14:07

Firebug 1.7 в FF4 отказывается показывать текст сценария на странице с примером, т.е. все отрабатывает как надо и случайное число вставляется, но посмотреть и подебажить нельзя (
«Содержимое, находящееся по указанному URL не является текстом: http://bolknote.ru/files/exp-jscss/js+css.php?1.js,1.css» пишет.

Чистяков Денис (инкогнито)
20 апреля 2011, 14:09

С CSS-ом все ок.

maxim-zotov (инкогнито)
20 апреля 2011, 16:02

Включить на сервере поддержку KeepAlive и забыть про эти хаки, как страшный сон.

bolk (bolknote.ru)
20 апреля 2011, 17:41, ответ предназначен maxim-zotov

Keep-Alive включен и он тут совершенно мимо.

bolk (bolknote.ru)
20 апреля 2011, 17:42, ответ предназначен maxim-zotov

Вы статью-то прочитайте внимательно.

maxim-zotov (инкогнито)
20 апреля 2011, 18:08, ответ предназначен bolk (bolknote.ru):

На сайте bolknote.ru keepalive выключен. Он тут не мимо, поскольку ваша цель уменьшить число соединений. Суть KeepAlive как раз в том, чтобы делать несколько запросов в одном соединении.

Увы, за 12 лет существования HTTP/1.0 производители браузеров и веб-серверов так и не довели до ума http-pipelining (http://en.wikipedia.org/wiki/HTTP_pipelining), с ним было бы вообще хорошо, по сетевому обмену это полный аналог вашего извращения, только прямой и стандартный, без головоломок "напиши хело ворлд на 10 языках в одном файле". Как мозговая зарядка нормально, как технический прием в реальной работе - ниже плинтуса.

maxim-zotov (инкогнито)
20 апреля 2011, 18:11, ответ предназначен maxim-zotov

HTTP/1.0
Опечатка, должно быть HTTP/1.1

maxim-zotov (инкогнито)
20 апреля 2011, 18:21, ответ предназначен maxim-zotov

На сайте bolknote.ru keepalive выключен
Извините, наврал. Включён.

SiMM (инкогнито)
20 апреля 2011, 20:31, ответ предназначен maxim-zotov

Он тут не мимо, поскольку ваша цель уменьшить число соединений.
Цель не уменьшить количество соединений, а уменьшить время загрузки - соединения тут лишь постольку поскольку их количество в браузере ограничено.

maxim-zotov (инкогнито)
20 апреля 2011, 22:33, ответ предназначен SiMM

уменьшить время загрузки
Ну да, ну да. Подмена отдачи статического файла вызовом php-скрипта это вообще изощрённое убийство сервера.

Что до времени, вот честно скажу, 5+5 миллисекунд значительно меньше 35 миллисекунд.

$ http_ping http://bolknote.ru/files/exp-jscss/js+css.php?1.js,1.css
153 bytes from http://bolknote.ru/files/exp-jscss/js+css.php?1.js,1.css: 35.912 ms (2.01c/33.867r/0.035d)

$ http_ping http://bolknote.ru/pavatar.png
3939 bytes from http://bolknote.ru/pavatar.png: 5.874 ms (2.113c/3.525r/0.236d)

desh (инкогнито)
20 апреля 2011, 23:45, ответ предназначен maxim-zotov

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

bolk (bolknote.ru)
22 апреля 2011, 07:28, ответ предназначен maxim-zotov

Подмена отдачи статического файла вызовом php-скрипта это вообще изощрённое убийство сервера.
В методике нигде не написано, что есть такая необходимость. Ссылка, которую вы используете, описана как «страничка, где можно проверить как это работает под вашим браузером», а *не* «рабочее решение, которое используется у меня на блоге».

mr-simm (mr-simm.livejournal.com)
22 апреля 2011, 16:18

Кстати к вопросу оптимизации времени загрузки - нет ли софтины, которая на автомате оптимизирует все PNG24 в указанной папке, минимизируя число битов на пиксель? Скажем, двухцветная PNG'шка в PNG24 весит заметно больше, чем могла бы в двухбитном варианте.

bolk (bolknote.ru)
22 апреля 2011, 20:23, ответ предназначен mr-simm (mr-simm.livejournal.com):

Все известные мне методы оптимизации графики есть в этой книге: http://speedupyourwebsite.ru/books/reactive-websites/

Прочитайте, я уверен, найдёте для себя массу полезного.

id.rambler.ru/users/16051976 (id.rambler.ru/users/16051976)
23 апреля 2011, 00:06

Интересно, но в некоторых проектах Яндекса (уже не помню где копался), огромные портянки ЦСС шли прямо в коде самой странички, не подключаясь извне.
Это можно видеть на странице с результататми поиска.

bolk (bolknote.ru)
23 апреля 2011, 09:24, ответ предназначен id.rambler.ru/users/16051976:

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

nextstation (openid.yandex.ru/nextstation/)
25 апреля 2011, 17:44, ответ предназначен bolk (bolknote.ru):

Не рискуйте, Евгений :-)
На самом деле сss и js вшиты в код всех страниц выдачи яндекса..
Видимо SERP формируется допотопным скриптом.
Планируется смена архитектуры, а допиливать существующую никто не хочет.

bolk (bolknote.ru)
25 апреля 2011, 19:20, ответ предназначен nextstation (openid.yandex.ru/nextstation/):

Видимо SERP формируется допотопным скриптом.
Планируется смена архитектуры, а допиливать существующую никто не хочет.
Мне-то хоть немного видно что в SERP происходит, я могу делать какие-то выводы, а у вас-то такое откуда? :)

Там ничего не делается просто так и никто ничего не забрасывает, это «градообразующий» сервис всего Яндекса, наш поиск.

Sam (инкогнито)
26 апреля 2011, 01:22, ответ предназначен mr-simm (mr-simm.livejournal.com):

mr-simm,

http://rmcreative.ru/blog/post/clio-command-line-image-optimization-1.0

Крунгадашш (openid.yandex.ru/nextstation/)
26 апреля 2011, 15:09, ответ предназначен bolk (bolknote.ru):

Мне-то хоть немного видно что в SERP происходит, я могу делать какие-то выводы, а у вас-то такое откуда?
Возможно, Яндекс и непрозрачная компания, но не скрывает, что ее архитектура поисковой выдачи устарела.
anatolix.livejournal.com/52547.html

А свои выводы я смогу сделать, просмотрев исходный код страниц выдачи.

bolk (bolknote.ru)
22 июня 2011, 16:16

О практической реализации: http://bolknote.ru/2011/06/22/~3287

Ваше имя или адрес блога (можно OpenID):

Текст вашего комментария, не HTML:

Кому бы вы хотели ответить (или кликните на его аватару)