Это сайт — моя персональная записная книжка. Интересна мне, по большей части, история, своя жизнь и немного программирование.

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

35 комментариев
Виктор 2011

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

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

Евгений Степанищев (bolknote.ru) 2011

Комментарий для Виктор:

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

Евгений Степанищев (bolknote.ru) 2011

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

reon (reon.livejournal.com) 2011

Комментарий для Евгения Степанищева:

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

Евгений Степанищев (bolknote.ru) 2011

Комментарий для reon.livejournal.com:

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

Он и не смешан. Оформление в CSS, код в JavaScript. То, что это приходит в одном файле к клиенту, концептуально ничего не меняет.

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

Если сайт состоит из одной страницы, так и надо делать, в остальных случаях так делать нельзя. Вынесенный в отдельный файл CSS и JS снижает вес HTML-страницы. Когда клиент перейдёт на неё, ресурсы (JS и CSS) будут загружены из кеша.

astur (kozlov.am) 2011

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

orcinus.ru 2011

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

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

agonych (agonych.livejournal.com) 2011

Комментарий для kozlov.am:

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

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

Евгений Степанищев (bolknote.ru) 2011

Комментарий для http://orcinus.ru:

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

Евгений Степанищев (bolknote.ru) 2011

Комментарий для http://orcinus.ru:

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

Я ничего про это сказать не могу, просто не знаю о таких случаях.

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

Производительность такого решения ниже.

Евгений Степанищев (bolknote.ru) 2011

Комментарий для http://orcinus.ru:

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

Чистяков Денис 2011

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

Чистяков Денис 2011

С CSS-ом все ок.

maxim-zotov 2011

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

Евгений Степанищев (bolknote.ru) 2011

Комментарий для maxim-zotov:

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

Евгений Степанищев (bolknote.ru) 2011

Комментарий для maxim-zotov:

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

maxim-zotov 2011

Комментарий для Евгения Степанищева:

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

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

maxim-zotov 2011

Комментарий для maxim-zotov:

HTTP/1.0

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

maxim-zotov 2011

Комментарий для maxim-zotov:

На сайте bolknote.ru keepalive выключен

Извините, наврал. Включён.

SiMM 2011

Комментарий для maxim-zotov:

Он тут не мимо, поскольку ваша цель уменьшить число соединений.

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

maxim-zotov 2011

Комментарий для SiMM:

уменьшить время загрузки

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

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

$ http_ping http://bolknote.ru/files/exp-jscss/js%2Bcss.php?1.js%2C1.css
153 bytes from http://bolknote.ru/files/exp-jscss/js%2Bcss.php?1.js%2C1.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 2011

Комментарий для maxim-zotov:

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

Евгений Степанищев (bolknote.ru) 2011

Комментарий для maxim-zotov:

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

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

mr-simm (mr-simm.livejournal.com) 2011

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

Евгений Степанищев (bolknote.ru) 2011

Комментарий для mr-simm.livejournal.com:

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

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

id.rambler.ru/users/16051976 2011

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

Это можно видеть на странице с результататми поиска.

Евгений Степанищев (bolknote.ru) 2011

Комментарий для id.rambler.ru/users/16051976:

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

nextstation (openid.yandex.ru/nextstation/) 2011

Комментарий для Евгения Степанищева:

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

Евгений Степанищев (bolknote.ru) 2011

Комментарий для openid.yandex.ru/nextstation/:

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

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

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

Sam 2011

Комментарий для mr-simm.livejournal.com:

mr-simm,

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

Крунгадашш (openid.yandex.ru/nextstation/) 2011

Комментарий для Евгения Степанищева:

Мне-то хоть немного видно что в SERP происходит, я могу делать какие-то выводы, а у вас-то такое откуда?

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

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

Евгений Степанищев (bolknote.ru) 2011

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

Алиса 2017

Добрый день! Очень интересный материал. Не могу понять одного — с каким расширением полученный файл — js или css?
Или это .jscss? Как это расширение проставить в имени файла?

Евгений Степанищев (bolknote.ru) 2017

Комментарий для Алиса:

Расширение файла ни на что не влияет в данном случае.