Hosting Next.js with Firebase

I use Firebase for some of my projects as the backend of some applications namely oneVcard and Clye. But now I need Server Side rendering for some of the routes to make Integrations and correct metadata handling as well as performance work the way it should.

To make it work I settled on Next.js. And there are a few options on how to make it work. If only static rendering is needed a simple next export is sufficient and the out folder can simply be deployed to Firebase. However that is not sufficient for my use case because I have dynamic content depending on data in Firestore and potentially other places.

What are the options?

I want the process to be as automatic as possible and to scale based on demand without having to manually manage instances. Also A CDN for all the static parts is appreciated.

I need some way to execute the JavaScript code in the cloud. So first things to consider are the server less offerings. Those are listed in Googles Documentation.

Source: cloud.google.com/serverless-options

According to this Diagram either Cloud Functions or App Engine standard environment are options. Maybe even Cloud Run is an option because It is Supported by Firebase Hosting.

Cloud Functions

The first thing I tried was to run the App as a Firebase Cloud Function. This worked but got without further Optimization the deployment took a few minutes and Invocations take 13s for the first invocation and speed up to just below 1s after that. That's not very amazing and definitely to slow for my use. Another problem is that I used firebase Hosting and just redirected all request to the cloud function. However I am in Europe and I could not find a way to redirect the traffic to a cloud function that is located anywhere else then us-central1. This is a pretty big problem because all the other data is stored in datacenters located in Europe and all users are there too. So this is not really an option for me.

Cloud Run

Cloud run seems to be a pretty compelling option. It uses docker containers to package everything that should run. Every instance supports up to 80 Connections, so it is not really efficient to hold the connection for example to send realtime updates and websocket is not supported. However that limitation is fine for my use case because it will only handle simple HTTP requests and Firestore or cloud-messaging can take care of the realtime aspect.

To test it out I followed the Instructions listed at cloud.google.com/run/docs/quickstarts/build.. .

Dockerfile
.dockerignore
node_modules
npm-debug.log

How to get access to Firebase services?

To start i just copied a firebase admin credential file into the docker container that I deploy. This is not a very gut solution, but good enough to get started.

Performance

local Node server

This is meant as a baseline. Any deployment will propably be slower.

Static
ab -n 100 -c 10 "http://localhost:3333/dashboard"
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.1      0       0
Processing:     4    8   1.1      8      10
Waiting:        3    7   1.0      7       9
Total:          4    8   1.1      8      10

Percentage of the requests served within a certain time (ms)
  50%      8
  66%      9
  75%      9
  80%      9
  90%     10
  95%     10
  98%     10
  99%     10
 100%     10 (longest request)
Server generated

In this case it communicates with The database and then redirect and communicates with firestore again to generate the dynamic HTML.

ab -n 100 -c 10 "http://localhost:3333/c/test"
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.1      0       1
Processing:    40   60  24.8     52     187
Waiting:       40   60  24.6     51     186
Total:         41   60  24.8     52     187

Percentage of the requests served within a certain time (ms)
  50%     52
  66%     55
  75%     57
  80%     67
  90%    101
  95%    117
  98%    132
  99%    187
 100%    187 (longest request)

Directly

I Call the cloudrun containers with their url given in the cloud console.

Static:

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       58   78   8.1     77     106
Processing:    64   89   9.7     90     112
Waiting:       38   50   6.3     49      68
Total:        122  167  13.8    168     202

Percentage of the requests served within a certain time (ms)
  50%    168
  66%    172
  75%    174
  80%    176
  90%    186
  95%    192
  98%    201
  99%    202
 100%    202 (longest request)

Generated:

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       58   71   7.3     72      87
Processing:    50   74  21.1     69     180
Waiting:       50   73  21.2     69     180
Total:        112  145  24.1    139     259

Percentage of the requests served within a certain time (ms)
  50%    139
  66%    149
  75%    157
  80%    158
  90%    173
  95%    186
  98%    255
  99%    259
 100%    259 (longest request)

This is expected and not to bad. It is over 100 ms, but that is also true for the local installation so not a real problem.

Firebase Hosting

Unfortunately not every location is supported but at least locations that are pretty near are avaliable. For Next.js all static resources are served under /_next/static those should be cashed automatically by Firebase Hosting. Unfortunately that did not work. But it is possible to enable it by setting the correct cache headers firebase.google.com/docs/hosting/manage-cache. To do this with Next.js you can just modify the next.config.js file to inlcude the following lines in the configuration:

module.export = {
  async headers() {
    return [
      {
        // match all static files
        source: "/_next/static/:path*",
        headers: [
          {
            key: "cache-control",
            // allow Firebase Hosting to cache it for up to one year
            value: "public, max-age=31536000, immutable, s-maxage=31536000",
          },
        ],
      },
    ];
  },
};

More documentation about headers can be found at nextjs.org/docs/api-reference/next.config.j...

Static:

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       46   54   7.2     51      78
Processing:   392  465  48.4    458     765
Waiting:      363  436  48.3    427     736
Total:        441  519  50.0    511     814

Percentage of the requests served within a certain time (ms)
  50%    511
  66%    528
  75%    537
  80%    542
  90%    565
  95%    596
  98%    707
  99%    814
 100%    814 (longest request)

Server generated:

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       46   51   4.4     49      71
Processing:   445 4236 3265.8   3449   16136
Waiting:      445 4236 3265.9   3449   16136
Total:        502 4287 3265.4   3500   16194

Percentage of the requests served within a certain time (ms)
  50%   3500
  66%   4841
  75%   5325
  80%   6128
  90%   8297
  95%  12776
  98%  14183
  99%  16194
 100%  16194 (longest request)

That is not acceptable, it takes up to 16 s for the page to load and normally it takes over 4 s which is unacceptable. That is far to slow.

App Engine standard environment

This runtime supports Node.js which is exactly what I need. There are also other runtimes available for Python, Java, PHP, Ruby and Go. But no Integration with Firebase Hosting exists which makes it harder to configure a CDN and to integrate with the rest of Firebase.

Solution

I will route my traffic directly to cloudrun and use Firebase Hosting as a CDN for static Assets. This way I should be able to avoid the overhead introduced by Firebase Hosting and at the same time I get most benefits of a CDN.