Firebase optimisation rules

October 4, 2017, 9:00 am Categories:

Categories

Ionic and Firebase make great bedfellows but even the best tools can suffer from poor performance if we aren't making best use of certain features and techniques.

Over the course of this tutorial I want to share with you some best practice tips and advice for optimising your Firebase apps and improving their performance when combined with Ionic.

Avoid nesting data

Unlike an SQL database (I.e. MySQL or PostgreSQL) Firebase uses a NoSQL approach - storing data as JSON objects. As a result there are no tables or records - as you might expect to find in a traditional database system - just a JSON 'tree' consisting of individual nodes.

For example, we could store data about particular music tracks like so:

{
   "songs" : {
      {
         "id"     : 1,
         "artist" : "Depeche Mode",
         "track"  : "Precious",
         "album"  : "Playing the Angel",
         "genre"  : "Electronic",
         "year"   : 2005,
         "rating" : 5
      },
      {
         "id"     : 2,
         "artist" : "Depeche Mode",
         "track"  : "Precious",
         "album"  : "Suffer Well",
         "genre"  : "Electronic",
         "year"   : 2005,
         "rating" : 5
      }
   }
}

This is great as it makes for a more versatile data structure but, as Firebase allows data to be nested up to 32 nodes deep, developers can sometimes get carried away with deeply nesting their data.

For example, if we had a music app that allowed users to comment on each track we might have a data structure that looks like the following:

{
   "songs" : {
      {
         "id"     : 1,
         "artist" : "Depeche Mode",
         "track"  : "Precious",
         "album"  : "Playing the Angel",
         "genre"  : "Electronic",
         "year"   : 2005,
         "rating" : 5,
         "comments": {
           "comment1": { 
             "sender": "dgahan", 
             "title": "Love this song",
             "message": "Could this band do any wrong?" 
           },
           "comment2": { 
             "sender": "mgore", 
             "title": "Plenty more where that came from",
             "message": "One of my faves for sure!" 
           },
            "comment3": { 
             ... 
           }
           // MANY more comments to follow...          
         }
      },
      {
         ...
      }
   }
}

The above example makes for a bad data structure as interating through each track would also require potentially downloading tens, if not hundreds, of megabytes of comments (and the Firebase online docs explicitly warn against this practice).

Not ideal if all you want is to display the title for each track.

A far better solution is to flatten the above data structure - also known as denormalisation - like so:

{
   "songs" : {
      {
         "id"     : 1,  
         "artist" : "Depeche Mode",
         "track"  : "Precious",
         "album"  : "Playing the Angel",
         "genre"  : "Electronic",
         "year"   : 2005,
         "rating" : 5
      }
   }


   // Move the comments into their own node
   // Reference the track for each comment by its ID
   "comments": {
      "comment1": { 
         "track" : 1,
         "sender": "dgahan", 
         "title": "Love this song",
         "message": "Could this band do any wrong?" 
      },
      "comment2": { 
         "track" : 1,
         "sender": "mgore", 
         "title": "Plenty more where that came from",
         "message": "One of my faves for sure!" 
      },
      "comment3": { 
         ... 
      }
   }
}

This separation of nodes now allows comments to be pulled in ONLY when they are specifically requested and by the track ID that they are associated with.

When designing your data structure to work with large volumes of data practice denormalisation as far as is possible.

Indexing your database

Firebase uses an arbitrary child key to perform ad-hoc queries on collections of nodes. This generally works well for small sets of data but as an application grows the performance of the query starts to degrade resulting in longer wait times for queries to be executed.

Not ideal if your application is requesting queries of large sets of data while being accessed by many different users simultaneously.

Enter the .indexOn rule which allows developers to specify which keys in their data node(s) can be defined as indexes within their Firebase Realtime Database Rules.

If we took the previous tracks node we might want to define indexes based on the artist, genre and year like so:

{
  "rules": {
    ".read": true,
    ".write": "auth != null",
    "songs": {
      ".indexOn": ["artist", "genre", "year"]
    }
  }
}

The use of such indexes helps Firebase to improve the performance of queries by reducing the time taken for data retrieval.

Using Transactions

Avoiding data overwriting/corruption when multiple users are updating a record is hugely important, particularly as you want the data contained in your apps to be accurate!

Firebase transactions help preserve data integrity by avoiding overwriting when two or more users are simultaneously trying to modify data at the same location within a database.

For example, if we have an application that allows users to accumulate points we could implement a transaction like so:

determinePoints(userName : string): Promise
{
   return new Promise((resolve) =>
   {
      var userRef    = firebase.database().ref('users/' + userName + '/points');
      userRef.transaction((currentPoints) => 
      {
         resolve(currentPoints + 1);
      });
   });
}

This then allows multiple modifications to be performed on the same data location at the same time without any conflicts occuring.

Using cloud functions

*NOTE: I'm a little wary of including this as cloud functions are still in beta release so the API might be liable to change at a moment's notice - so to take the following with a measure of caution.

Cloud functions allow developers to automatically run backend code in response to events triggered by Firebase features and HTTPS requests. This backend code is stored in a managed environment in Google's cloud.

Such functions might include, for example, registering new user profiles, firing off welcome e-mails when a user registers with a service or emailing an administrator when a user has created an account.

Use of these functions, which can be written by the developer and implemented through the Firebase CLI, can help offload logic from the client side (I.e. within the application itself) and place this on the server. Doing so can assist with reducing device CPU loads, memory usage and speed up app performance.

I won't be going into Cloud functions here as that would be an article in its own right so I'll leave you to look at the docs...for now ;)

In closing

There's no denying that Firebase provides a rich feature set making it a perfect backend choice for app developers but performance can sometimes be less than optimal.

Hopefully with the above tips and techniques you can ensure that the marriage of Ionic and Firebase in your projects runs smoothly and efficiently.

Please feel free to share your thoughts and feedback with regards to this article using the commenting system below.

HINT - I am currently working on a new e-book dedicated to exploring the integration between Ionic & Firebase so stay tuned for updates on that particular front! :)

Tags

Categories

Post a comment

All comments are welcome and the rules are simple - be nice and do NOT engage in trolling, spamming, abusiveness or illegal behaviour. If you fail to observe these rules you will be permanently banned from being able to comment.

Top