[Из песочницы] Java библиотека для эффективной передачи CSS и JavaScript
В данной статье описывается способ передачи JavaScript и CSS методом соединения ресурсов, с последующими их минимизацией и сжатием, при помощи небольшой Java библиотеки «Combinatorius», что позволяет ускорить и упростить передачу контента.
Демо: combinatorius.dkiriusin.com
GitHub: github.com/deniskiriusin/combinatorius
- Соединение JavaScript и CSS в один JavaScript и CSS ресурс соответственно для сокращения количества HTTP запросов.
- Локальное кэширование сгенерированных данных для улучшения времени отклика.
- Правильные Expires и Cache-Control HTTP заголовки для помощи браузеру с условными запросами.
- Поддержка ETag для определения соответствия между кэшем браузера с данными на сервере.
- Сжатие методом gzip для уменьшения размера HTTP ответа.
- Поддержка YUI Compressor.
- Поддержка версий передаваемых ресурсов (fingerprinting & static resources versioning).
- Поддержка CSS тем через параметры URL или Cookies.
- Простая конфигурация.
Скорость загрузки Интернет страницы зависит от многих факторов о некоторых из которых необходимо знать Веб-разработчикам.
Мы не будем обсуждать скорость предоставляемую Интернет провайдером, 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.
#---------------------#
# 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