DEV Community

MartinJ
MartinJ

Posted on • Updated on

3.1 Getting serious with Firebase V9 - Moving to "ECMA modules"

Last reviewed : May 2023

Introduction

Although this series (see A beginner's guide to Javascript development using Firebase V9. Part 1 - project configuration) is all about the new modular Firebase v9 syntax, we've so far chosen to avoid V9's "ECMA module" libraries in our imports and have used Google's "browser module" libraries instead.

Here's an example of a "browser module" import

import { initializeApp } from 'https://www.gstatic.com/firebasejs/9.1.3/firebase-app.js';
Enter fullscreen mode Exit fullscreen mode

We're now going to replace this with the corresponding mainstream "ECMA module" import :

import { initializeApp } from 'firebase/app';
Enter fullscreen mode Exit fullscreen mode

What are ECMA modules exactly and why might we want to use them? Well, they're the latest episode in a long-running saga directed towards ensuring the efficiency and stability of library code. ECMA stands for European Computer Manufacturers Association and ECMA (or ESM as they're sometimes called) modules follow the standard that the IT world has recently agreed.

Using ECMA modules requires you first to install the library in your project's node_modules folder and then use a "bundler" to build the production webapp. They enable Google to produce much leaner, faster code.

Why haven't we been using ECMA modules before in this course? Well, as you've just seen, they do introduce some minor complications and, previously, you already had quite enough to think about. But, we're trying to be professional now, so, it's time to bite the bullet. ...

Bundling

When you're using "browser module" script imports, your scripts are loaded in their entirety. Google want us to arrange things so that we just load the bits that we actually need.

Unfortunately, achieving this isn't a straightforward task. The "tree structure" of dependencies hidden within modules in your source index.js file has to be unpacked and a completely new "bundled" version of your index.js file constructed. This one will contain all the relevant components in a "ready to roll" form.

Google have declined taking this task on themselves and suggest you engage the services of a third party "bundler". This makes sense when you realise that a specialist in this field will also have suggestions for lots of other useful things that can be done besides just unpacking modules. For example, the bundler might trim out unnecessary code such as comments and blank lines. More dramatically, the bundler can offer to "Minify" your code - see https://www.imperva.com/learn/performance/minification/ for more detail. Finally, the bundler is able to apply "tree shaking" to exclude any bits of the module content that aren't actually being used. Techniques such as this can deliver truly drastic reductions in browser load time.

The main thing to take away from this is that "bundling" produce a packed, self-sufficient version of your code that includes everything that is needed and nothing more.

The downside, of course, is that gearing up for this complicates your development procedures and represents another steep pull on your "learning curve". But the good news is that it's nothing compared to what you've experienced already and, once again, it's free.

The bundler that I'm describing here is called "webpack". This is another "terminal" application and we install it with npm. So here we go with my 6-point guide to using V9 modular code.

Step 1 : Install Webpack

npm i webpack webpack-cli -D
Enter fullscreen mode Exit fullscreen mode

Step 2 : Install your Firebase SDK

When webpack reads your firebase import statements, the first thing it needs to do is get hold of the code for the functions that you've said you want to use. The central repository for these is the Firebase SDK and we now need to put a copy of this somewhere where webpack can find it. This means we need to "install" the Firebase SDK in the terminal window and download the libraries into npm's node_modules folder.

npm i firebase
Enter fullscreen mode Exit fullscreen mode

Step 3 : Edit your source code to reflect the use of "ECMA modules"

Since we're doing nothing more complicated than building a simple webapp, all we have to do to our index.js file is replace the "browser module" references in theimport statements with the corresponding ECMA "javascript" versions. So, for example

import { initializeApp } from 'https://www.gstatic.com/firebasejs/9.1.3/firebase-app.js';
Enter fullscreen mode Exit fullscreen mode

should be replaced by:

import { initializeApp } from 'firebase/app';
Enter fullscreen mode Exit fullscreen mode

Changes for all other imports should follow this pattern, with the exception of "firebase/firestore" where, for simple applications like ours, Google recommends use of the 'firebase/firestore/lite' module (see Cloud Firestore Lite Web SDK)

The index.html source with its <script>reference to the index.js modular script doesn't need much attention either, but this step perhaps requires a little more thought and explanation.

When we actually get round to running webpack, you'll see that the process largely involves simply telling webpack where to find our input index.js and where it should put the output.

webpack schematic
If we were doing something rather more complicated than building a webapp - say developing a shareable library module - I would be talking at this point about main.js and bundle.js files and storing them in src and dist folders. But we really don't need these complications here. Accordingly I suggest that you simply tell webpack to create its output in a file called packed_index.js and to place this in your public folder alongside the input index.js file.

Once you've done this, all you need to do to complete your code preparations is to switch the src reference in index.html from index.js to packed_index.js. You can also remove the type="module" qualifier on the <script> tag if you like. Because the bundled script doesn't contain import statements any more it will work either way.

Step 4 : Configure webpack for production running

To configure webpack to produce a packed and minified version of index.js as described above I suggest you create a webpack_production_config.js file in the project root with the following content:

const path = require('path');

module.exports = {
    mode: 'production',
    entry: './public/index.js',
    output: {
        path: path.resolve(__dirname, 'public'),
        filename: 'packed_index.js'
    }
};
Enter fullscreen mode Exit fullscreen mode

With this in place, you can then run webpack with the following command:

node_modules/.bin/webpack --config webpack_production_config.js
Enter fullscreen mode Exit fullscreen mode

This will produce output as follows:

asset packed_index.js 134 KiB [emitted] [minimized] (name: main) 1 related asset
orphan modules 706 KiB [orphan] 11 modules
runtime modules 221 bytes 1 module
./public/index.js + 11 modules 710 KiB [built] [code generated]
webpack 5.64.1 compiled successfully in 3025 ms
Enter fullscreen mode Exit fullscreen mode

All being well here and with your index.html file pointing to the packed_index.js file created by webpack, you now just need to deploy the project in the usual way.

firebase deploy
Enter fullscreen mode Exit fullscreen mode

When you refresh your browser at https://fir-expts-webapp.web.app, your project should now run exactly as before.

Step 5 : Configure webpack for "debuggable" development operation

Things are now looking good, but if you "inspect" the webapp in the browser, you'll see there's a problem. If you try to inspect the source of the main.js minified javascript supplying the webapp's logic, you'll see that it is an incomprehensible string of opaque code:

Minified index.js

This is because we asked webapp to produce a high-performance "minified" packed_index.js. If you need to set breakpoints on this in order to investigate a problem, you're stuck!

What we need to do, for development purposes, is find a way to supplement the bundled, modular code with some sort of "magnifying glass" that allows us to see the original code behind it.

What we need is something called a "source-map".

Mozilla at How to use a source map describe the arrangement thus:

The JavaScript sources executed by the browser are often transformed in some way from the original sources created by a developer. For example:

  1. sources are often combined and minified to make delivering them from the server more efficient.
  2. JavaScript running in a page is often machine-generated, as when compiled from a language like CoffeeScript or TypeScript.

In these situations, it's much easier to debug the original source, rather than the source in the transformed state that the browser has downloaded. A source map is a file that maps from the transformed source to the original source, enabling the browser to reconstruct the original source and present the reconstructed original in the debugger.

All we need to do in order to achieve this in webpack is create a webpack_development_config.js version of our original config file with the following content:

const path = require('path');

module.exports = {
    mode: 'development',
    devtool: 'eval-source-map',
    entry: './public/index.js',
    output: {
        path: path.resolve(__dirname, 'public/'),
        filename: 'main.js'
    }
};
Enter fullscreen mode Exit fullscreen mode

Here, the mode" parameter value has been changed to "development" to alert webpack to throttle back on some of its more sophisticated packing processes.

More importantly, a "devtool" parameter has been added to tell webpack that we want to create a source map of type 'eval-source-map'.

Webpack's 'eval-source-map' devtool facility is one of a family of similar source map types, each providing different combinations of functionality and performance. Source-map build times, in particular, may be an issue for large projects. The 'eval-source-map' specification is recommended as a good all-purpose version of the tool. (see Devtool)

If you run webpack with the new config file:

node_modules/.bin/webpack --config webpack_development_config.js
Enter fullscreen mode Exit fullscreen mode

You'll now see output as follows:

asset packed_index.js 1.89 MiB [emitted] (name: main)
runtime modules 891 bytes 4 modules
modules by path ./node_modules/ 706 KiB
  modules by path ./node_modules/@firebase/ 694 KiB
    modules by path ./node_modules/@firebase/auth/dist/esm2017/*.js 369 KiB
      ./node_modules/@firebase/auth/dist/esm2017/index.js 2.08 KiB [built] [code generated]
      ./node_modules/@firebase/auth/dist/esm2017/index-8593558d.js 367 KiB [built] [code generated]
    5 modules
  modules by path ./node_modules/firebase/ 976 bytes
    ./node_modules/firebase/app/dist/index.esm.js 826 bytes [built] [code generated]
    ./node_modules/firebase/auth/dist/index.esm.js 70 bytes [built] [code generated]
    ./node_modules/firebase/firestore/lite/dist/index.esm.js 80 bytes [built] [code generated]
  ./node_modules/tslib/tslib.es6.js 11.5 KiB [built] [code generated]
./public/index.js 3.84 KiB [built] [code generated]
webpack 5.64.1 compiled successfully in 659 ms
Enter fullscreen mode Exit fullscreen mode

You now simply deploy as before

firebase deploy
Enter fullscreen mode Exit fullscreen mode

When you reloaded the webapp, you'll find that it runs exactly as before but, when you inspect it in the browser, you'll see that the project's page-structure is rather more complicated. If you dig into this, you'll find your mapped code in a file with a name something like index.js?fcdd buried inside a public folder inside a root folder with a name based on your Project_id (firexptsapp in this instance).

Mapped index.js

When you've located this file, you'll find you can use it to set breakpoints and perform debugging tasks in the usual way.

Step 6 : Create yourself some script files to make life easier

Once you've moved to modular scripts, you'll have to run webpack before every deploy, regardless of whether you're targeting your development environment or your live environment. (Note, if you ever find your webapp is throwing a Relative references must start with either /'', ./'', or firebase error message it's probably because you're somehow running an "un-webpacked" copy of your modular index.js).

But those webpack build commands are a bit tedious to type and, when you're doing repeated builds, it's actually quite easy to forget that you need to follow each one with a deploy command.

To save yourself time and stop yourself making stupid errors I recommend you create yourself a couple of script files:

build_for_production.ps1, with content:

    node_modules/.bin/webpack --config webpack_production_config.js
    firebase deploy
Enter fullscreen mode Exit fullscreen mode

and build_for_development.ps1, with content:

    node_modules/.bin/webpack --config webpack_development_config.js
    firebase deploy
Enter fullscreen mode Exit fullscreen mode

When using Powershell in VSCode, you'll find that you can run the appropriate script by opening it in the editor and then selecting "Run Active File" on the Terminal tab. This is a great time-saver!

Moving On

If you've digested all of my preceding advice on webapp development with Firebase and are now running a professional-looking modular V9 application in the Google Cloud, you may be wondering how you will maintain this into the future. How do you test changes when you've only got a "production" platform to work on?

In this case, you may be interested in the next post in this series - see "3.2 Using the Firebase emulators" in the series index.

Top comments (0)