4

Problem

After researching the Firebase docs, videos, StackOverflow, lots of articles, ... organizing multiple (lots of) cloud functions in an "easy" way is not obvious. Especially, since the official Firebase docs do not provide a clear vision/recommendation. Actually, the real problem is the lack of clear documentation on how to setup a Firebase project with lots of functions right from the beginning.

I am trying to find an easy approach considering the following points:

  • Based on Firebase docs (that every new Firebase user is reading)
  • One single index.js requirement for Firebase CLI deployments
  • Using plain JavaScript over TypeScript, not too much syntactic magic sugar
  • Transparent control over which files/functions are included/referenced in the index.js
  • Scalability in terms of # of functions
    • Going from initial setup (beginner using Firebase docs) to advanced, how to migrate the structure?
    • Minimize effort for adding/deploying more and more cloud functions
  • Performance: Lazy loading/initialization the admin SDK + ensure it's initialized only once
    • Minimizing server instance cold-start time
  • Easy to explain, easy to use, easy to maintain (sure it's a bit subjective)

Solution?

Coming from manually deployed cloud functions to a simple initial CLI setup using JavaScript, I tried the following file structure with some success:

[project]/
- functions/
  - index.js
  - src/
    - functionA.js
    - functionB.js
    - ...
...

index.js

Structure based on the official docs: https://firebase.google.com/docs/functions/organize-functions

const functions = require('firebase-functions');

const functionA = require('./src/FunctionA');
exports.FunctionA = functionA.FunctionA;

const functionB = require('./src/FunctionB');
exports.FunctionB = functionA.FunctionB;

FunctionA.js

Using https://gist.github.com/saintplay/3f965e0aea933a1129cc2c9a823e74d7#file-index-js

const functions = require('firebase-functions');
const admin = require('firebase-admin');

// Prevent firebase from initializing twice
try { admin.initializeApp() } catch (e) {console.log(e);}

exports.FunctionA = ...

FunctionB.js

const functions = require('firebase-functions');
const admin = require('firebase-admin');

// Prevent firebase from initializing twice
try { admin.initializeApp() } catch (e) {console.log(e);}

exports.FunctionB = ...

Question(s)

  • Is this a practicable and viable solution?
  • Is there any performance concern compared to async/await imports?
  • Is the try-catch block around admin.initializeApp() a clean implementation?
  • What would be the Typescript equivalent solution?
  • Cold-start time: When deploying via Firebase CLI, I notice in the Google Cloud Console "Cloud Functions" section that all cloud function instances contain source code of all other functions
    • When creating functions manually in the cloud console, each function only has it's own piece of code to be deployed/initialized/executed
  • Any suggestions on optimization? (e.g. maintainability of index.js)
dimib
  • 716
  • 6
  • 9
  • If you have a lynda.com account, there is an excellent relatively new course on Firebase Functions with a walk-through of how to set up multiple functions. – redshift Jan 02 '21 at 12:12
  • I have just shared my POV [in this post](https://stackoverflow.com/questions/43486278/how-do-i-structure-cloud-functions-for-firebase-to-deploy-multiple-functions-fro/69075998#69075998), in case you want to check it out. – Fabio Moggi Sep 06 '21 at 15:03

1 Answers1

1

I will try to answer your questions all together:

Organizing your functions files and folders is a matter of opinion. You can always require other functions to your index.js using the require() method. You don't need to follow the official documentation regarding this. check out this nice solution from another stackoverflow question

what you need to give attention to is not to include require statements in the global scope that other functions doesn't use. like if you have a function that use a Nodejs library and another function doesn't use this library and you required this library in the global scope then the cold start to fetch the library will effect both functions:

const functions = require('firebase-functions');
const admin = require('firebase-admin');

// need the admin sdk
exports.functionA = ...

// doesn't need the admin sdk
exports.functionB = ...

in the example above the cold start to get the admin sdk will apply on both functions. you can optimize it by:

const functions = require('firebase-functions');


// need the admin sdk
exports.functionA = functions.https.onRequest((request, response) => {

const admin = require('firebase-admin');
 ...
})


// doesn't need the admin sdk
exports.functionB = ...

We required the admin sdk only in the function that needs it to reduce cold start and optimize memeory usage.

you can even improve this more by using dynamic imports in typescript which will fetch the library or the module code at the time of invocation rather than the static import with require in JS.

import * as functions from 'firebase-functions'

// this variable will help to initialize the admin sdk once
let is_admin_initialized = false

export const functionA =
functions.https.onRequest(async (request, response) => {
    // dynamically import the Admin SDK here
    const admin = await import('firebase-admin')

    // Only initialize the admin SDK once per server instance
    if (!is_admin_initialized) {
        admin.initializeApp()
        is_admin_initialized = true
    }

  ...

})

regarding:

try { admin.initializeApp() } catch (e) {console.log(e);}

I believe this is a valid implementation and this will check for the error:

The default Firebase app already exists. This means you called initializeApp() more than once without providing an app name as the second argument. In most cases you only need to call initializeApp() once. But if you do want to initialize multiple apps, pass a second argument to initializeApp() to give each app a unique name.

unless you want to do something with this error i suggest using the implementation above with a boolean flag: is_admin_initialized = false

regarding cold start, it's unavoidable evil in serverless architecture and there is no way to eliminate it at the time being but you can reduce by following different practices:

1- you should expect more or less around 8s of cold start for JS functions with 1 or 2 dependencies but that all depend on the end on these packages size.

2- cold start can happen in different cases such as:

  • your function has not been triggered yet let's say after your function deployment
  • your functions instances has been shut down and there are no idle instances to receive the upcoming request Cloud Functions in my experience can keep instances idle for few minutes and around 10m more or less.
  • your functions is autoscalling and provisioning new instances

3- don't include libraries (dependencies) that your functions don't need and scope the dependenices only to which functions need them using either static import with require() in JS or dynamic async import with TS. Remember, sometimes you don't need to use the whole library but only one function of it. In this case, try to import just that function from the library rather than the whole thing.

Methkal Khalawi
  • 2,368
  • 1
  • 8
  • 13