Why change?
A new library
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 better performance and developer experience. 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.
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.
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 module. However, our commitment to backwards compatibility and a stable API has kept us from taking advantage of a modular 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 specific organization to be modular.
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
});
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 library 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. 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, 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 of 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.
Again, let's go through nearly line by line.
import { initializeApp } from 'firebase/app';
import { getAuth, onAuthStateChanged } from 'firebase/auth';
The first thing to note is that 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.
An easier upgrade
Change is never easy. Our new library provides new benefits but it's hard to go back and rewrite code that already works. To make that process easier, we're working on a compatibility library that allows you to port your code piece by piece. You won't get all of the tree shaking advantages upfront, but once you match the new modular library you can switch off the compatibility library and rake in the savings. The compatibility library isn't available yet, but stay tuned it's coming soon.
Today the new modular SDK cannot be used alongside the existing namespace based library. If you read the section above, you can understand why. This is what the compatibility library is going to help with.
The size difference
Let's get real for a second and talk size. Firebase has always been a larger library on the web. The web itself is a balance of features and performance and we wanted to make that balance a lot easier for you. This was the biggest reason for us to take on tree shaking. We suspect that no matter what you'll see a sizeable drop in your bundle size. However, we're hoping that with specific use cases you'll see significant size reductions.
The two biggest areas of improvement we have seen in our early studies are with the new firebase/firestore/lite
package (more on that in a bit!) and firebase/auth
. The table below shows a use case, the current SDK version size, the current experimental version size, the size decrease, and the percentage the SDK is lighter than current v8 version.
Firestore
Usecase | v8 | exp | difference | lighter |
---|---|---|---|---|
CRUD + Realtime | 79.8 kB | 53.1 kB | -26.68 kB | 33.45% |
Read data once | 79.8 kB | 15.9 kB | -63.87 kB | 80.06% |
Query data once | 79.8 kB | 16.3 kB | -63.51 kB | 79.61% |
Auth
Usecase | v8 | exp | difference | lighter |
---|---|---|---|---|
Sign in with Email & Password | 61.2 kB | 20.2 kB | -40.96 kB | 66.97% |
Sign in with Redirect: Google Auth | 61.2 kB | 23 kB | -38.12 kB | 62.32% |
Sign in with Phone Number | 61.2 kB | 21.9 kB | -39.25 kB | 64.18% |
Sign in with Anonymous Auth | 61.2 kB | 20.2 kB | -40.95 kB | 66.96% |
Storage
Usecase | v8 | exp | difference | lighter |
---|---|---|---|---|
Upload bytes | 14.9 kB | 9.4 kB | -5.53 kB | 37.02% |
Get download URL | 14.9 kB | 8.1 kB | -6.85 kB | 45.82% |
Firestore & Auth
Usecase | v8 | exp | difference | lighter |
---|---|---|---|---|
Load user & get collection | 145 kB | 39.2 kB | -105.8 kB | 72.97% |
You'll notice that with authentication that each sign in use case listed is over 60% lighter than before. When using firebase/firestore/lite
you can save 80% from the current version of firebase/firestore
if you only need one-time reads. This new pay-as-you-go service allows you to decide what to include in your bundles and make that features and performance balance much easier.
Firestore Lite
Another improvement we wanted to add was various on top of Firestore. Firestore is such a powerful library, it does so many things behind the scenes that we encourage you all to take for granted. Firestore has a complex caching, realtime streaming, persistent storage, multi-tab offline sync, retries, optimistic concurrency, and so much more. But we heard from you all that sometimes you just need to get a collection. For those cases we wanted to make Firestore a simple and light solution, so we created a brand new subpackage: firebase/firestore/lite
.
import { initializeApp } from 'firebase/app';
import { getFirestore, getDocs, collection } from 'firebase/firestore/lite';
import { getAuth, onAuthStateChanged } from 'firebase/auth';
const firebaseApp = initializeApp({ /* */ });
const auth = getAuth(firebaseApp);
const db = getFirestore(firebaseApp);
onAuthStateChanged(auth, user => { });
getDocs(collection('todos')).then(snap => { });
This sample uses firebase/app
, firebase/auth
, and the new firebase/firestore/lite
package. You'll notice that calling getDocs()
has a .then()
function for a one-time data read. Firestore Lite allows you to create, read, update, and delete data with Firestore in a much smaller library. Realtime streaming is not included, but you can always switch back to firebase/firestore
if that's what you need.
Firestore Lite is significantly smaller. As you saw in the table above, it can be 80% lighter. The code sample referenced above is nearly 73% lighter than the current v8
version. We know that you Firestore users will find a great fit for this library.