RapidFMS is not your typical website. From the outset and more than hinted by the name, it is supposed to be fast. To achieve the speed we do, many techniques were researched but also other’s recommendations were adopted. One site that contains a long list of recommendations for website performance as well as a tool to enable evaluation of you site from within your browser is Yahoo (See Yahoo’s performance recommendations here https://developer.yahoo.com/performance/rules.html).
For RapidFMS we actually looked at each and every one of them and this along with PageSpeed from Google (https://developers.google.com/speed/pagespeed/) and the awesome web tool GTMetrix http://gtmetrix.com/ we were able to double the very fast performance we had.
This is how we managed to reduce the loading time of RapidFMS (Sydney to Melbourne) from about 2s (non-cached login form) / 10s (non-cached application home) to 0.5s (non-cached login form) / 2s (non-cached application home) and how we reduced the size of the payloads to about 1/3rd. Note: the cached versions were already sub-1s to begin with but are now even faster than they were.
(We’ll go into some of our own additional findings after we go over Yahoo’s recommendations.)
Yahoo’s recommendations:
– Minimize HTTP Requests
Before we optimised we had about 114 JS files to download in our first payload. Partly this was due to our code modularisation of code but also a couple of the 3rd party components we used were modularised.
At the end of the day we have to download the content of the JS files, but the user’s perception is what matters. For the login form we only required about 80 of them, the rest were required only after having logged in.
Reducing the JS files from 114 to 80 gave a very noticeable speed increase in page loading. 80 is still a lot of JS files, so we looked at bundling them into different payloads as follows which worked very well and is what we have stuck with at this moment. The structure of the payload is somewhat important to ensure that what is needed early on is delivered, but also re-used later on. We put the minimum we required to bootstrap the system and load the login form into a bootstrap payload (payload 1). We put the 3rd party components that could be minified together in another (payload 2) and 3rd party components that wouldn’t work minified into the remaining (payload 3). Our system still caters for optional lazy loading of components, optional pre-fetching of non-bundled components to cater for 3rd party component oddities where required.
– Use a Content Delivery Network
Using a CDN is just not plain black and white any more. There are issues if you have an SSL site with non-SSL components with browsers poping up warnings and then there are Australia’s new privacy rules. To make things simpler for the latter, we decided not to use a CDN hosted overseas. In fact, we chose not to use a dedicated CDN all-together pretty much for the following reasons (http://wonko.com/post/javascript-ssl-cdn). Having said that, we believe our performance is still pretty awesome without but will consider our options here in future if required.
– Add an Expires or a Cache-Control Header
For most part we actually started with smart caching from the start – that is one of the main ways we managed to get such great performance from the outset. We didn’t have much to do here but we also cater for dumber (or older) browsers which don’t honour cache control mechanisms properly.
– Gzip Components
As a server-side setting, we always had the luxury of this enabled for our TEST / DEMO environments, but we actually had it disabled on our slower DEV environments. This does noticeably reduce the actually transmitted payload size by about half.
– Put Stylesheets at the Top
This is an HTML requirement, we always did this.
– Put Scripts at the Bottom
This is something that is quite easy to do for code you write, however it isn’t always possible for the 3rd party components you may use. For most part this really is worthwhile. Moving all the scripts we could was a pretty trivial exercise.
– Avoid CSS Expressions
– Make JavaScript and CSS External
Not only for performance reasons, but even for modularity and debugging ability, everything is just easier if your JS in particular are external. Nothing to do here.
– Reduce DNS Lookups
Nothing to do here.
– Minify JavaScript and CSS
We had already chosen to minify our JS files so we didn’t have anything to do here. However, we chose to not use a minifier but rather Google’s closure compiler (https://developers.google.com/closure/compiler/). Although technically it isn’t a minifier, it does an admiral job of having the same effect. In fact, in many many situations it can reduce code size to be smaller than a plain minifier (see my other blog here clould.com.au/?p=161) – and you can actually minify the closure compiled code for even greater impact – but… bundling of multiple minified files proved to be an issue, but bundling of multiple closure compiled wasn’t. So forgoing an additional saving of less than 1% we only closure compile.
– Avoid Redirects
For most part we don’t have a need for redirects as we essentially have a single-page Web App.
– Remove Duplicate Scripts
Nothing to do here.
– Configure ETags
At present we aren’t using ETags, something to play with in future.
– Make Ajax Cacheable
Making Ajax cachable is an interesting exercise. It makes sense for some things (lazy loaded JS files) but not others (dynamic data). We already did have fine control over what we required in this area so nothing to do here.
– Flush the Buffer Early
Likely an easy thing to try in future but our pages are very very small given the lazy loading nature so we won’t have much to send after a flush.
– Use GET for AJAX Requests
We already have the ability to use both GET or POST but for the time being we have left it configured to use POST. We didn’t see much noticeable different in performance here.
– Post-load Components
This is likely one of the single most important factors in making your website faster. Not only does a large application only then need to fetch (and cache) what a user wants to use, the individual payloads are so tiny the user doesn’t necessarily realise what was happening behind the scenes. Lazy loading forms is very easy as they are loaded and cached on demand.
– Preload Components
Like Yahoo says, this sounds like the opposite of Post-load, but actually what we call pre-fetch you can do in an intelligent way behind the scenes, in parallel to whatever the user happens to be doing.
– Reduce the Number of DOM Elements
Yahoo gives a simplistic explanation of this. In our research performance of a page wasn’t that great a difference with a few or a lot of DOM elements – what was more important was the structuring or ‘scoping’ of DOM searches. We like and use JQuery and the way the selectors work, but what most people don’t realise is that selectors have an optional scope – this scope is extremely important to use and use correctly to ensure you are not traversing the entire DOM tree whenever you need to select something.
– Split Components Across Domains
For similar reasons to using a CDN, we’ve chosen not to do anything here yet.
– Minimize the Number of iframes
None of the reasons not to use iframes suggested by Yahoo affect RapidFMS. Nothing to do here.
– No 404s
Nothing to do here.
– Reduce Cookie Size
We also don’t like cookies (unless they are edible) but we need a cookie. Because a cookie in our case is required, we have one and only one.
– Use Cookie-free Domains for Components
For similar reasons to using a CDN, we’ve chosen not to do anything here yet.
– Minimize DOM Access
See my comments above in the section Reduce the Number of DOM Elements.
– Develop Smart Event Handlers
There is a lot of good and bad that can be done with event handlers. We have actually expanded on the idea of how JQuery provides event handler’s to minimise the overheads required where large numbers of handlers may normally have been required. Having said that, we typically don’t require large numbers of event handlers.
– Choose <link> over @import
Although YSlow states we are using @import, we actually don’t have any in our or 3rd party components. Maybe there is a bug in YSlow that mis-identifies !important for @import? Haven’t investigated.
– Avoid Filters
Minor and we have decided not to consider this for the time being.
– Optimize Images
Agreed on this. Nothing to do here.
– Optimize CSS Sprites
We really only debated this for 5 of the images we use but the issue with this, is those images are only used in cases where we have a dumb browser (which may not cater for CSS Sprites). We could consider just removing the 5 images for text equivalents.
– Don’t Scale Images in HTML
Nothing to do here.
– Make favicon.ico Small and Cacheable
Nothing to do here.
– Keep Components under 25K
It’s worth doing your own research here and also reading articles such as this (http://www.guypo.com/uncategorized/mobile-browser-cache-sizes-round-2/). Not everything is as it seams and deferring or lazy loading components will help make your situation much much easier if you have to satisfy tight cache requirements.
– Pack Components into a Multipart Document
For future investigation.
– Avoid Empty Image src
This is easy to do and causes no issues. Nothing to do here.
Our additional recommendations:
– Client side over Server side
Make as much of your web site or web app client side. This with all caching considerations considered allows much of your site to be cached. This means the only payloads to/from the server should be data. A re-visit of RapidFMS to get to the home page has a payload of around 12kb.
– Lazy load lazy load
Where-ever you have the ability to do so in an un-noticeable manner, lazy load.
– Cache
Be sure to cache what you can in the most appropriate way. This can include caching in the file system or even caching in DOM for the session. The latter can be useful for devices that may not cache anything, but then watch your memory usage also.
– JQuery
JQuery is an awesome library, use it well. Learning selectors and scoping – and doing it right will make your code faster and more re-usable.
Here really only touches the surface of optimisations. Perhaps one day we’ll do a followup articule.
JC