8 ways to make your single-page web app faster | Eventual Consistency
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
I know, this sounds really obvious. But the truth is that most client side frameworks (AngularJS, Backbone, Ember, etc.) haven’t been around for that long.
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
Most JavaScript templating engines are actually libraries that take some string with placeholders in it, and compile that into a JavaScript function. Instead having your HTML templates compiled every time the browser loads the page, you could save a decent amount of startup time by precompiling your templates and serving the compiled version to the client.
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 <script>
tags originating from a different domain. A JSONP call is the dynamic construction of such a script tag, passing URL parameters to the required resources, which then responds with a JSON (or JavaScript representation) of the requested resource.
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.
For huge collections I would generally either employ some sort of pagination, relying on the server to provide persistence, or develop some sort of LRU algorithm to discard unused items from memory. A good reference to such a design in JavaScript is monsur’s jscache library.
7. Don’t reinvent the wheel
Again, it’s one of those things that seem so obvious, but us developers have a tendency to try and solve problems that already have a proven solution available. Don’t be afraid of 3rd party libraries (Javascript has a ton of them).
- Use jQuery’s built-in
$.animate()
to do fancy animations. - Use Twitter Bootstrap to bring up a quick prototype, or customize it and use it as a foundation for your app. Their JavaScript plugins are awesome.
- Use Underscore.js to fill the many blanks left by Javascript’s (lack of a) standard library.
- 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).