I'm working on a Firebase app called That's Groce! based on preact-cli
, with TypeScript, and I wanted to see how it appears with or without Firestore and Authenticated lazy-loaded.
In the root, there's an app.tsx
that used look like this:
import { FunctionalComponent, h } from "preact";
import { useState, useEffect } from "preact/hooks";
import firebase from "firebase/app";
import "firebase/auth";
import "firebase/firestore";
import { firebaseConfig } from "./firebaseconfig";
const app = firebase.initializeApp(firebaseConfig);
const App: FunctionalComponent = () => {
const [auth, setAuth] = useState<firebase.auth.Auth | null>(null);
const [db, setDB] = useState<firebase.firestore.Firestore | null>(null);
useEffect(() => {
const appAuth = app.auth();
setAuth(appAuth);
appAuth.onAuthStateChanged(authStateChanged);
const db = firebase.firestore();
setDB(db);
}, []);
...
While this works, it does make a really large bundle when both firebase/firestore
and firebase/auth
imported in the main bundle. In fact, it looks like this:
▶ ls -lh build/*.esm.js -rw-r--r-- 1 peterbe staff 510K Sep 1 14:13 build/bundle.0438b.esm.js -rw-r--r-- 1 peterbe staff 5.0K Sep 1 14:13 build/polyfills.532e0.esm.js
510K is pretty hefty to have to ask the client to download immediately. It's loaded like this (in build/index.html
):
<script crossorigin="anonymous" src="/bundle.0438b.esm.js" type="module"></script>
<script nomodule src="/polyfills.694cb.js"></script>
<script nomodule defer="defer" src="/bundle.a4a8b.js"></script>
To lazy-load this
To lazy-load the firebase/firestore
and firebase/auth
you do this instead:
...
const App: FunctionalComponent = () => {
const [auth, setAuth] = useState<firebase.auth.Auth | null>(null);
const [db, setDB] = useState<firebase.firestore.Firestore | null>(null);
useEffect(() => {
import("firebase/auth")
.then(() => {
const appAuth = app.auth();
setAuth(appAuth);
appAuth.onAuthStateChanged(authStateChanged);
})
.catch((error) => {
console.error("Unable to lazy-load firebase/auth:", error);
});
import("firebase/firestore")
.then(() => {
const db = firebase.firestore();
setDB(db);
})
.catch((error) => {
console.error("Unable to lazy-load firebase/firestore:", error);
});
}, []);
...
Now it looks like this instead:
▶ ls -lh build/*.esm.js -rw-r--r-- 1 peterbe staff 173K Sep 1 14:24 build/11.chunk.b8684.esm.js -rw-r--r-- 1 peterbe staff 282K Sep 1 14:24 build/12.chunk.3c1c4.esm.js -rw-r--r-- 1 peterbe staff 56K Sep 1 14:24 build/bundle.7225c.esm.js -rw-r--r-- 1 peterbe staff 5.0K Sep 1 14:24 build/polyfills.532e0.esm.js
The total sum of all (relevant) .esm.js
files is the same (minus a difference of 430 bytes).
But what does it really look like? The app is already based around that
const [db, setDB] = useState<firebase.firestore.Firestore | null>(null);
so it knows to wait until db
is truthy and it displays a <Loading/>
component until it's ready.
To test how it loads I used the Chrome Performance devtools with or without the lazy-loading and it's fairly self-explanatory:
Clearly, the lazy-loaded has a nicer pattern in that it breaks up the work by the main thread.
Conclusion
It's fairly simple to do and it works. The main bundle becomes lighter and allows the browser to start rendering the Preact component sooner. But it's not entirely obvious if it's that much better. The same amount of JavaScript needs to downloaded and parsed no matter what. It's clearly working as a pattern but it's still pretty hard to judge if it's worth it. Now there's more "swapping".
And the whole page is server-side rendered anyway so in terms of immediate first-render it's probably the same. Hopefully, HTTP2 loading does the right thing but it's not yet entirely clear if the complete benefit is there. I certainly hope that this can improve the "Total Blocking Time" and "Time to Interactive".
The other important thing is that not all imports from firebase/*
work in Node because they depend on window
. It works for firebase/firestore
and firestore/auth
but not for firestore/analytics
and firestore/performance
. Now, I can add those lazy-loaded in the client and have the page rendered in Node for that initial build/index.html
.
Comments