Using module bundlers with Firebase

JavaScript module bundlers can do many things, but one of their most useful features is the ability to add and use external libraries in your code base. Module bundlers read import paths in your code and combine (bundle) your application-specific code with your imported library code.

From version 9 and higher, the Firebase JavaScript modular API is optimized to work with the optimization features of module bundlers to reduce the amount of Firebase code included in your final build.

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

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

/**
 * getRedirectResult is unused and should not be included in the code base.
 * In addition, there are many other functions within firebase/auth that are
 * not imported and therefore should not be included as well.
 */

This process of eliminating unused code from a library is known as tree shaking. It would be extremely time consuming and error prone to manually remove this code by hand, but module bundlers can automate this removal.

There are many high quality module bundlers in the JavaScript ecosystem. This guide is focused on covering using Firebase with webpack, Rollup, and esbuild.

Get started

This guide requires you to have npm installed in your development environment. npm is used to install and manage dependencies (libraries). To install npm, install Node.js, which includes npm automatically.

Most developers are properly set up once they have installed Node.js. However, there are common problems many developers run into when setting up their environment. If you run into any errors, make sure your environment has the npm CLI and that you have the proper permissions set up so you don't have to install packages as an administrator with the sudo command.

package.json and installing Firebase

Once you have npm installed you will need to create a package.json file at the root of your local project. Generate this file with the following npm command:

npm init

This will take you through a wizard to supply the needed information. Once the file is created it will look similar to the following:

{
  "name": "your-package-name",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {

  }
}

This file is responsible for many different things. This is an important file to familiarize yourself with if you want to learn more about module bundling and building JavaScript code in general. The important piece for this guide is the "dependencies" object. This object will hold a key value pair of the library you have installed and the version it is using.

Adding dependencies is done through the npm install or npm i command.

npm i firebase

When you run npm i firebase, the installation process will update package.json to list Firebase as a dependency:

  "dependencies": {
    "firebase": "^9.0.0"
  },

The key is the name of the library and the value is the version to use. The version value is flexible and can accept a range of values. This is known as semantic versioning or semver. To learn more about semver, see npm's guide about semantic versioning.

Source vs build folders

The code you write is read and processed by a module bundler and then output as a new file or set of files. It's important to separate these two types of files. The code the module bundlers read and process is known as "source" code. The files they output are known as the built or "dist" (distribution) code.

A common setup in code bases is to store source code in a folder called src and the built code in a folder named dist.

- src
 |_ index.js
 |_ animations.js
 |_ datalist.js


- dist
 |_ bundle.js

In the example file structure above, consider that index.js imports both animations.js and datalist.js. When a module bundler processes the source code it will produce the bundle.js file in the dist folder. The bundle.js is a combination of the files in the src folder and any libraries the import as well.

If you are using source control systems such as Git, it is common to ignore the dist folder when storing this code in the main repository.

Entry points

Module bundlers all have a concept of an entry point. You can think of your application as a tree of files. One file imports code from another and so on and so forth. This means that one file will be the root of the tree. This file is known as the entry point.

Let's revisit the previous file structure example.

- src
 |_ index.js
 |_ animations.js
 |_ datalist.js


- dist
 |_ bundle.js
// src/index.js
import { animate } from './animations';
import { createList } from './datalist';

// This is not real code, but for example purposes only
const theList = createList('users/123/tasks');
theList.addEventListener('loaded', event => {
  animate(theList);
});

The src/index.js file is considered the entry point because it begins the imports of all the needed code for the application. This entry point file is used by module bundlers to begin the bundling process.

Using Firebase with webpack

There is no specific configuration needed for Firebase apps and webpack. This section covers a general webpack configuration.

The first step is to install webpack from npm as a development dependency.

npm i webpack webpack-cli -D

Create a file at the root of your local project named webpack.config.js and add the following code.

const path = require('path');

module.exports = {
  // The entry point file described above
  entry: './src/index.js',
  // The location of the build folder described above
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  // Optional and for development only. This provides the ability to
  // map the built code back to the original source format when debugging.
  devtool: 'eval-source-map',
};

Then make sure you have Firebase installed as a dependency.

npm i firebase

Then initialize Firebase in your code base. The following code imports and initializes Firebase in an entry point file and uses Firestore Lite to load a "city" document.

// src/index.js
import { initializeApp } from 'firebase/app';
import { getFirestore, doc, getDoc } from 'firebase/firestore/lite';

const firebaseApp = initializeApp({ /* config */ });
const db = getFirestore(firebaseApp);

async function loadCity(name) {
  const cityDoc = doc(db, `cities/${name}`);
  const snapshot = await getDoc(cityDoc);
  return {
    id: snapshot.id,
    ...snapshot.data(),
  };
}

The next step is to add an npm script to run the webpack build. Open the package.json file and add the following key value pair to the "scripts" object.

  "scripts": {
    "build": "webpack --mode=development"
  },

To run webpack and generate the build folder run the following command.

npm run build

Finally, check the dist build folder. It should contain a file named bundle.js that contains your bundled application and dependency code.

For more information on optimizing your webpack build for production, see their official documentation on the "mode" configuration setting.

Using Firebase with Rollup

There is no specific configuration needed for Firebase apps and Rollup. This section covers a general Rollup configuration.

The first step is to install Rollup and a plugin used to map imports to dependencies installed with npm.

npm i rollup @rollup/plugin-node-resolve -D

Create a file at the root of your local project named rollup.config.js and add the following code.

import { nodeResolve } from '@rollup/plugin-node-resolve';

export default {
  // the entry point file described above
  input: 'src/index.js',
  // the output for the build folder described above
  output: {
    file: 'dist/bundle.js',
    // Optional and for development only. This provides the ability to
    // map the built code back to the original source format when debugging.
    sourcemap: 'inline',
    // Configure Rollup to convert your module code to a scoped function
    // that "immediate invokes". See the Rollup documentation for more
    // information: https://rollupjs.org/guide/en/#outputformat
    format: 'iife'
  },
  // Add the plugin to map import paths to dependencies
  // installed with npm
  plugins: [nodeResolve()]
};

Then initialize Firebase in your code base. The following code imports and initializes Firebase in an entry point file and uses Firestore Lite to load a "city" document.

// src/index.js
import { initializeApp } from 'firebase/app';
import { getFirestore, doc, getDoc } from 'firebase/firestore/lite';

const firebaseApp = initializeApp({ /* config */ });
const db = getFirestore(firebaseApp);

async function loadCity(name) {
  const cityDoc = doc(db, `cities/${name}`);
  const snapshot = await getDoc(cityDoc);
  return {
    id: snapshot.id,
    ...snapshot.data(),
  };
}

The next step is to add an npm script to run the rollup build. Open the package.json file and add the following key value pair to the "scripts" object.

  "scripts": {
    "build": "rollup -c rollup.config.js"
  },

To run rollup and generate the build folder, run the following command.

npm run build

Finally, check the dist build folder. It should contain a file named bundle.js that contains your bundled application and dependency code.

For more information on optimizing your Rollup build for production, see their official documentation on plugins for production builds.

Using Firebase with esbuild

There is no specific configuration needed for Firebase apps and esbuild. This section covers a general esbuild configuration.

The first step is to install esbuild as a development dependency.

npm i esbuild -D

Create a file at the root of your local project named esbuild.config.js and add the following code.

require('esbuild').build({
  // the entry point file described above
  entryPoints: ['src/index.js'],
  // the build folder location described above
  outfile: 'dist/bundle.js',
  bundle: true,
  // Replace with the browser versions you need to target
  target: ['chrome60', 'firefox60', 'safari11', 'edge20'],
  // Optional and for development only. This provides the ability to
  // map the built code back to the original source format when debugging.
  sourcemap: 'inline',
}).catch(() => process.exit(1))

Then initialize Firebase in your code base. The following code imports and initializes Firebase in an entry point file and uses Firestore Lite to load a "city" document.

// src/index.js
import { initializeApp } from 'firebase/app';
import { getFirestore, doc, getDoc } from 'firebase/firestore/lite';

const firebaseApp = initializeApp({ /* config */ });
const db = getFirestore(firebaseApp);

async function loadCity(name) {
  const cityDoc = doc(db, `cities/${name}`);
  const snapshot = await getDoc(cityDoc);
  return {
    id: snapshot.id,
    ...snapshot.data(),
  };
}

The next step is to add an npm script to run esbuild. Open the package.json file and add the following key value pair to the "scripts" object.

  "scripts": {
    "build": "node ./esbuild.config.js"
  },

Finally, check the dist build folder. It should contain a file named bundle.js that contains your bundled application and dependency code.

For more information on optimizing esbuild for production, see their official documentation on minification and other optimizations.