Deep dive into the new Firebase JS SDK design

Firebase has kept a stable JavaScript interface for around 5 years now. If you wrote the following line of code 5 years ago, it would still work today.

import firebase from 'firebase/app';
import 'firebase/auth';
firebase.initializeApp({ /* config */ });
const auth = firebase.auth();
auth.onAuthStateChanged(user => { 
  // Check for user status
});

No one wants to rewrite code for the sake of rewriting code. A stable experience is one of the top decision factors when you choose to invest in a library. We have always taken that seriously. Our dedication to a stable API has been an ongoing balance of maintaining existing patterns and adopting new techniques for a better performance and developer experience. But, as Firebase lands more features, the SDK itself becomes larger. In order to reduce size and fit the modern web, we decided to make changes that required a break in our longstanding API.

A modular approach

When the original Firebase library was authored in 2012 the window was the only way to emulate a module system in the browser. It was a common practice to attach a “namespace” for your library on the window, hence window.firebase.

Today we have a native module system in the browser. We have a rich ecosystem of JavaScript module bundlers like Rollup and Webpack that make it easy to efficiently package application code with library code. These tools work best when dealing with module based JavaScript code.

The benefit is an effect called “tree shaking”, which has the ability to eliminate unused code from your application and the libraries you import.

Firebase is changing to follow a modular pattern that provides tree shaking and therefore better performance for your sites. This modular approach removes “side-effect” imports and isolates features as individual functions.

Take a look at the sample below.

import { initializeApp } from 'firebase/app';
import { getAuth, onAuthStateChanged } from 'firebase/auth';

const firebaseApp = initializeApp({ /* config */ });
const auth = getAuth(firebaseApp);
onAuthStateChanged(auth, user => {
  // Check for user status
});

In the snippet above there’s a lot of new pieces, especially around the imports, but there’s also a lot of familiarity. The biggest difference is the organization of the code. It all comes down to namespaces versus modules.

Namespaces and services

Firebase has been available as a JavaScript module for quite some time now. However, our commitment to backwards compatibility and a stable API has kept us from taking advantage of a module first approach. It’s one thing to be used as a module, but it’s another to actually be modular. Any library can work in a module system, but it takes a specific organization to get the benefits of modules.

Firebase has followed a namespace and service pattern.

const firestore = firebase.firestore();
const colRef = firestore.ref('cities');

In this sample firebase is a namespace that contains the firestore service. Other services like Firestore, Authentication, Remote Config, Realtime Database, and Messaging can all also live on the namespace. Each service is also a namespace as well. The firestore service has a set of methods attached, like collection().

Organizing code in this way has its benefits. It’s mentally easier for developers to “dot chain” to see what’s available on a service. This approach was also easier to package before JavaScript had a bonafide module system. As JavaScript modules entered mainstream development, Firebase adapted but without breaking the namespace and service pattern. This kept the library stable but did not take full advantage of what JavaScript modules offer. Take the following code sample into account.

import firebase from 'firebase/app';
import 'firebase/auth';
firebase.initializeApp({ /* config */ });
const auth = firebase.auth();
auth.onAuthStateChanged(user => { 
  // Check for user status
});

Going through, line by line

Let’s go through the sample above, nearly line by line.

import firebase from 'firebase/app';
import 'firebase/auth';

The code starts by importing the firebase/app and firebase/auth packages. Notice though that they’re imported differently. The firebase/app package has an export that gives us methods like initializeApp. However, the firebase/auth package has no exports. This type of import has many names, but I’m going to refer to it as a side-effect import. The side-effect import does not have any exports and typically when used they augment something. What does that mean for Firebase in this example? That’s the firebase export.

firebase.initializeApp({ /* config */ });
const auth = firebase.auth();

It’s hard to tell what a side-effect import does knowing exactly what that importing firebase/auth does. In this case firebase/auth augments the firebase export from firebase/app and creates the firebase.auth namespace. If firebase/auth was not imported there would be an error when accessing firebase/auth.

The sample goes on to monitor a user’s status.

auth.onAuthStateChanged(user => { 
  // Check for user status
});

The onAuthStateChanged method is available because of the side-effect import that augments the firebase export. But as a side-effect, the rest of the features offered by Firebase Authentication are on the namespace, whether you are using them or not. The current page may not handle any sign-in logic, but all 9 of Firebase Auth’s sign-in methods will be included in your bundle. This is because of the namespace pattern.

The ability to chain methods off of the auth namespace is easy to understand and works well with IDEs that provide code completion like VSCode. It does not work well with tree shaking because no current tools are able to detect which methods on the chain are not used or what parts of a side-effect import are not needed. This leads to sites and apps that include more JavaScript than necessary. At Firebase we decided to reorganize our libraries in a modular pattern that supports tree shaking and therefore a smaller footprint in your site.

The new modular library

The new library moves away from the namespace approach and instead towards isolating features in JavaScript functions. Functions are a great way of organizing code and to promote tree shaking. Functions are independent units of code that take in arguments and return new values. Take a look at the new version of the sample code shown above.

import { initializeApp } from 'firebase/app';
import { getAuth, onAuthStateChanged } from 'firebase/auth';

const firebaseApp = initializeApp({ /* config */ });
const auth = getAuth(firebaseApp);
onAuthStateChanged(auth, user => {
  // Check for user status
});

The first thing to notice is that this sample is similar to one shown above. The first sample was eight lines of code, this sample is 8 lines of code. Both samples use two packages and accomplish the same objective: monitor authentication state.

Going through, line by line

Again, let’s go through nearly line by line.

import { initializeApp } from 'firebase/app';
import { getAuth, onAuthStateChanged } from 'firebase/auth';

The side-effect imports are gone. The firebase/auth package provides exports rather than augmenting the firebase namespace. Another thing to note is there is no longer a firebase namespace. The firebase/app package does not return a “catch-all” export that contains all the methods from the package. Instead the package exports individual functions. Tree shaking tools like Rollup know that if a function isn’t used it doesn’t get included in the final build. This is unlike the firebase namespace or side-effect import in the previous sample. Build tools have to include everything when code is organized in that fashion.

The ergonomics of functions are different from a namespace with a bunch of methods attached to it. This is where the new organization really starts to show.

const firebaseApp = initializeApp({ /* config */ });
const auth = getAuth(firebaseApp);

The main difference in the lines above is that there is no more chaining from firebaseApp.auth(). Instead there is a getAuth() function that takes in firebaseApp and returns an auth instance. This may seem strange at first, but it provides more clarity than a side-effect import. Previously, the side-effect import augmented the firebase namespace behind the scenes. It was not clear how an auth service was created and it did not allow for tree shaking. The getAuth() function returns an initialized auth service from the details needed from the firebaseApp. This is a clear process: call a function with an argument, get a result back.

Creating a service this way allows the rest of the features of the library to be tree shake-able as well. Methods are no longer chained. Services are passed as the first argument and the function then uses the details of the auth service to do the rest. The rest of the functions in the firebase/auth package work this way as well. The auth service is the first argument and then what specific function needs next. Passing the auth service allows the other functions to use the details they need without needing a “catch-all” service that contains all the methods.

This new modular approach strips out unused code and builds upon modern web features.

Image with text saying A Smaller Firebase with code snippet below
Image with text saying A Smaller Firebase with code snippet below

The compatibility library

We fully understand that this upgrade is a breaking change which requires you to update existing code. As you would imagine, we have a lot of code internally that uses the Firebase JavaScript library as well. We understood this pain point immediately and created the compatibility library.

The compatibility library provides you the same API as the previous library version (version 8), but uses the new version 9 library under the hood. This allows you to use the new API and the old at the same time as you upgrade your site.

The ability to use these APIs side by side gives you flexibility when upgrading.

import firebase from 'firebase/compat/app';
import 'firebase/compat/auth';

const firebaseApp = firebase.initializeApp({ /* config */ });
const auth = firebaseApp.auth();
auth.onAuthStateChanged(user => { 
  // Check for user status
});

The first thing to notice is that the only change needed was to change the import paths to the compat entry point. From here you can add the new libraries.

import firebase from 'firebase/compat/app';
import { getAuth } from 'firebase/auth';
import 'firebase/compat/auth';

const firebaseApp = firebase.initializeApp({ /* config */ });
const auth = getAuth(firebaseApp);
auth.onAuthStateChanged(user => { 
  // Check for user status
});

Interop mode

In the snippet above, both Firebase Auth library versions are being used. This code works and builds just fine. However, we don’t recommend you use both versions unless you are in the middle of an upgrade.

import firebase from 'firebase/compat/app';
import { getAuth, onAuthStateChanged } from 'firebase/auth';

const firebaseApp = firebase.initializeApp({ /* config */ });
const auth = getAuth(firebaseApp);
onAuthStateChanged(auth, user => { 
  // Check for user status
});

The last step you’ll take when upgrading is to upgrade firebase/app. While upgrading you will need to keep the namespace import from the firebase/compat/app library until each Firebase service has been upgraded to the new version.

import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';

const firebaseApp = initializeApp({ /* config */ });
const auth = getAuth(firebaseApp);
onAuthStateChanged(auth, user => { 
  // Check for user status
});

Once everything is upgraded, you won’t have any dependencies left on the compatibility library and you’ll start to see the full tree shakeable benefits.

Let us know what you think!

We are really excited about the future of this library and the performance benefits it can provide. This library is still in beta and we want to know what you think of these changes. Come visit us on our GitHub discussion board and let us know what you think, if you’re seeing size reductions in your bundles or if you have any questions about upgrading.