Next.js on Cloud Functions for Firebase with Firebase Hosting

SSR with Clean URLs

James Hegedus
codeburst

--

Welcome to a series exploring Cloud Functions for Firebase with a modern application stack (React, Apollo, GraphQL, Next.js & Firebase). If you are not familiar with this stack, read on! If you are familiar, checkout the TOC above.

Next.js app on Firebase Hosting with Cloud Functions for Firebase

What is Server-side Rendering?

Most apps today are built as Single Page Applications (SPAs) using a JavaScript framework to enable rich user interactions. With a traditional SPA the client makes a single request to the server for the HTML, CSS and JavaScript required for the app. Because the app is a bunch of JS scripts, it must wait until some of the JS bundles are completely downloaded before it can make any requests to the server for data to populate the UI. This means that there are two round trips to the server before the user can interact with the app. Not ideal.

Server-side Rendering (herein SSR) in a JS SPA is used to evaluate the JS on the server (server-side 😉) and then request the appropriate data before sending back the computed HTML page along with the CSS. Any JS bundles can then be loaded asynchronously since the UI will appear completed before the JS finishes downloading and parsing. This gives the appearance of a faster app.

Page Load Metrics

The image below defines common terms for each step in the web app loading sequence from a user’s perspective:

source: Google I/O 2017 — Staying off the Rocks
  • Navigation Begins/Time to First Byte (TTFB) to First Contentful Paint (FCP) is the time taken for the server (or CDN) to respond with the app’s HTML.
  • First Meaningful Paint (FMP) is when the data requested from the API Server/Database is returned.
  • Visually Ready is when the data has been completely loaded.
  • Time to Interactive (TTI) is achieved when the data and JavaScript is completely loaded resulting in a page the user can engage with.

For more detail on the metrics used to measure page loads I would recommend watching this Google IO Lighthouse video from the Google Lighthouse web auditing and performance tool website.

SSR Benefits & Caveats

The main benefit to SSR is that the app appears to load completely, more quickly. These images illustrate the difference between Client-side Rendering to Server-side Rendering of an SPA:

Client-side Rendering
Server-side Rendering

With SSR the TTFB can be increased due to the time spent on the server evaluating HTML and populating the page with data before being sent to the client. If you are using a common back-end service for processing and data storage (say, Firebase), the data request is made inside the same data center and is extremely fast. So while the TTFB can be slower, it’s shouldn’t be any order of magnitude slower than making two round trips across the internet.

Another condition of SSR is that the TTFB, First Paint, FCP and FMP are compressed to the same point on the timeline. They’re essentially the same thing. The JS bundles enabling client-side interactivity may still be loading, but the user sees a page closer to the finished product sooner.

The latter is why SSR is so desirable.

A way around the TTFB being slow in both CSR and SSR is to use the App Shell model or client-side caching. This can significantly improve the user experience in all cases. We will cover this in a future post.

For more discussion on SSR vs CSR I would recommend these articles:

Tom Dale — You’re Missing the Point of Server-side Rendered JS Apps

Juan Vega — Client-side vs. server-side rendering: why it’s not all black and white

Hacker News discussion

SSR Requirements

A condition of SSR apps is that the host serving the app requires a runtime to perform the JS evaluation. This means you cannot simply use a CDN with static files and an API server, you must have a server to host your application. We’ll explore how to use Firebase to achieve this.

So now that we have an understanding of SSR and why you would decide to use it, we can talk about the framework of choice.

Next.js — SSR made simple

Next.js Logo — Zeit Co — image source

a small framework for server-rendered universal JavaScript webapps, built on top of React, Webpack and Babel — Zeit.co

Place React components in a pages directory and running next, and you'll get automatic code splitting, routing, hot code reloading and universal (server-side and client-side) rendering. — Zeit.co

If you want to learn about the motivations behind Next.js I recommend the official blog post.

Personally, I find that routing, code-splitting and hot-module reloading for React applications too complex to get right when learning. Having infinite flexibility to implement any routing method or code-splitting is great, if you’re a pro. There’s conflicting information about which method is best and it’s all just to much to configure, let alone for a universal SSR React app as a beginner.

Next.js solves these issues by having opinions (they aren’t always bad) about how to manage routing in a React app and abstracts away any of the complexities with handling SSR. Then you get the bonus of HMR and extensible Webpack and Babel configurations for when your skills develop.

Next.js Features

Simple Routing — React components inside pages/ become page routes 👍 There’s no need to try and figure out which version of which router library the project uses, or unravel the mesh of routes mixed with pages.

Code Splitting — Routes are code-split. There’s an algorithm in Next that performs common module bundling/splitting and it works quite well 👍 KISS

SSR — it just works 👍

Data-fetching on Server-side Render — it’s a function you implement so you can use any data-fetching or storage you prefer.

Pre-fetching pages — you specify when this happens.

Oh, and the soon to be released Next.js 3.0 has support for outputting a static SPA as well. So if you have no desire to use SSR, but like the features listed above, then you can simply output a static app and use a CDN as your host. But let’s do something new and drop SSR on Cloud Functions.

Dynamic Content with Firebase Hosting & Cloud Functions

Traditionally hosted SPAs use static resources on a CDN as mentioned earlier. Firebase Hosting is such a service and therefore cannot do back-end processing. However, with the addition of Cloud Functions for Firebase in March 2017 an integration was made with Firebase Hosting to allow serving dynamic content. This allows us to do server-side processing with Cloud Functions and use a custom domain with the Cloud Function through Firebase Hosting. This Q/A with David East goes through a quick example.

It’s just a short video, give the first question a watch!

Clean URLs

Regular Cloud Functions have the following URLs:

https://us-central1-<project-name>.cloudfunctions.net/<function-name>

After using Firebase Hosting redirects, we can get the following URL format:

<project-name>.firebaseapp.com/

Unfortunately the above video only covers redirecting a sub-route of your Firebase Hosting URL to a single Cloud Function returning custom HTML. We don’t want to write a different Cloud Function for each URL in our app. And we also don’t want to host our app on a sub-route of our domain. We want the app hosted on the root of our domain. Like so:

<project-name>.firebaseapp.com/

We’ll get to clean URLs with Firebase Hosting Rewrites later. First let’s host our app on Firebase!

SSR on Firebase

Next.js Setup

Create a folder structure like this:

We will build our Next.js app in the src/app folder.

Navigate to the src/app/ folder and install our dependencies like so:

yarn add next@beta react react-dom

I use Yarn, feel free to use NPM instead. I would recommend npm@5.2.0 or greater as it’s finally stable and super speedy.

Now we will add our Next.js pages and components. This will create a simple site that has a Home and About page with a standard header that has links to each page.

A simple Next.js app with multiple pages.
You should now have something like this.

N.B.: For a complete guide on learning Next.js there’s no going past https://learnnextjs.com/. It’s quick and covers everything you need to get started with the framework.

Add the following scripts to the package.json file in the src/app/ folder.

"scripts": {
"dev": "next",
"build": "next build"
}
I added the “name” and “version” fields.

These scripts will help us run local development with features like HMR etc from our project root later. It will error at the moment thanks to our next.config.js build directory redirect.

Init our Firebase Project

To start, create a project in the Firebase web console — call the project nextonfirebase.

Then go to the root of your local project nextonfirebase/ and run the following few commands:

yarn init -y

yarn global add firebase-tools

firebase login — login to the Firebase CLI.

firebase init — initialise a Firebase project.

  1. Use Firebase Functions and Hosting.
  2. Link it to the project we created in the web console earlier.
  3. Do NOT install the Firebase Functions dependencies.
  4. Specify to use the src/public folder.
  5. Say NO to a single-page app.

Unlike with Firebase Hosting, the Firebase CLI does not ask us for a directory to keep our Cloud Functions, it just creates a folder at the current dir: nextonfirebase/functions/. Before we delete this folder, we need to move the contents to our existing nextonfirebase/src/functions/ folder:

Now we need to update the firebase.json file to point the Firebase CLI tool to the correct location of our Cloud Function code. Replace the file’s contents with the JSON below:

Next.js on Cloud Functions for Firebase

When setting up our Next.js app earlier we created a file called next.config.js. This file is used to tell the Next.js framework what destination directory to output our built app to. You will see that it says to output the built app to a ../functions/next directory. This is a significant step in getting Next.js to operate correctly. The default settings of Next.js specify a destination directory with the format .next. That little . in the name has been the cause of some headaches when getting resources to be uploaded through the CLI and to be found once hosted on FaaS services (Solutions are in the works in various places to solve this, but it’s best to go with this method for now. There’s no real downside this way). I haven’t run into any problems since trying this renaming method that just specifies a directory without a . in the name.

Thanks to @geovanisouza92 (GitHub) for their help and collaboration with this (if you use the serverless framework, check out their serverless-next example of this on AWS Lambda with API Gateway).

Getting to the code.

We don’t want to commit our Next.js build folder to our repo so add a .gitignore file in our functions folder.

I like to be verbose 😝

Since we’re in our src/functions/ folder, run yarn to install the dependencies. We’ll also need to add new dependencies for next.

yarn add next@beta react react-dom

This is an annoying issue where the node_modules/ for the Next app are required to be packaged alongside the next build folder in our Cloud Functions, but we can’t simply copy the src/app/node_modules/ to src/functions/node_modules/ as that would defeat the purpose of using a package manager with a lockfile and probably cause a myriad of other problems (who wants to manually merge node_modules 😑) . There’s probably a better solution to this, but for now just add all dependencies from src/app/package.json into src/functions/package.json (please comment if you find a nice cross-platform solution to this and I will update the article to spread the word!).

I’ve recently found that firebase-tools doesn’t install the latest releases of the Cloud Functions dependencies; firebase-admin and firebase-functions. To make sure we’re using the latest and most compatible versions run the following:

yarn upgrade firebase-admin firebase-functions

Now we can create our Cloud Function that serves our Next app. In src/functions/index.js paste the following:

No ES6+ here today. KISS while learning new things.

This code simply sets up the Next.js app to be returned from a Cloud Function called next. Here you can see the (req, res) variables passed from the Cloud Function through to the Next.js app to handle.

Functions folder should look like this now.

Building Blocks

Before we can go any further we need some build scripts to manage local Firebase testing and deploying. I’m a fan of NPM scripts because of their ease of use and simplicity. Run yarn init -y in the project root directory so we can manage our project from there and add the code below:

Final root folder structure.
Our build and deploy scripts.

The scripts we have here are not magic. They simply allow us to manage the project from the root directory easily. The list below shows what goals we want to achieve with the scripts, which scripts align to each goal and explains how each script does so:

  • Local development of our Next.js app from the project root.
    ✔️next — install firebase function deps, nav to app folder, install deps, run Next.js dev server.
  • Install all node_modules required and build our Next.js app.
    ✔️build-next — navs to app folder, installs deps, runs local build script
  • install all node_modules required by our Cloud Functions.
    ✔️build-firebase — navs to functions folder, install deps.
  • Serve our app locally on Firebase Hosting with Cloud Functions.
    ✔️serve — builds app and functions using scripts above. Serves local Firebase Hosting & Cloud Functions
  • Deploy our app to Firebase through the CLI.
    ✔️deploy — builds app and functions using scripts above. Deploys to Firebase project.

The pre scripts just ensure all deps are installed before running serve or deploy. These are scripts using NPM script hooks.

Local Next.js App Development

Now that we have our scripts sorted, let’s test out our local development workflow and see if it works as expected:

yarn next
It works! HMR, routing, caching and all!

It all works as expected, cool!

Testing Next.js on Cloud Functions — without Clean URLs

Let’s now see how our app works when hosted on Cloud Functions for Firebase alone. Run the following command to deploy (we won’t use this again so it’s not in the NPM scripts):

yarn build-all && firebase deploy --only functions

N.B.: This deployment will take some time ~2–3minutes depending on your machine and internet connection.

2 minutes 48 seconds to deploy the Cloud Function. Lucky that local development works!

Open a browser to a new tab with DevTools open on the network tab. Copy the Cloud Function’s URL from the Terminal. It should look like this:

https://us-central1-<project-name>.cloudfunctions.net/<function-name>/

Load the page.

N.B.: If you get Internal Server Error then you forgot the trailing slash. Yes, that again (repeat readers will understand)!

When you try to navigate to the /about page you will see that Next.js expects to route to the root of the URL, not some /next/ subroute. This affects two things:

  1. Next.js routing does not work for routing our app’s pages.
Page routing does not resolve properly! Inserting the Cloud Function name demonstrates the issue.

2. Next.js routing does not work for our bundled JS files.

JS Bundles aren’t resolved properly. We must insert the Cloud Function name here too!

This is the crux of the problem people are having with SSR Next.js on ephemeral compute services like Cloud Functions and AWS Lambda etc.

There are probably some workarounds for getting this all to work within Next.js itself. I tried many permutations of assetPrefix with other config settings, but could only solve some of the problems. Either way, there’s a better solution!

Clean URLs with Firebase Hosting Rewrites!

Our Next.js app wants to be hosted on the root URL, and that’s what we want too! We also want to use the clean URL from our Firebase Hosting service instead of the long Cloud Function URL.

<project-name>.firebaseapp.com/

Firebase Hosting Rewrites

Hosting rewrites use a glob pattern to match URLs. The docs state that

** matches any file or folder in an arbitrary sub-directory. Note that * just matches files and folders in the root directory.

The key word here is “an”. This routing rule does not solve the issues with files under the /_next/* path or nested subroutes. We want to match

any file or folder in ALL arbitrary sub-directories.

**/** — actually matches any file in any sub-directory.

Update your firebase.json to contain the following:

Firebase Hosting Deployment

Don’t try to test the rewrite rules yet. There’s one more thing standing in our way. Firebase Hosting has a set priority for resolving content:

https://firebase.google.com/docs/hosting/url-redirects-rewrites#section-priorities

As you can see, static content gets matched before rewrites.

Simply deleting all the .html files in the src/public/ folder seems like a good idea, until firebase deploy is run.

No dice.

A way around this is to put your static assets into the src/public/ folder, or simply leave the auto-generated 404.html file (because it won’t be routed to as Next.js has it’s own 404 page) and delete the index.html.

The deployment upload still gives a warning informing you that the Hosting directory doesn’t contain an index.html file. This can be ignored (unless your CI/CD pipeline requires no warnings, in which case I cannot help you, sorry).

Re-deploy the app with yarn deploy and you should now be able to use the Hosting URL for the Next.js App. Woo! 📦

IT WORKS!!!

Firebase can also run a local web server with your Firebase Hosting configuration. It even runs our Cloud Function if we have rewrites setup. So we can actually test our Firebase Hosting SSR locally to determine if everything works as expected. Just run yarn serve from your project’s root directory for the correct script.

Local Firebase Hosting! It’s not the fastest, but it’s faster than a full deployment.

Conclusion

So SSR on Firebase Hosting is now possible thanks to the integration with Cloud Functions. It’s worth noting that Cloud Functions is still in Beta, so performance is set to improve over time. Once the Firebase Hosting rewrite rules are in place, Next.js’s internal page routing just works. I’m excited for static support to come to the framework as it means I can use the same JS framework for SSR and static CSR sites on the same Firebase infrastructure! It’s a exciting future!

Why go to all this trouble to host an SSR app on Firebase?

In my previous post about GraphQL on Firebase, I touched on why I go to the trouble getting everything working on Firebase. I like that it’s an all-in-one managed and hosted service. It’s ideal for serverless development which I believe to be the future of all cloud/web based applications. Code packaging, billing, logging & metrics, just everything you would need is in one place. There’s no need to deal with multiple user accounts for team members on multiple platforms/services.

It may be more difficult to get up and running, but you only have to deal with those problems once. Using multiple tools you’ll have to deal with the myriad of ecosystems for the entire project, or go through the hell of migrating to consolidate. I’d rather put in the effort here.

Plus, it’s fun solving new problems.

Thanks

Thanks to the Next.js and Firebase teams for building such cool products. And thanks to all those who’ve created content that I’ve linked throughout this post.

A special thanks to Callum Gardner for reviewing this post numerous times.

If you found this useful, please recommend and share with your friends & colleagues.

--

--

GCP, Firebase, Sveltejs & Deno fan! @jthegedus on all platforms.