Achieve top Lighthouse LCP Scores
Google’s Firebase team recently released a new Modular Firebase Web SDK which promises to reduce the amount of Firebase code in your app by making it “tree-shakable”. This means your bundler only needs to add the code you actually use, not the entire library.
It’s defintiely a step in the right direction, but slightly disappointing that we didn’t get anything truly, “svelte” erm … I’ll say “tiny”, such as this 2kb firebase auth package because the Firebase bundles still become pretty large pretty quickly.
Even worse, they no longer appear to offer the option to just load the Firebase libs from a CDN. Even though they were big, you could at least lazy-load them so they weren’t in your app bundle at all. Now they push you to bundling their code in your app which of course makes things simpler to implement but can cause performance issues.
Adding several hundred kilobytes of JavaScript to your app can slow your startup time, limit your Lighthouse Score and cripple your Core Web Vitals. Ouch. No one wants that. But how do you use the new Modular Firebase SDK and still achieve fast app startup times and lazy-loading?
We’ll explore how to use Firebase Auth specifically with Svelte / SvelteKit. Our aim is to make the Firebase code small and lazy, so it is only ever loaded if it’s needed and it doesn’t slow down our app startup. At the same time, it should hopefully be a useful guide on how Svelte Stores work and can be used.
First of all, what is a Svelte Store? Well, it’s a simple way to share some state within our app which will trigger UI updates anytime the state changes. Stores are small, elegant but amazingly powerful as we’ll see.
Let’s start with our basic auth store. What would we want it to contain? Well, the auth state for starters together with some methods to allow us to trigger signing in and signing out. So let’s start with this simple stubbed out implementation:
import { readable } from 'svelte/store'
function createAuth() {
const { subscribe } = readable(undefined)
function sign_in() {
// trigger sign-in
}
function sign_out() {
// trigger sign-out
}
return {
subscribe,
sign_in,
sign_out,
}
}
export const auth = createAuth()
I like to encapsulate the store code inside a factory function, in this case createAuth
, and we call that to create the auth
instance that we can import in the rest of the app. The value of the auth store can be used in Svelte components by prefixing it with a $
symbol and Svelte automatically handles subscribing and unsubscribing for us. Any time the store value changes, anything depending on it will be updated to. Magic!
Here’s how we might import it, render the state value and call the methods it provides:
<script lang="ts">
import { auth } from '$lib/auth'
</script>
Auth state is {$auth}
<button on:click={auth.sign_in}>Sign In</button>
<button on:click={auth.sign_out}>Sign Out</button>
Of course right now that’s going to always show that the auth state is undefined
because that is the value used to initialize the store. There are actually three auth states to handle with Firebase - authenticated (signed in), unauthenticated (signed out) and unknown. That’s because there is always a period before the firebase lib has initialized and checked the auth state where the state is really undetermined, so undefined
seems like the perfect way to represent it. We’re going to use null
to indicate that the user is signed out and will set the Firebase user object when the user is signed in.
An alternative would be to have a separate flag to indicate if the status was known yet or store an unresolved Promise
in the store that is resolved when the status is known. I like using undefined
as it’s simple, a single value but allows for all three states.
Worth noting is that our store is read-only. It is of course created as a readable
store, which means it can’t be updated externally even from within the createAuth
function where it’s defined. But even if it were to be initialized as a writable
store the fact that we only return the subscribe
function is what makes it read-only to the rest of the Svelte app. If a store is managing it’s own state, it shouldn’t allow itself to be updated from outside.
This is important - if you don’t want someone to mis-use your store, always think about whether it should be writable. In this case, someone may imagine that setting the state to null would sign the user out - it wouldn’t, but it would make for an inconsistent UX. Even if that someone is you, it helps to avoid accidental bugs (e.g. from a typo making a comparison into an assignment).
// don't make stores that should manage their own state writable!
// this would just make for an inconsistent UI
$auth = null
So if the store is only readable, how do we ever set the auth state inside it? Well, the next piece of magic is that Svelte stores can take an additional start / stop function as a parameter. This function is run anytime the first subscriber attaches to the store and returns a function that is called whenever the last subscriber disconnects. A set
function is passed in (what a writable
store returns) which allows the store state to be set, even asynchronously, so the store can do it’s own thing. It’s very simple, but incredibly powerful.
This is the function signature showing how it could log when a subscriber first connects and then when the last subscriber disconnects. Nothing would happen if additional subscribers connect and disconnect in between.
const { subscribe } = readable<User>(undefined, set => {
console.log('someone is watching us!')
return () => console.log('where did everybody go?')
})
Let’s step through what our store start / stop function is going to do.
First, we are going to want to return the stop function that can be called when all subscribers have disconnected and can stop us listening to the firebase auth state. You might think that the Auth Status will always be displayed, so it doesn’t matter, but it’s good practice to “do things right” and the same patterns will apply to other stores later (e.g. subscribing to data from Firestore).
Initially, this will be a no-op function and we’ll return it at the end. The reason for making it a no-op is that there’s a chance that something connects and disconnects before other parts of the code has executed, so it needs to be defined because it would still be called.
const { subscribe } = readable(undefined, set => {
let unsubscribe = () => {}
// TODO
return unsubscribe
})
The code that we’re going to run is asynchronous, so to allow us to use the nicer async
/ await
syntax we’re going to add an asynchronous function and call it (we can’t make the start / stop function itself async
). The firebase auth code only runs on the client browser so to keep this store SSR friendly we’re going to take advantage of SvelteKit and use the browser
flag it provides. On the server, this code will be tree-shaken out and won’t execute. While we’re adding an import for that, let’s also import the Firesbase User
type so the rest of our code can use strong-typing when accessing the store:
import { browser } from '$app/env'
import type { User } from 'firebase/auth'
// ...other code...
const { subscribe } = readable<User>(undefined, set => {
let unsubscribe = () => {}
async function init() {
// TODO
}
if (browser) {
init()
}
return unsubscribe
})
Finally, on to the implementation itself! For this we want to dynamically import the firebase auth package but there are actually two parts to this. First, we need to initialize a firebase app and because this can be used by all firebase services (auth, firestore, storage etc…) we will separate this out into it’s own small module:
import { initializeApp } from 'firebase/app'
export const app = initializeApp({
apiKey: "ABCaSyA6xqywOoYGNK5_1234_999ABCXYZ123",
authDomain: "example.firebaseapp.com",
databaseURL: "https://example.firebaseio.com",
projectId: "example",
storageBucket: "example.appspot.com",
messagingSenderId: "9387492384792",
appId: "1:8347625983984:web:de5a90cfd28dc8bf"
})
We’ll import this inside our init
function together with the functions we need to use from the firebase/auth
package:
const { app } = await import('./app')
const { getAuth, onAuthStateChanged } = await import('firebase/auth')
Note that we could avoid the cumulative latency of awaiting them in sequence by instead using await Promise.all([ ... ])
to wait for them to load in parallel, but the code would be more complex and the gain is minor in this case.
The getAuth
function simply initializes and returns the firebase auth service for the app, then the onAuthStateChanged
function provides a listener to the auth state.
const auth = getAuth(app)
unsubscribe = onAuthStateChanged(auth, set)
Anytime the auth state changes, such as a user signing in or out, the function passed to it will be called. In this case, we simply want to set the store state to the user which will either be a Firebase User
object or null, so we just pass on the set
function that the store provided to our start / stop function.
This is equivalent to the slightly more verbose:
unsubscribe = onAuthStateChanged(auth, user => set(user))
We also now re-set the subsubscribe variable to the return value of the onAuthStateChanged
function. This provides the way for our listener to stop handling updates when the final subscriber disconnects.
This would also be the place to handle more complex scenarios such as exchanging the firebase auth token for a custom minted one or calling an endpoint to set a server-side session cookie to enable Server Side Rendering.
Now we’ll move on to the custom store methods that will allow triggering sign-in and sign-out of firebase auth. Because we are going to use the same firebase auth service in these it makes sense to change our earlier init
function slightly and capture the auth
variable at the createAuth
scope. Although we could re-import and re-call getAuth
(and it wouldn’t be as inefficient as it sounds) it is simply exta code and simpler this way.
Implementing the custom functions is quite straightforward. We dynamically import the methods that they use and call them. Currently, all the firebase auth code ends up bundled into one file, but it’s possible in future that Vite / SvelteKit will conspire to make our imports even more granular for added efficency in future.
async function sign_in() {
const { signInWithRedirect, GoogleAuthProvider } = await import('firebase/auth')
await signInWithRedirect(auth, new GoogleAuthProvider())
}
async function sign_out() {
const { signOut } = await import('firebase/auth')
await signOut(auth)
}
Before we show the complete code, let’s go back to the auth status and show a proper example with the three different states being handled. Any reference to $auth
will be completely typed and provide all the Firebase User
object properties.
<script lang="ts">
import { auth } from './firebase/auth'
</script>
{#if $auth === undefined}
<p>Waiting auth status</p>
{:else}
{#if $auth === null}
<p>Anonymous Visitor</p>
<button on:click={auth.sign_in}>Sign In</button>
{:else}
<p>Welcome {$auth.displayName} ({$auth.email}})</p>
<button on:click={auth.sign_out}>Sign Out</button>
{/if}
{/if}
It’s also possible to make an auth component that will render a <slot>
if the user is authenticated or a login component if they are not, as a simple auth-guard. This works great if you have a part of your app that requires authentication (esp. if you can put it in a SvelteKit __layout.svelte
component)
One final question, is why we lazy-load the firebase libs and not the Auth Status component itself? Well, for a few reasons:
Here is the completed auth store code. You can also checkout a more complete svelte example github repo.
import { readable } from 'svelte/store'
import { browser } from '$app/env'
import type { Auth, User } from 'firebase/auth'
function createAuth() {
let auth: Auth
const { subscribe } = readable<User>(undefined, set => {
let unsubscribe = () => {}
async function init() {
if (browser) {
const { app } = await import('./app')
const { getAuth, onAuthStateChanged } = await import('firebase/auth')
auth = getAuth(app)
unsubscribe = onAuthStateChanged(auth, set)
} else {
// TODO: set auth on server from session (?)
}
}
init()
return unsubscribe
})
async function sign_in() {
const { signInWithRedirect, GoogleAuthProvider } = await import('firebase/auth')
await signInWithRedirect(auth, new GoogleAuthProvider())
}
async function sign_out() {
const { signOut } = await import('firebase/auth')
await signOut(auth)
}
return {
subscribe,
sign_in,
sign_out,
}
}
export const auth = createAuth()
In a future article, I’ll share some tips and methods for consuming Firebase Firestore data using multiple Svelte Stores.