[Из песочницы] Java библиотека для эффективной передачи CSS и JavaScript

В данной статье описывается способ передачи JavaScript и CSS методом соединения ресурсов, с последующими их минимизацией и сжатием, при помощи небольшой Java библиотеки «Combinatorius», что позволяет ускорить и упростить передачу контента.

Демо: combinatorius.dkiriusin.com
GitHub: github.com/deniskiriusin/combinatorius


  1. Соединение JavaScript и CSS в один JavaScript и CSS ресурс соответственно для сокращения количества HTTP запросов.
  2. Локальное кэширование сгенерированных данных для улучшения времени отклика.
  3. Правильные Expires и Cache-Control HTTP заголовки для помощи браузеру с условными запросами.
  4. Поддержка ETag для определения соответствия между кэшем браузера с данными на сервере.
  5. Сжатие методом gzip для уменьшения размера HTTP ответа.
  6. Поддержка YUI Compressor.
  7. Поддержка версий передаваемых ресурсов (fingerprinting & static resources versioning).
  8. Поддержка CSS тем через параметры URL или Cookies.
  9. Простая конфигурация.


Скорость загрузки Интернет страницы зависит от многих факторов о некоторых из которых необходимо знать Веб-разработчикам.

Мы не будем обсуждать скорость предоставляемую Интернет провайдером, DNS настройки или географическое расположение ресурса, а сфокусируемся на HTTP протоколе и тех его методах которые мы можем использовать для ускорения передачи CSS и JavaScript контента.

Итак, основными факторами влияющими на скорость загрузки страницы являются:

— Размер передаваемого контента
— Количество HTTP запросов

Соответственно, наша цель — уменьшить размер передаваемого контента и количество запросов к серверу до минимума.

Проблема 1: Размер передаваемого контента


Рассмотрим что происходит во время загрузки страницы. Современные Интернет-ресурсы с богатым пользовательским интерфейсом посылают десятки, а порой и сотни HTTP запросов для загрузки контента. Многие из них приходятся на CSS и JavaScript. Общий вес передаваемого CSS и JavaScript как правило составляет несколько сотен килобайт и более. Уменьшить объём передаваемого контента можно при помощи его минимизации и сжатия.

Минимизация — это процесс, задача которого уменьшение скрипта при сохранении его функциональности. Достигается это посредством удаления из скрипта комментариев, пробелов, а так же сокращением имён переменных.

Библиотека полностью поддерживает YUI Compressor для минимизации CSS/JavaScript и gzip для сжатия данных, что способно уменьшить общий вес передаваемого CSS и JavaScript в разы. По умолчанию библиотека не минимизирует ресурсы, имена которых содержат суффикс ».min.».

prop.YUI.OmitFilesFromMinificationRegEx = .*\.min\.(js|css)$


Регулярное выражение можно изменить в combinatorius.properties.

Так как сжатие данных — ресурсоемкий процесс, то конечные данные кэшируются как на стороне сервера так и клиента, и предоставляются напрямую из кэша при последующих запросах. В случае изменения CSS и JavaScript, данные минимизируются, сжимаются и кэшируются заново.

Проблема 2: Количество HTTP запросов


Нам нужно что бы клиент посылал всего два запроса в независимости от количества CSS и JavaScript ресурсов на странице. По одному на CSS и JavaScript соответственно. Поглядим в чем выгода такого подхода.

Как гласит RFC 2616, HTTP запрос должен соответствовать формату:

Request = Request-Line
         *(( general-header 
         | request-header 
         | entity-header ) CRLF) 
          CRLF
         [ message-body ]


В реальной жизни это будет выглядеть приблизительно так:

GET /Protocols/rfc2616/rfc2616-sec5.html HTTP/1.1
Host: www.w3.org
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.89 Safari/537.36
HTTPS: 1
Referer: https://www.google.ie/
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8
Cookie: _ga=GA1.2.587820689.1448903370; JSESSIONID=00002Fn37WPDiDzeIspqmDaEY1J:-1; web_vid=1140991966240108


Исходя из того же RFC 2616, формат HTTP ответа следующий:

Response = Status-Line 
         *(( general-header
         | response-header
         | entity-header ) CRLF)
          CRLF
         [ message-body ]


Chrome DevTools нам покажет что-нибудь вроде:

HTTP/1.1 200 OK
Date: Tue, 12 Apr 2016 15:56:01 GMT
Last-Modified: Thu, 18 Feb 2016 10:16:05 GMT
ETag: "19982-52c08a77e8340"
Accept-Ranges: bytes
Content-Length: 104834
Keep-Alive: timeout=10, max=100
Connection: Keep-Alive
Content-Type: text/css
X-Pad: avoid browser bug


Размер HTTP заголовков может варьироваться от 200 байт до 2KB и более, принимая во внимание Cookies.

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

Но основная проблема даже не в этом, а в том что — запросы медленные. Современные браузеры многопоточны и стараются изо всех сил, но так или иначе почти на каждый HTTP запрос необходимо определить DNS, создать соединение с сервером, затем SSL рукопожатия, в случае HTTPS… И только после этого мы можем получить HTTP ответ с сервера. На все это уходит время, и чем больше запросов тем больше времени уходит на загрузку страницы.

К счастью, HTTP заголовки могут быть крайне полезны, и библиотека умело расставляет их что бы максимально ускорить скорость загрузки CSS и JavaScript.

Cache-Control (HTTP/1.1)


Директивы заголовка Cache-Control определяют кто может кэшировать HTTP ответ, на каких условиях и как долго. Лучше всего не посылать запрос вовсе, а сохранять копию ответа в кэше браузера и брать его оттуда да бы не общаться с сервером. Это устраняет необходимость платить за передачу данных по сети.
Так директива «max-age» определяет максимальное время в секундах в течении которого полученный ответ может быть повторно использован из кэша браузера. Библиотека кэширует данные на один год по умолчанию.

Cache-Control: public, s-maxage=31536000, max-age=31536000


Изменить конфигурацию можно в combinatorius.properties.

Expires (HTTP/1.0)


Данный заголовок по сути является аналогом Cache-Control, вытиснявшим его в HTTP/1.1. Expires так же определяет на сколько долго данные могут кэшироваться на стороне клиента. Библиотека устанавливает Expires на один год вперёд по умолчанию.

Expires: Thu, 15 Apr 2017 22:00:00 GMT


Изменить конфигурацию можно в combinatorius.properties.

ETag (HTTP/1.1)


Обеспечивает проверку кэша и позволяет клиенту послать условный запрос. Это позволяет кэшу быть более эффективным, так как веб-серверу не нужно отправлять полный ответ, если содержимое не изменилось. Библиотека использует ETag аналогично использованию отпечатков пальцев. Так например, нет необходимости изменять имена CSS и JavaScript ресурсов, после внесения в них изменений, при долгом кэшировании. Библиотека автоматически распознаёт изменения внесённые в CSS и JavaScript. Данные автоматически минимизируются если нужно, сжимаются, помещаются в кэш и доставляются клиенту со всеми необходимыми заголовками.
Библиотека доступна из центрального репозитория.


    com.dkiriusin
    combinatorius
    1.0.56



Регистрируем сервлет в web.xml.


    Combinatorius
    com.dkiriusin.combinatorius.ComboServlet
    0


    Combinatorius
    /combo/*



, отныне все запросы к /combo/* будут обрабатываться библиотекой.

Все что нужно далее — создать файл combinatorius.properties и поместить его в Classpath.
На примере Tomcat добиться этого можно изменив common.loader в catalina.properties и добавив в него путь к combinatorius.properties. В моем случае (Ubuntu 12.04 LTS):

view /etc/tomcat7/catalina.properties


До:

common.loader=${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar,/var/lib/tomcat7/common/classes,/var/lib/tomcat7/common/*.jar


После:

common.loader=${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar,/var/lib/tomcat7/common/classes,/var/lib/tomcat7/common/*.jar,${catalina.base}/combinatorius-conf


Соответственно создаём директорию:

mkdir /var/lib/tomcat7/combinatorius-conf


И копируем в неё combinatorius.properties.

combinatorius.properties
#---------------------#
# required properties #
#---------------------#

# root CSS directory
prop.css.dir = /var/lib/tomcat7/webapps/my_project/css
# cached CSS directory
prop.css.cache.dir = /var/lib/tomcat7/webapps/my_project/css_cache
# root JS directory
prop.js.dir = /var/lib/tomcat7/webapps/my_project/js
# cached JS directory
prop.js.cache.dir = /var/lib/tomcat7/webapps/my_project/js_cache

#---------------------#
# optional properties #
#---------------------#

# themes root directory
prop.themes.dir = /var/lib/tomcat7/webapps/my_project/themes
# Cache-Control: s-maxage directive (31536000 by default)
prop.s-maxage = 31536000
# Cache-Control: max-age directive (31536000 by default)
prop.max-age = 31536000
# Enables gzip compression (true by default)
prop.isCompressionEnabled = true
# Enables YUI compressor (true by default)
prop.isYUICompressorEnabled = true
# Insert line breaks in output after the specified column number (-1 by default)
prop.YUI.CSSCompressor.linebreakpos = -1
# Splits long lines after a specific column (100 by default)
prop.YUI.JavaScriptCompressor.linebreak = 100
# Minify only, do not obfuscate (false by default)
prop.YUI.JavaScriptCompressor.nomunge = false
# verbose output (false by default)
prop.YUI.JavaScriptCompressor.verbose = false
# Preserve unnecessary semicolons (such as right before a '}') (false by default)
prop.YUI.JavaScriptCompressor.preserveAllSemiColons = true
# Disable all the built-in micro optimizations (true by default)
prop.YUI.JavaScriptCompressor.disableOptimisations = true
# Define files to be omitted of minification ('.*\.min\.(js|css)$' by default)
prop.YUI.OmitFilesFromMinificationRegEx = .*\.min\.(js|css)$


Библиотека работает с CSS и JavaScript ресурсами в prop.css.dir и prop.js.dir директориях, а так же их суб-директориях. CSS и JavaScript файлы рекурсивно считываются в алфавитном порядке, минимизируются, сжимаются и отправляются клиенту. Минимизированные данные кэшируются на стороне сервера в директориях prop.css.cache.dir и prop.js.cache.dir.

Ресурсы соответствующие регулярному выражению prop.YUI.OmitFilesFromMinificationRegEx не минимизируются.

CSS темы


Так же предусмотрена поддержка CSS тем. CSS тема представляет из себя prop.themes.dir суб-директорию с одним или более CSS файлами. Например prop.themes.dir/green/theme.css. Имя темы должно совпадать с именем суб-директории и может передаваться библиотеке в виде URL параметра theme или как значение combinatorius.theme в Cookies.

Подключение дополнительных ресурсов


Возможно подключение дополнительных ресурсов, не входящих в prop.css.dir и prop.js.dir. Такая необходимость может возникнуть в случае если скрипт используется редко (на одной-двух страницах в проекте) и не должен быть включён в «сборку» по умолчанию. Передать дополнительные ресурсы можно при помощи URL параметра resources.

/combinatorius/combo/&type=js&resources=extra_js/extra1.js,extra_js/extra2.js&theme=blue


JSP Тег


Для простоты и надежности рекомендую использовать JSP тег для генерации URL. По одному тегу на CSS и JavaScript соответственно. Обязательными атрибутами являются type и path.

<%@ taglib uri="https://github.com/deniskiriusin/combinatorius" prefix="cb" %>


  blue
  extra_css/extra1.css,extra_css/extra2.css





Использование JSP тега имеет одно важное преимущество. Тег автоматически подписывает ресурсы, добавляя версию в конце URL для решения проблем связанных с опустошением кэша при агрессивном кэшировании (cache busting).

/combinatorius/combo/&type=js&v=1465737376000

Ссылки для чтения:

developer.yahoo.com/performance/rules.html
developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/? hl=en

© Habrahabr.ru