Optimizing firebase functions cold start with expressjs
Asked Answered
C

1

6

I was trying to figure out how I could optimize cold start times for my firebase functions. After reading this article, I wanted to try it out but I realized that the article specifically targets the base usage of the http onRequest function and doesn't give an example using express.

A similar question popped up here but doesn't seem like there's a clear answer. I saw the author of the article Doug actually commented on the question and he mentions to create a dynamic import for each route in the app since onRequest() only allows for passing the app as its only argument, but I wasn't understanding exactly what he meant by that other than to use the base API without the express app. Ideally I'd be able to use express so I can have finer control over the api url paths and use some of utility that express offers.

Can anyone give me an example of how to use express with Doug's example? Even if I have to define a new express app for each route, I'm okay with that. Just don't see how to configure it that way.

EDIT: To be clear, the goal is to optimize cold starts across all function invocations, not just the http routed ones. From my understanding, Doug's example eliminates the imports being preloaded with single routes declared using onRequest, but it doesn't show how that is possible when defining routes through express.

Claret answered 26/3, 2021 at 8:38 Comment(0)
X
4

Assuming each router you split out is defined in it's own file like so:

// $FUNCTIONS_DIR/routes/some-route-handler.js
import express from "express";

const router = express.Router();

/* ... define routes ... */

export default router;

You could then use this middleware to load each route handler module only when it's needed.

function lazyRouterModule(modulePath) {
  return async (req, res, next) {
    let router;

    try {
      router = (await import(modulePath)).default;
    } catch (err) {
      // error loading module, let next() handle it
      next(err);
      return;
    }
    
    router(req, res, next);
  }
}

In your sub-function file, you'd use that middleware to create your express app and connect the routes.

// $FUNCTIONS_DIR/fn/my-express.js
import express from "express";

const app = express();

app.use('/api', lazyRouterModule('./routes/api.js'));

app.use('/profiles', lazyRouterModule('./routes/profiles.js'));

export default app;

Then in your main functions file, you'd connect up your subfunction files on-demand:

// $FUNCTIONS_DIR/index.js
import * as functions from 'firebase-functions'

export const myExpress = functions.https
  .onRequest(async (request, response) => {
    await (await import('./fn/my-express.js')).default(request, response)
  });

export const newUserData = functions.firestore.document('/users/{userId}')
  .onCreate(async (snap, context) => {
    await (await import('./fn/new-user-data.js')).default(snap, context)
  });

When lazy-loading modules like this, you will want to lazy-load firebase-admin from a common file so you don't end up calling initializeApp() multiple times.

// $FUNCTIONS_DIR/common/firebase-admin.js
import * as admin from "firebase-admin";

admin.initializeApp();

export = admin;

In any function that wants to use "firebase-admin", you'd import it from here using:

// $FUNCTIONS_DIR/fn/some-function.js OR $FUNCTIONS_DIR/routes/some-route-handler.js
import * as admin from "../common/firebase-admin";

// use admin as normal, it's already initialized
Xanthous answered 26/3, 2021 at 10:27 Comment(6)
Wouldn't this load the express dependency for all other function invocations now though since we're importing it at the root global scope? Is express something that's already loaded as part of the global scope since the onRequest framework supports the express api underneath?Claret
I'll revisit later when I have the chance, but with the way you phrased your question, you were looking for dynamic imports for each route which implied you had one large express-based cloud function, not that you were looking for dynamic imports for each cloud function individually. The latter is trickier to support with the environment variable changes made with the Node 10 migration so I'll have to dig through my notes tomorrow.Xanthous
Gotcha, thanks for the help regardless. I modifed the question to be a little more clear. If I read the above solution correctly, the only thing is express being loaded globally, but beyond that everything is optimized behind the dynamic import. Making express itself dynamically loaded seems tricky if possible at all.Claret
@KaneChong updated with the subfunction structure as proposed by Doug.Xanthous
After looking into the environment variables, it seems a Node 8 Cloud Function's FUNCTION_NAME environment variable was renamed to K_SERVICE in later Node versions. It also seems the better-firebase-functions package still works all these years later. Instead of a messy index file, it allows you to just export your functions as some-function.func.js in your file tree, using parent folders as their deployment groups.Xanthous
Than you @Xanthous ! I didn't think about nesting the express app itself. Makes sense! As to your comment about better-firebase-functions, the package did come up when I was exploring this whole thing, but I figured if I could avoid having another dependency I should.Claret

© 2022 - 2024 — McMap. All rights reserved.