Skip to content
This repository was archived by the owner on Apr 17, 2023. It is now read-only.

Commit

Permalink
Load Firebase asynchronously. (#77)
Browse files Browse the repository at this point in the history
Update src/firebase/index.ts to export getFirebase, getAuth,
getFirestore, getFunctions, and getFirebaseUI functions that
load the corresponding Firebase modules asynchronously. This
lets webpack create separate chunks for each piece of
Firebase, reducing the app entrypoint from 1.12 MiB to 593
KiB.

It also improves Lighthouse measurements for the login page
on a simulated 3G network with 4x CPU slowdown:

First Contentful Paint: 3.1s -> 2.4s
Time to Interactive:    3.5s -> 3.0s
First Meaningful Paint: 3.2s -> 2.4s
First CPU Idle:         3.3s -> 2.9s

I'd expected an improvement here, since the login page
doesn't depend on Firestore.

It's harder to tell if there's a benefit on the other pages.
They all require login, which means that "Clear Storage"
needs to be unchecked in Chrome's Dev Tools Audit tab, which
I think means that the cache is preserved, so download time
doesn't get measured. See
GoogleChrome/lighthouse#2599 for
more discussion of this. It stands to reason that there
would be less benefit there, though, since all of those
pages require both the auth and Firestore modules to be
loaded before they can do anything meaningful.
  • Loading branch information
derat authored Aug 17, 2019
1 parent f785858 commit 4561c4e
Show file tree
Hide file tree
Showing 21 changed files with 320 additions and 235 deletions.
29 changes: 12 additions & 17 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<template>
<v-app>
<Toolbar v-if="signedIn()" />
<Toolbar v-if="signedIn" />
<v-content>
<!-- Ideally, this could be wrapped in <keep-alive include="Routes"> to
keep the slow-to-render Routes view alive after navigating away from
Expand Down Expand Up @@ -39,15 +39,18 @@

<script lang="ts">
import { Component, Mixins } from 'vue-property-decorator';
import { auth } from '@/firebase/auth';

import Perf from '@/mixins/Perf.ts';
import { getAuth } from '@/firebase';
import Perf from '@/mixins/Perf';
import Toolbar from '@/components/Toolbar.vue';

@Component({
components: { Toolbar },
})
export default class App extends Mixins(Perf) {
// Whether the user is currently signed in or not.
signedIn = false;

// Snackbar text currently (or last) displayed.
snackbarText = '';
// Color currently used for the snackbar.
Expand All @@ -58,20 +61,6 @@ export default class App extends Mixins(Perf) {
// Amount of time to display snackbar before autohiding, in milliseconds.
snackbarTimeoutMs = 0;

// This apparently needs to be a method rather than a computed property (i.e.
// don't add the 'get' keyword to make it a getter). Otherwise, it doesn't
// seem to pick up changes to auth.currentUser. Another option that seems to
// work is explicitly asking Firebase for auth state changes in mounted():
//
// auth.onAuthStateChanged(user => {
// this.signedIn = !!user;
// });
//
// This is a bit less code, though.
signedIn() {
return !!auth.currentUser;
}

// Displays the snackbar in response to a request from a component.
onMessage(msg: string, color: string, timeout: number) {
this.snackbarText = msg;
Expand All @@ -81,6 +70,12 @@ export default class App extends Mixins(Perf) {
}

mounted() {
getAuth().then(auth => {
auth.onAuthStateChanged(user => {
this.signedIn = !!user;
});
});

this.$nextTick(() => {
const data: Record<string, any> = { userAgent: navigator.userAgent };
try {
Expand Down
2 changes: 1 addition & 1 deletion src/components/ClimbDropdown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Vue from 'vue';
import Vuetify from 'vuetify';

import ClimbDropdown from './ClimbDropdown.vue';
import { ClimbState } from '@/models.ts';
import { ClimbState } from '@/models';

Vue.use(Vuetify);

Expand Down
2 changes: 1 addition & 1 deletion src/components/RouteList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Vuetify from 'vuetify';

import ClimbDropdown from './ClimbDropdown.vue';
import RouteList from './RouteList.vue';
import { ClimbState, ClimberInfo, SetClimbStateEvent } from '@/models.ts';
import { ClimbState, ClimberInfo, SetClimbStateEvent } from '@/models';

Vue.use(Vuetify);

Expand Down
14 changes: 8 additions & 6 deletions src/components/Toolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { auth } from '@/firebase/auth';
import { getAuth } from '@/firebase';

// An entry in the navigation drawer.
interface NavItem {
Expand Down Expand Up @@ -69,11 +69,13 @@ export default class Toolbar extends Vue {
text: 'Sign out',
icon: 'exit_to_app',
method: () => {
auth.signOut().then(() => {
// It makes no sense to me, but this.$router produces an exception
// here: "TypeError: Cannot read property '_router' of undefined".
// this.$root.$router works, though...
this.$root.$router.replace('login');
getAuth().then(auth => {
auth.signOut().then(() => {
// It makes no sense to me, but this.$router produces an exception
// here: "TypeError: Cannot read property '_router' of undefined".
// this.$root.$router works, though...
this.$root.$router.replace('login');
});
});
},
},
Expand Down
20 changes: 0 additions & 20 deletions src/firebase/auth.ts

This file was deleted.

20 changes: 0 additions & 20 deletions src/firebase/firestore.ts

This file was deleted.

11 changes: 0 additions & 11 deletions src/firebase/functions.ts

This file was deleted.

65 changes: 65 additions & 0 deletions src/firebase/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright 2019 Daniel Erat and Niniane Wang. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// This file exports methods that can be used to load different parts of
// Firebase asynchronously.

// Loads the core Firebase library asynchronously.
// Outside code should only call this if it needs access to Firebase's weird
// constant values, e.g. firebase.auth.GoogleAuthProvider or
// firebase.firestore.FieldValue.Delete().
export function getFirebase() {
return import(/* webpackChunkName: "firebase-app" */ 'firebase/app');
}

// Loads Firebase auth asynchronously.
export function getAuth(): Promise<firebase.auth.Auth> {
return Promise.all([
import(/* webpackChunkName: "firebase-init" */ './init'),
import(/* webpackChunkName: "firebase-auth" */ 'firebase/auth'),
]).then(([init, _]) => init.app.auth());
}

// Tracks whether first-time Firestore setup has been performed.
let initializedFirestore = false;

// Loads Cloud Firestore asynchronously.
export function getFirestore(): Promise<firebase.firestore.Firestore> {
return Promise.all([
import(/* webpackChunkName: "firebase-init" */ './init'),
import(/* webpackChunkName: "firebase-firestore" */ 'firebase/firestore'),
]).then(([init, _]) => {
if (!initializedFirestore) {
// Enable persistence the first time the module is loaded:
// https://firebase.google.com/docs/firestore/manage-data/enable-offline
init.app
.firestore()
.enablePersistence()
.catch(err => {
import('@/log').then(log => {
// 'failed-precondition' means that multiple tabs are open.
// 'unimplemented' means a lack of browser support.
log.logError('firestore_persistence_failed', err);
});
});
initializedFirestore = true;
}

return init.app.firestore();
});
}

// Loads Cloud Functions asynchronously.
export function getFunctions(): Promise<firebase.functions.Functions> {
return Promise.all([
import(/* webpackChunkName: "firebase-init" */ './init'),
import(/* webpackChunkName: "firebase-functions" */ 'firebase/functions'),
]).then(([init, _]) => init.app.functions());
}

// Loads FirebaseUI asynchronously. Note that this isn't part of the core
// Firebase library.
export function getFirebaseUI() {
return import(/* webpackChunkName: "firebaseui" */ 'firebaseui');
}
5 changes: 2 additions & 3 deletions src/firebase/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// This file initializes Firebase. It is imported for its side effects by other
// files in this directory.
// This file initializes the Firebase app.

import firebase from 'firebase/app';

firebase.initializeApp({
export const app = firebase.initializeApp({
apiKey: process.env.VUE_APP_FIREBASE_API_KEY,
authDomain: process.env.VUE_APP_FIREBASE_AUTH_DOMAIN,
databaseURL: process.env.VUE_APP_FIREBASE_DATABASE_URL,
Expand Down
20 changes: 15 additions & 5 deletions src/firebase/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,12 +258,20 @@ const mockDeleteSentinel = {};

jest.mock('firebase');
jest.mock('firebase/app', () => {
const obj = {
initializeApp: () => {},
// This implementation is gross, since we'll return a new object each time
// e.g. firebase.firestore() is called. I think we can get away with it for
// now since all of these are just wrappers around groups of methods, though.
const app = {
initializeApp: () => app,
auth: () => ({
get currentUser() {
return MockFirebase.currentUser;
},
onAuthStateChanged: (observer: any) => {
new Promise(() => {
observer(MockFirebase.currentUser);
});
},
}),
firestore: () => ({
batch: () => new MockWriteBatch(),
Expand All @@ -272,11 +280,13 @@ jest.mock('firebase/app', () => {
enablePersistence: () => new Promise(resolve => resolve()),
}),
};
// Also set weird sentinel values that live on the firestore method.
(obj.firestore as any).FieldValue = {

// Also set weird sentinel value that lives on the firestore method.
(app.firestore as any).FieldValue = {
delete: () => mockDeleteSentinel,
};
return obj;

return app;
});

// TODO: Is this actually necessary? These modules are imported for their side
Expand Down
15 changes: 6 additions & 9 deletions src/log/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// found in the LICENSE file.

import { Logger, LogFunc } from './logger';
import { getAuth, getFunctions } from '@/firebase';

enum LogDest {
STACKDRIVER,
Expand All @@ -20,11 +21,9 @@ let devLogDest = LogDest.NONE;
// Returns an appropriate function to pass to the default Logger.
function getLogFunc(): LogFunc | Promise<LogFunc> {
if (isProd || (isDev && devLogDest == LogDest.STACKDRIVER)) {
// Import the module asynchronously and return a promise so that importing
// this module doesn't require synchronously loading bulky Firebase code.
return import('@/firebase/functions').then(fn =>
fn.functions.httpsCallable('Log')
);
// Return a promise so that importing this module doesn't require
// synchronously loading bulky Firebase code.
return getFunctions().then(functions => functions.httpsCallable('Log'));
}
if (isDev && devLogDest == LogDest.CONSOLE) {
return (data?: any) => {
Expand All @@ -40,11 +39,9 @@ const defaultLogger = new Logger('log', getLogFunc());
// Helper function that sends a log message to Stackdriver.
// See the Logger class's log method for more details.
function log(severity: string, code: string, payload: Record<string, any>) {
// Import the module asynchronously so that importing this module doesn't
// require synchronously loading bulky Firebase code.
import('@/firebase/auth')
getAuth()
.then(auth => {
const user = auth.getUser();
const user = auth.currentUser;
if (!user) {
defaultLogger.log(severity, code, payload);
return;
Expand Down
20 changes: 11 additions & 9 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import { auth } from '@/firebase/auth';
import { logError } from '@/log';
import { getAuth } from '@/firebase';

const isTestEnv = process.env.NODE_ENV == 'test';

Expand Down Expand Up @@ -68,12 +68,14 @@ import router from '@/router';
// authenticated or not. Otherwise, router.beforeEach may end up trying to
// inspect Firebase's auth state before it's been initialized.
let app: Vue | null = null;
auth.onAuthStateChanged(() => {
if (!app) {
app = new Vue({
router,
i18n,
render: h => h(App),
}).$mount('#app');
}
getAuth().then(auth => {
auth.onAuthStateChanged(() => {
if (!app) {
app = new Vue({
router,
i18n,
render: h => h(App),
}).$mount('#app');
}
});
});
Loading

0 comments on commit 4561c4e

Please sign in to comment.