A long time I go I wrote an angular app that was pleasantly straight forward. It loads all records from the server in one big fat AJAX GET. The data is large, ~550Kb as a string of JSON, but that's OK because it's a fat-client app and it's extremely unlikely to grow any multiples of this. Yes, it'll some day go up to 1Mb but even that is fine.
Once ALL records are loaded with AJAX from the server, you can filter the whole set and paginate etc. It feels really nice and snappy. However, the app is slightly smarter than that. It has two cool additional features...
-
Every 10 seconds it does an AJAX query to ask "Have any records been modified since {{insert latest modify date of all known records}}?" and if there's stuff, it updates.
-
All AJAX queries from the server are cached in the browser's local storage (note, I didn't write
localStorage
, "local storage" encompasses multiple techniques). The purpose of that is to that on the next full load of the app, we can at least display what we had last time whilst we wait for the server to return the latest and greatest via a slowish network request. -
Suppose we have brand new browser with no local storage, because the default sort order is always known, instead of doing a full AJAX get of all records, it does a small one first: "Give me the top 20 records ordered by modify date" and once that's in, it does the big full AJAX request for all records. Thus bringing data to the eyes faster.
All of these these optimization tricks are accompanied with a flash message at the top that says: <img src="spinner.gif"> Currently using cached data. Loading all remaining records from server...
.
When I built this I decided to use localForage which is a convenience wrapper over localStorage
AND IndexedDB
that does it all asynchronously and with proper promises. And to make it work in AngularJS I used angular-localForage so it would work with Angular's cycle updates without custom $scope.$apply()
stuff. I thought the advantage of this is that it being async means that the main event can continue doing important rendering stuff whilst the browser saves things to "disk" in the background.
Also, I was once told that localStorage
, which is inherently blocking, has the risk that calling it the first time in a while might cause the browser to have to take a major break to boot data from actual disk into the browsers allocated memory. Turns out, that is extremely unlikely to be a problem (more about this is a future blog post). The warming up of fetching from disk and storing into the browser's memory happens when you start the browser the very first time. Chrome might be slightly different but I'm confident that this is how things work in Firefox and it has for many many months.
What's very important to note is that, by default, localForage
will use IndexedDB
as the storage backend. It has the advantage that it's async to boot and it supports much large data blobs.
So I timed, how long does it take for localForage
to SET and GET the ~500Kb JSON data? I did that like this, for example:
var t0 = performance.now();
$localForage.getItem('eventmanager')
.then(function(data) {
var t1 = performance.now();
console.log('GET took', t1 - t0, 'ms');
...
The results are as follows:
Operation | Iterations | Average time |
---|---|---|
SET | 4 | 341.0ms |
GET | 4 | 184.0ms |
In all fairness, it doesn't actually matter how long it takes to save because my app actually doesn't depend on waiting for that promise to resolve. But it's an interesting number nevertheless.
So, here's what I did. I decided to drop all of that fancy localForage
stuff and go back to basics. All I really need is these two operations:
// set stuff
localStorage.setItem('mykey', JSON.stringify(data))
// get stuff
var data = JSON.parse(localStorage.getItem('mykey') || '{}')
So, after I've refactored my code and deleted (6.33Kb + 22.3Kb) of extra .js files and put some performance measurements in:
Operation | Iterations | Average time |
---|---|---|
SET | 4 | 5.9ms |
GET | 4 | 3.3ms |
Just WOW!
That is so much faster. Sure the write operation is now blocking, but it's only taking 6 milliseconds. And the reason it took IndexedDB less than half a second also probably means more hard work for it to sweat CPU over.
Sold? I am :)