UiPerformance Plugin supported by Pivotal

  • Tags: performance
  • Latest: 1.2.2
  • Last Updated: 22 December 2009
  • Grails version: 1.0 > *
  • Authors: Burt Beckwith
15 votes
Dependency:
compile ":ui-performance:1.2.2"

 Issues

Summary

Installation

grails install-plugin ui-performance

Description

UI Performance Plugin

The UI Performance Plugin addresses some of the 14 rules from Steve Souders and the Yahoo performance team.

Starting with version 1.2 the plugin is enabled in all environments, including development. See the discussion below about the 'enabled' config attribute for how to disable it for development.

Installation

To integrate the plugin into your application just run the 'install-plugin' Grails script, e.g.

grails install-plugin ui-performance

The plugin is tested primarily in 1.0.4 and 1.1 but does work in 1.0.3. Due to a change in the name of Events.groovy to _Events.groovy, it doesn't work in 1.0.3 without a small change. After you install the plugin, rename plugins/ui-performance-1.2/scripts/_Events.groovy to Events.groovy. This only needs to be done once, and only for 1.0.3.

Features

  • minifies and gzips .js and .css files
  • configures .js, .css, and image files (including favicon.ico) for caching by renaming with an increasing build number and setting a far-future expires header
  • provides taglibs that are version- and gzip-aware to allow developers to work without dealing with gzip or version details
  • gzips dynamic text content (GSPs, JSON, XML, etc.)
  • the bulk of the work is done during the build, and problems are caught during the build instead of after deployment. An _Events.groovy script hooks into the war building process and gzips/versions/minifies static resources before the war file is packaged
  • rewrites inner urls in CSS files to point to versioned images
  • supports Sprite images via a taglib
  • helps move JavaScript to the end of the body via a taglib
  • doesn't use a servlet - only a few taglibs and a Filter, letting the container (or an external web server) continue to serve static resources
  • no temp files and very minimal runtime cost - everything is in the war - and less processing at the server - it only needs to check if client supports gzip when rendering <javascript>, <css>, and <img> tags in GSPs
  • Grails-friendly - can be enabled per-environment so that in development mode it does nothing, and is only enabled in production (or 'qa', if you have a 'qa' environment for example) mode, so there's no need to check for modifications when enabled since Grails doesn't support modifications except in development mode
  • lots of configuration options
Dynamic text compression uses the PJL Compressing Filter with configuration borrowed from the Compress plugin. See this discussion about the configuration options borrowed from Compress plugin, and the filter's Sourceforge project page for more details about its configuration.

Motivation

To reduce the download size of JavaScript and CSS files, the plugin gzips .js and .css files. Of course not all browsers support gzip, so the original uncompressed files are kept. The "accept-encoding" header is checked to determine if the gzipped version or uncompressed should be sent.

The plugin also minifies both .js and .css using YUI Compressor.

Another feature that reduces server load is versioning and far-future "Expires" and "Cache-Control" headers. If you do no caching then each time a browser loads a page, it will make a server request to determine if each file has been updated. Since static resources rarely change, these requests cause unnecessary server traffic with a 304 response code.

Each time you do a build, a new version is applied to all image, .js, and .css files so files can be cached forever. The next time you deploy a new version of your application, all of the resource file names will have changed, so only then will files be requested.

Of course images aren't very compressible, so gzipping them doesn't make much sense, but the image payload of most sites is going to be a lot larger than that of the .js and .css files, so it makes sense to try to get clients to cache those also.

The plugin optionally gzips the HTML and other dynamic content generated by your GSPs and controllers. If you prefer to configure this yourself in your web server then you can disable this feature but leaving it enabled means more server portability and less configuration. You can configure gzip for HTML and text types in Tomcat by adding

compression='on' compressableMimeType='text/html,text/xml,text/plain'
to the HTTP <Connector> element in conf/server.xml. If you're using Apache to server static content, you'll want to use mod_deflate.

You could configure your web server to compress .css and .js files but this is resource-intensive and wasteful if the files rarely change. The plugin instead compresses the files once at build time.

Usage

The plugin is enabled by default in all environments (including development) so configure whether it's enabled per-environment in Config.groovy, for example

environments {
   development {
      uiperformance.enabled = false
   }
}
to disable it in development but enable it for production or custom environments.


You'll need to make changes in your GSPs to take advantage of these features.

To render a <script> tag for a .js file, use

<p:javascript src='util/collections' />

which will render

<script src='/appcontext/js/util/collections__v123.gz.js'></script>

where 'appcontext' is the context the app is deployed under (blank for the root context). Note that the gzipped version is shown here, and the current version is '123'. Also since the extension is obvious, it's omitted.

To render a <link> tag for a .css file, use

<p:css name='main'/>

which will render

<link rel='stylesheet' type='text/css' href='/appcontext/css/main__v123.gz.css' />

As with .js files the extension is obvious so it's omitted.

To render an <img> tag for an image file, use

<p:image src='logo.png'/>

which will render

<img src='/appcontext/images/logo.png' border='0' />

To render an <input type='image' ...> tag for an image-based form submit button, use

<p:inputImage src='edit.png'/>

which will render

<input type='image' src='/appcontext/images/edit.png' border='0' />

Note that each tag allows any number of extra attributes which are just passed through to the HTML (e.g. to specify the id, style, etc.)

favicons

By default your favicon.ico (or whatever you've named the file) will be processed like any other image. To disable this, remove the 'ico' image extension from the list of valid extensions:

uiperformance.imageExtensions = ['gif', 'png', 'jpg']

To render a versioned favicon tag, use

<p:favicon/>

which will render

<link rel='shortcut icon' href='/favicon.ico' type='image/x-icon'/>

If you have a nonstandard name or location, specify that using the 'src' attribute (omit the extension):

<p:favicon src='images/myfavicon' />

<link rel='shortcut icon' href='images/myfavicon.ico' type='image/x-icon'/>

CSS images

Embedded image urls in CSS files are a problem using this versioning scheme, but since each file is processed during the build, we can rewrite the urls inside all CSS files with the version embedded, so they also agree with the names in the war, e.g.

background-image: url(/images/cluetip/wait.gif);

becomes

background-image: url(/images/cluetip/wait__v123.gif);

Bundling

Another provided optimization is to reduce the number of static files. Typically you'll partition .css and .js files for modularity and reuse but often send the same few files in several pages. So another step in the build process involves bundling JavaScript and CSS files into larger single files. These are minified and gzipped like the rest, and the net effect is fewer requests with a smaller total size.

Bundles are configued as a list of maps:

  • the 'type' property is either 'js' or 'css'
  • the 'name' property determines the relative name of the combined file
  • and the 'files' property specifies a list of relative filenames
Note that the names of the bundled file and the source files are relative just like they'd be specified using the taglibs above - omit the 'js' or 'css' folder and the extension. For example, this configuration

uiperformance.bundles = [
   [type: 'js',
    name: 'jquery/jquery.all',
    files: ['jquery/jquery-1.2.6',
            'jquery/jquery.cluetip']],
   [type: 'css',
    name: 'bundled',
    files: ['yui/reset-min',
            'main',
            'plugins/custom_cluetip']]
]

would create js/jquery/jquery.all.js which would contain the contents of js/jquery/jquery-1.2.6.js and js/jquery/jquery.cluetip.js.

It would also create css/bundled.css which would contain the contents of css/yui/reset-min.css, css/main.css, and css/plugins/custom_cluetip.css.

You can create any number of bundles, which contain any number of source files.

Bundles are "un-bundled" if the plugin isn't enabled (e.g. in dev mode). So you would declare

<p:javascript src='jquery/jquery.all'/>
regardless of environment, and in prod mode it would resolve the single bundled file, and in dev mode would output multiple script or css tags for each original file that comprises the bundle.

Image Sprites

The plugin recognizes CSS files that are marked up using SmartSprites comments. These are discussed in detail in the library's site documentation but I'll show some examples here. Everything is defined as a comment, so these changes have no impact on development mode. Any positioning tweaks that are required are also defined in comments (sprite-margin-bottom, sprite-margin-left, etc.).

In a CSS file that contains one or more style rules that define a background image, add a comment defining an image sprite, e.g.

/** sprite: mysprite; sprite-image: url('../images/mysprite.png'); sprite-layout: vertical */

You can choose whatever name you want for the sprite and its file name. The sprite name (@mysprite@ in this example) is used as a key to link individual CSS classes with a sprite file since you can declare multiple sprite files per CSS file (although this shouldn't be necessary) or even split CSS files and refer to one sprite image from all of them.

Then you can instruct SmartSprites to merge some or all of the individual images into the sprite and replace the CSS with a reference to the sub-image within the sprite. As an example I've replaced several if the images in the standard Grails main.css with sprites. Some changes are necessary though - you can't define the background attribute as a combined attribute. Instead you must split it into background-image, background-repeat, background-position, etc. So you would need to redefine

.menuButton a.home {
   background: url(../images/skin/house.png) center left no-repeat;
   …
}

as the equivalent but more verbose

.menuButton a.home {
   background-repeat: no-repeat;
   background-position: center left;
   background-image: url('../images/skin/house.png');
   …
}

(I've omitted the non-background attributes since they're fine as they are)

and to include the image in the sprite you add a comment to the background-image attribute referencing the sprite file declared at the top (link the sprite-ref name to the declaration's sprite attribute, along with any extra attributes that are necessary for fine-tuning:

.menuButton a.home {
   background-repeat: no-repeat;
   background-position: center left;
   background-image: url('../images/skin/house.png'); /** sprite-ref: mysprite; sprite-margin-bottom: 5px; sprite-margin-top: 3px; */
   …
}

Images that repeat as backgrounds aren't good candidates for inclusion in sprites, so I didn't rework those but the remaining classes are as follows:

.menuButton a.list {
   background-repeat: no-repeat;
   background-position: center left;
   background-image: url('../images/skin/database_table.png'); /** sprite-ref: mysprite; sprite-margin-bottom: 5px; sprite-margin-top: 3px; */
}

.menuButton a.create { background-repeat: no-repeat; background-position: center left; background-image: url('../images/skin/database_add.png'); /** sprite-ref: mysprite; sprite-margin-bottom: 5px; sprite-margin-top: 3px; */ }

.message { background-repeat: no-repeat; background-position: 8px 50%; background-image: url('../images/skin/information.png'); /** sprite-ref: mysprite; sprite-margin-bottom: 4px; sprite-margin-top: 4px; sprite-margin-left: 8px; */ background-color: #f3f8fc; }

div.errors li { background-repeat: no-repeat; background-position: 8px 0%; background-image: url('../images/skin/exclamation.png'); /** sprite-ref: mysprite; sprite-margin-bottom: 8px; sprite-margin-left: 8px; */ }

th.asc a { background-image: url('../images/skin/sorted_asc.gif'); ** sprite-ref: mysprite; sprite-margin-bottom: 5px; sprite-margin-top: 3px; * }

th.desc a { background-image: url('../images/skin/sorted_desc.gif'); ** sprite-ref: mysprite; sprite-margin-bottom: 5px; sprite-margin-top: 3px; * }

.buttons input.delete { background-color: transparent; background-repeat: no-repeat; background-position: 5px 50%; background-image: url('../images/skin/database_delete.png'); /** sprite-ref: mysprite; sprite-margin-bottom: 6px; sprite-margin-top: 2px; sprite-margin-left: 5px; */ }

.buttons input.edit { background-color: transparent; background-repeat: no-repeat; background-position: 5px 50%; background-image: url('../images/skin/database_edit.png'); /** sprite-ref: mysprite; sprite-margin-bottom: 6px; sprite-margin-top: 2px; sprite-margin-left: 5px; */ }

.buttons input.save { background-color: transparent; background-repeat: no-repeat; background-position: 5px 50%; background-image: url('../images/skin/database_save.png'); /** sprite-ref: mysprite; sprite-margin-bottom: 6px; sprite-margin-top: 2px; sprite-margin-left: 5px; */ }

DependantJavascriptTagLib

Since browsers evaluate scripts as they appear in the html (in case they write to the document) it's best to include them at the end of the body if possible to decrease the apparent load time (the total page and dependent files still takes the same total time, but the browser appears ready quicker).

To help with this and to deal with Grails' use of Sitemesh and includes, you can use the <p:dependantJavascript> and <p:renderDependantJavascript> tags. Wherever you have <script> and <p:javascript> tags (either inline scripts or external scripts using the 'src' attribute) that don't write to the document, enclose them in <p:dependantJavascript> tags, e.g.

<a href='...'>click here<a>
<div id='the_div'>...</div>

<p:dependantJavascript> <script type='text/javascript'> // inline stuff </script>

<p:javascript src='whatever'/>

</p:dependantJavascript>

which cache the content inside the tag. Then render the cached content using the p:renderDependantJavascript tag (typically in a template after the body), e.g.:

<body>

<g:layoutBody/>

<p:renderDependantJavascript />

</body> </html>

Note that you can use as many <p:dependantJavascript> tags as you want in the main gsp, any included templates, and the main template and all of the the combined scripts will be rendered by the single renderDependantJavascript tag.

A new feature allows you to specify a short block of javascript as a parameter to the tag, e.g.

<p:dependantJavascript javascript='var foo = 1; alert(foo);' />

This feature can also be used when calling the taglib from another, e.g.

def firstFieldFocus = { attrs, body ->
   String javascript = "javascript that gives focus to the first field in the document"
   p.dependantJavascript([javascript:javascript])
}

<p:addJavascript> is another tag that generates the <script> tags for you but is otherwise the same as dependantJavascript:

<p:addJavascript>
// your javascript
</p:addJavascript>

which will render

<script type='text/javascript'>
// your javascript
</script>

or

<p:addJavascript src='foo' />

which will render

<script type='text/javascript' src='/js/foo.js'></script>

Customization

The default process for determining the version is to look at the version of the root project folder in the Subversion metadata files. If you're not using Subversion or you want to use some other version incrementing logic, you can define a closure in Config.groovy:

uiperformance.determineVersion = { -> /* calculate version here */ }

Also, if there are files or groups of files that you want to exclude from processing, you can define a list of Ant-style matcher patterns, e.g.

uiperformance.exclusions = [
   "**/grails_logo.jpg",
   "**/dojo/**"
]

Other properties:

PropertyDefault ValueMeaning
uiperformance.enabledtrueset to false to disable processing
uiperformance.processImagestrueset to false to disable processing for all images
uiperformance.processCSStrueset to false to disable processing for all .css files
uiperformance.processJStrueset to false to disable processing for all .js
uiperformance.imageExtensions'gif', 'png', 'jpg', 'ico' (+)specify different values to change image types that are processed
uiperformance.minifyJstrueset to false to disable minification for all .js
uiperformance.minifyJsAsErrorCheckfalseset to true to minify .js files in-memory for error checking but discard
uiperformance.continueAfterMinifyJsErrorfalseset to true to only warn about .js minification problems rather than failing the build
uiperformance.minifyCsstrueset to false to disable minification for all .css
uiperformance.minifyCssAsErrorCheckfalseset to true to minify .css files in-memory for error checking but discard
uiperformance.continueAfterMinifyCssErrorfalseset to true to only warn about .css minification problems rather than failing the build
uiperformance.keepOriginalsfalseset to true to keep the original uncompressed and unversioned files in the war along with the compressed/versioned files
uiperformance.html.compressfalseset to true to enable gzip for dynamic text content
uiperformance.html.urlPatternsnoneset to restrict dynamic text url patterns that should be processed
uiperformance.html.debugfalseset to true to enable PJL filter debug logging
uiperformance.html.statsEnabledfalseset to true to enable PJL filter stats tracking
uiperformance.html.compressionThreshold1024sets the minimum content length for compression
uiperformance.html.jakartaCommonsLoggernoneset the category of the commons/log4j logger to debug to

In addition you can configure include/exclude path patterns via uiperformance.html.includePathPatterns and uiperformance.html.excludePathPatterns, include/exclude content types via uiperformance.html.includeContentTypes and uiperformance.html.excludeContentTypes, and include/exclude user agents via uiperformance.html.includeUserAgentPatterns and uiperformance.html.excludeUserAgentPatterns. See the Configuration section here for a discussion of the meaning of these options.

If you don't configure content types, the uiperformance.html.includeContentTypes property is set to ['text/html','text/xml','text/plain'] since if this is left unset, the filter will double-gzip already gzipped .css and .js files.

Author

Burt Beckwith burt@burtbeckwith.com

Please report any issues to the Grails User mailing list and/or write up an issue in JIRA at http://jira.codehaus.org/browse/GRAILSPLUGINS under the Grails-UI-Performance component.

History

  • December 22, 2009
    • released version 1.2.2
  • July 28, 2009
    • released version 1.2.1
  • July 23, 2009
    • released version 1.2
  • May 20, 2009
    • released version 1.1.1
  • March 13, 2009
    • released version 1.1
  • December 19, 2008
    • released version 1.0
  • December 10, 2008
    • released version 0.2 in the Grails repository
  • August 26, 2008
    • released initial version 0.1