Over the last couple of years, the idiom of single-page web applications has gotten to be quite popular. This happened for several reasons, some technological, some driven by the demand for better user experience - but whatever the reason, it really changed the way web developers carry their work.
A lot of logic has been pushed up the stack, to the client, resulting in thinner server side code. In a single-page app, the server handles mostly authentication and persistence, while the business logic itself (or at least a big chunks of it) has moved to the client, along with the presentational logic.
This shift from backend to frontend naturally isn't always smooth. Client side code has a few different paradigms and uses a few principals less familiar to those coming from a server-side world (excluding maybe async environments such as node.js).
This post is a collection of things I generally learned the hard way. A collection of solutions to common problems: some were easier to solve, some less obvious - but I bet a lot of people still tackle them when making this transition. As you'll see, these are not specific to any single framework, library or browser, but rather a general set of things to look out for.
1. Avoid reflowing the DOM
This sounds a bit cryptic to those unfamiliar with the concept, so let me explain: when we add, remove or change the size of an element that is visible in the browser, the browser then has to recalculate the dimensions of all other elements and realign them according to their style rules. This is called a reflow, and it's a relatively expensive operation.
This is made really easy when writing single-page applications, where we might have large collections of objects, and we then decide to render their HTML. The intuitive thing to do would be to chose some parent element, maybe a list or a table, iterate over the collection, rendering HTML for every item, and appending that to the parent element.
For most cases this would work just fine without a noticeable performance hit. But for really large collections containing hundreds or more elements, this easily becomes visible.
One useful technique to overcome this is to add all items to a parent element that isn't yet added to the DOM, and only then add it.
This triggers only one reflow for the whole collection. A good explanation about how to achieve this using
document.createDocumentFragment() is available on this BackboneFu article.
2. Know your framework
Being new, it's safe to assume that we don't yet have a lot of experience with them and best practices are hard to come by. Combine this with the fact that they usually make it pretty easy to hit the server with AJAX requests and you are inviting trouble. Analyze your application and try to understand the cases in which state could be cached in the client. Retrieving information from memory or from the browser's localStorage is infinitely faster than making HTTP requests, even if you have the fastest, most efficient server available. Network is many orders of magnitude slower than memory.
To make this easier, check out lscache. It's a memcache-like client-side caching library that uses localStorage behind the scenes.
3. Know your templates
This is faster because the browser no longer has to perform the compilation, has less HTML to load, and in some cases you might not even need to load the entire template engine but only a smaller runtime component instead.
Handlerbars.js makes this especially easy.
4. Use WebSockets where it makes sense to do so
WebSockets are the hottest buzzword at the moment, and for good reasons. Finally a standardized way to make real-time stuff possible on the web. But the truth is, it's far from being a widely adopted standard, and the fact that it isn't even yet a final standard makes things hard to manage and invites cross browser interoperability problems.
This means that while on our bleeding edge machines, running the latest version of Chrome, Firefox or Safari things are silky smooth, while for the rest of the world, that is generally using a Nokia 1100 or older versions of Internet Explorer - this technology is not available at all. So they have to fallback to other, generally slower mechanisms (or even worse, are left unable to use your app).
If we do decide to go down that path, I had some good experiences with socket.io. It's a client + server (officially in node.js, but implementations are available for other platforms as well) solution that helps alleviate some of the difficulties described above. It helps pick the best transport protocol to use given the client's ability and gives a common interface on top of all different implementations of this ongoing standard.
5. Two calls are slower than one
The web is a magical place full of amazing data and tools open to use by other via APIs. Many applications rely on these APIs to carry out certain tasks or provide richer data within the application. Generally, because AJAX calls are restricted to the same domain the page originates from, we would have a flow similar to this one:
- The client requests a resource from the origin server
- The origin server would then make another request to the external resource
- The external resource returns some data to the origin server
- The origin server returns that data to the client.
As you can see, the origin server is basically acting as a proxy. So if we don't do any persistence or special processing to the data returned from the external resource, we might as well have the client call that resource directly.
There are two widely available ways to accomplish this, both require the external resource's support.
The first one is called JSONP. JSONP takes advantage of the fact that browsers allow the inclusion of
Many external APIs support this method today, and it has built-in support in jQuery's
$.ajax method. The biggest problem however, is that since it is done using a
<script> tag, it is limited to
GET requests only.
The second way of achieving cross domain AJAX is called CORS, or Cross Origin Resource Sharing. Again, this is still a draft and is only supported in recent browsers (no support at all in IE7 for example) - it does solve the current limitations of JSONP by allowing POST requests, so for modern browsers you could even upload files directly to S3 without going through your servers.
6. Stop hoarding
This one is related to collections again. As I mentioned in #2 - memory is fast and keeping things in memory is awesome. But don't go overboard. Not all machines are blessed with gigabytes of memory (think mobile browsers for example), and keeping things in memory is not always needed. For many use cases, once we draw an item to the screen, we no longer need to keep it's representation anywhere else.
7. Don't reinvent the wheel
- Use jQuery's built-in
$.animate()to do fancy animations.
- Whatever crazy thing you want to accomplish, someone on GitHub has already done it first.
8. Measure, optimize, repeat (but only when you have to)
The last tip I am going to leave you with has nothing to do with single-page applications. It is applicable to software in general - and it is all too easy to forget.
Speed and performance are ongoing tasks. Every new feature or bug fix we add to our code could easily damage or enhance the perceived speed of our application. So always measure how things behave, but be careful when optimizing. It has been said many times before, but premature optimization is the root of all evil.
It's all too easy to get carried away and spend a lot of time optimizing something that 99% of your users will never notice. Before writing even a line of code, weigh the benefit and the cost, and make an informed decision. In many cases that time would be better spent delivering something new and awesome. So happy shipping!
So this is my collection of tips. I hope you find at least parts of it useful. I'd love to hear your tips, tricks and secrets and discuss them, so comments are always welcome (even if just to say how wrong I am or how bad my spelling is).