Java library for efficient CSS and JavaScript rendering

This article describes a method for transferring JavaScript and CSS by combining resources, with their subsequent minimization and compression, using the small Combinatorius Java library, which allows you to speed up and simplify the transfer of content.

Demo: combinatorius.dkiriusin.com
GitHub: github.com/deniskiriusin/combinatorius

Key library features


  1. Combining JavaScript and CSS into one JavaScript and CSS resource, respectively, to reduce the number of HTTP requests.
  2. Local caching of generated data to improve response time.
  3. The correct Expires and Cache-Control HTTP headers to help the browser with conditional requests.
  4. ETag support for determining the correspondence between the browser cache and the data on the server.
  5. Gzip compression to reduce the size of the HTTP response.
  6. Support for YUI Compressor.
  7. Support for versions of transferred resources (fingerprinting & static resources versioning).
  8. Support for CSS themes via URL or Cookies options.
  9. Simple configuration.

Brief analysis


The speed of loading a web page depends on many factors, some of which Web developers need to know.

We will not discuss the speed provided by the Internet provider, DNS settings or the geographical location of the resource, but focus on the HTTP protocol and those methods that we can use to accelerate the transfer of CSS and JavaScript content.

So, the main factors affecting the page loading speed are:

- The size of the transmitted content
- The number of HTTP requests

Accordingly, our goal is to reduce the size of the transmitted content and the number of requests to the server to a minimum.

Problem 1: Content Transferred


Consider what happens when the page loads. Modern Internet resources with a rich user interface send dozens and sometimes hundreds of HTTP requests to download content. Many of them are in CSS and JavaScript. The total weight of the transmitted CSS and JavaScript is usually several hundred kilobytes or more. You can reduce the amount of transmitted content by minimizing and compressing it.

Minimization is a process whose task is to reduce the script while maintaining its functionality. This is achieved by removing comments from the script, spaces, as well as reducing the names of variables.

The library fully supports YUI Compressorto minimize CSS / JavaScript and gzip for data compression, which can reduce the total weight of the transmitted CSS and JavaScript at times. By default, the library does not minimize resources whose names contain the suffix ".min.".

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

You can change the regular expression in combinatorius.properties.

Since data compression is a resource-intensive process, the final data is cached both on the server and client side, and is provided directly from the cache on subsequent requests. If CSS and JavaScript change, data is minimized, compressed and cached again.

Problem 2: Number of HTTP requests


We need the client to send only two requests, regardless of the number of CSS and JavaScript resources on the page. One per CSS and JavaScript respectively. Let's see what the benefits of this approach are.

As RFC 2616 states, an HTTP request must conform to the format:

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

In real life, it will look something like this:

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

Based on the same RFC 2616, the HTTP response format is as follows:

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

Chrome DevTools will show us something like:

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

The size of HTTP headers can vary from 200 bytes to 2KB or more, taking into account Cookies.

Funny, but most of all you have to pay for the little things. For example, creating a page with ten static resources, each one byte in size, the browser will have to send and receive in response a few kilobytes of HTTP headers alone to download 10 bytes of useful data.

But the main problem is not even that, but that - the requests are slow. Modern browsers are multi-threaded and try their best, but somehow for almost every HTTP request you need to determine the DNS, create a connection to the server, then SSL handshake, in the case of HTTPS ... And only after that we can get an HTTP response from the server. All this takes time, and the more requests, the more time it takes to load the page.

Fortunately, HTTP headers can be extremely useful, and the library skillfully arranges them to maximize the speed of loading CSS and JavaScript.

Cache-Control (HTTP / 1.1)


The Cache-Control header directives determine who can cache the HTTP response, under what conditions and for how long. It’s best not to send the request at all, but to save a copy of the response in the browser’s cache and take it from there so as not to communicate with the server. This eliminates the need to pay for data transfer over the network.
So the max-age directive determines the maximum time in seconds during which the received response can be reused from the browser cache. The library caches data for one year by default.

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

You can change the configuration at combinatorius.properties.

Expires (HTTP / 1.0)


This header is essentially an analogue of Cache-Control, which has supplanted it in HTTP / 1.1. Expires also determines how long data can be cached on the client side. The library sets Expires one year ahead by default.

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

You can change the configuration at combinatorius.properties.

ETag (HTTP / 1.1)


Provides cache validation and allows the client to send a conditional request. This allows the cache to be more efficient, since the web server does not need to send a full response if the content has not changed. The library uses ETag similarly to using fingerprints. For example, there is no need to change the names of CSS and JavaScript resources, after making changes to them, with long caching. The library automatically recognizes changes made to CSS and JavaScript. Data is automatically minimized if necessary, compressed, cached and delivered to the client with all the necessary headers.

User guide


The library is accessible from a central repository.

com.dkiriusincombinatorius1.0.60com.yahoo.platform.yuiyuicompressor2.4.8

Register servlet c web.xml.
Combinatoriuscom.dkiriusin.combinatorius.CombinatoriusServlet0Combinatorius/combo/*

, from now on, all requests to /combo/*will be processed by the library.

All that is needed next is to create a file combinatorius.propertiesand place it in the Classpath.
Using Tomcat as an example, you can achieve this by changing common.loaderto catalina.propertiesand adding the path to combinatorius.properties. In my case (Ubuntu 12.04 LTS):

view /etc/tomcat7/catalina.properties

Before:

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

After:

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

Accordingly, create a directory:

mkdir /var/lib/tomcat7/combinatorius-conf

And copy to it 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)$

The library works with CSS and JavaScript resources in prop.css.dirand prop.js.dirdirectories, as well as their sub-directories. CSS and JavaScript files are recursively read in alphabetical order, minimized, compressed and sent to the client. Minimized data is cached on the server side in the prop.css.cache.dirand directories prop.js.cache.dir.

Resources matching a regular expression are prop.YUI.OmitFilesFromMinificationRegExnot minimized.

CSS themes


Support for CSS themes is also provided. CSS theme is a prop.themes.dirsub-directory with one or more CSS files. For example prop.themes.dir/green/theme.css. The topic name must match the name of the sub-directory and can be passed to the library as a parameter URL themeor as a value combinatorius.themein Cookies.

Connect additional resources


It is possible to connect additional resources that are not included in prop.css.dirand prop.js.dir. Such a need may arise if the script is rarely used (on one or two pages in the project) and should not be included in the "assembly" by default. You can transfer additional resources using the parameter URL resources.

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

Jsp tag


For simplicity and reliability, I recommend using the JSP tag to generate URLs. One tag per CSS and JavaScript respectively. Mandatory attributes are typeand path.

<%@ taglib uri="https://github.com/deniskiriusin/combinatorius" prefix="cb" %>
blueextra_css/extra1.css,extra_css/extra2.css

Using a JSP tag has one important advantage. The tag automatically subscribes resources by adding a version at the end of the URL to solve cache cache busting problems.

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


Read links:

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

Also popular now: