UiPerformance Plugin supported by SpringSource
Dependency :
compile ":ui-performance:1.2.2"
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
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 addingcompression='on' compressableMimeType='text/html,text/xml,text/plain'
Usage
The plugin is enabled by default in all environments (including development) so configure whether it's enabled per-environment in Config.groovy, for exampleenvironments {
development {
uiperformance.enabled = false
}
}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' />
<script src='/appcontext/js/util/collections__v123.gz.js'></script>
<p:css name='main'/>
<link rel='stylesheet' type='text/css' href='/appcontext/css/main__v123.gz.css' />
<p:image src='logo.png'/>
<img src='/appcontext/images/logo.png' border='0' />
<p:inputImage src='edit.png'/>
<input type='image' src='/appcontext/images/edit.png' border='0' />
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']
<p:favicon/>
<link rel='shortcut icon' href='/favicon.ico' type='image/x-icon'/>
<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);
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
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']]
]<p:javascript src='jquery/jquery.all'/>
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 */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;
…
}.menuButton a.home {
background-repeat: no-repeat;
background-position: center left;
background-image: url('../images/skin/house.png');
…
}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; */
…
}.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>
<body><g:layoutBody/><p:renderDependantJavascript /></body> </html>
<p:dependantJavascript javascript='var foo = 1; alert(foo);' />def firstFieldFocus = { attrs, body ->
String javascript = "javascript that gives focus to the first field in the document"
p.dependantJavascript([javascript:javascript])
}<p:addJavascript> // your javascript </p:addJavascript>
<script type='text/javascript'> // your javascript </script>
<p:addJavascript src='foo' />
<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 */ }uiperformance.exclusions = [ "**/grails_logo.jpg", "**/dojo/**" ]
| Property | Default Value | Meaning |
|---|---|---|
| uiperformance.enabled | true | set to false to disable processing |
| uiperformance.processImages | true | set to false to disable processing for all images |
| uiperformance.processCSS | true | set to false to disable processing for all .css files |
| uiperformance.processJS | true | set 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.minifyJs | true | set to false to disable minification for all .js |
| uiperformance.minifyJsAsErrorCheck | false | set to true to minify .js files in-memory for error checking but discard |
| uiperformance.continueAfterMinifyJsError | false | set to true to only warn about .js minification problems rather than failing the build |
| uiperformance.minifyCss | true | set to false to disable minification for all .css |
| uiperformance.minifyCssAsErrorCheck | false | set to true to minify .css files in-memory for error checking but discard |
| uiperformance.continueAfterMinifyCssError | false | set to true to only warn about .css minification problems rather than failing the build |
| uiperformance.keepOriginals | false | set to true to keep the original uncompressed and unversioned files in the war along with the compressed/versioned files |
| uiperformance.html.compress | false | set to true to enable gzip for dynamic text content |
| uiperformance.html.urlPatterns | none | set to restrict dynamic text url patterns that should be processed |
| uiperformance.html.debug | false | set to true to enable PJL filter debug logging |
| uiperformance.html.statsEnabled | false | set to true to enable PJL filter stats tracking |
| uiperformance.html.compressionThreshold | 1024 | sets the minimum content length for compression |
| uiperformance.html.jakartaCommonsLogger | none | set the category of the commons/log4j logger to debug to |
Author
Burt Beckwith burt@burtbeckwith.comPlease 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