firebaseexpressgoogle-cloud-platformgoogle-cloud-functionscold-start

Optimizing firebase functions cold start with expressjs


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.


Solution

  • 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