From Web to App 2

FWTA is a project embraces the vision to create cross-platform mobile first applications using morden web technologies and best practices, such as Server Side Rendering (SSR), Progressive Web Application (PWA), Accelerated Mobile Pages (AMP), PRPL Pattern, Severless Backend.

This is one of the article series which records thoughts and notes during my implementation of FWTA.

In the previous article, I have setup a very simple web app with next.js and react.js. It feaures hot reloading in development and SSR, and has only one DOM in the index page:

1
<div>FWTA first web app</div>

In this article, I will setup cloud infrastructure, deploy and publish the web app to the world. Then continue setup with some of my favorite settings. From this point, it makes sense to use my favorite IDE or editor, a.k.a VS Code.

Cloud

Cloud has been a popular term in the industry for years, it provides services and tools to help startups and developers to establish their business. There are a lot of cloud services providers now, and it will be more, just like the clouds on the sky.

I personally have some experience with AWS, Azure, GCP, Zeit, Firebase, as well as cloud services providers from China, such as Alibaba Cloud, Tencent Cloud, Qi Niu.

If you’d like to run business in Chinese Market, I recommend to host your web app and purchase domain with providers from China for easier and faster compliance.

All of the cloud servcies mentioned above are great candidates for consideration when start something new. I would use Firebase with free plan here for simplicity and keep everything under control, it’s backed by Google Cloud.

Serverless

In the “stone age”, people bought physical machines, put them on the rack in a clean glass sealed room. They also bought a lot of network devices, such as routers, switcher in order to turn those machines into servers. They spent a big amount of time configuring routers, switchers, DNS and paying electricity bills. And hire others to keep the machines working 24h x 7. If they still have time and money, they hire somebody else to write code for business.

In the “age of cloud”, large companies bought those machines and devices, put them into large warehouses, and does every single dirty but common job for you. Let you focus on writing code for business by renting you the well prepared and maintained machines. They encourage customers to use their cloud services by buying out all the machine, SSD, RAM on the market. They maximize profits by cutting the machines into multiple pieces then rent the pieces out, they call them Virtual Machines, sometimes also called Containers when cutting into even more pieces.

Now, in the “age of serverless cloud”, those large companies hide the process of signing contracts for those pieces of machines. You hand over your codes, they will execute them for you. Though these codes would likely to be executed on the same pieces of machines, you don’t have to touch them anymore and even not feeling they exist. Yep, this is what I am about to do.

Prepare

Restructure Source Code

Before I mess things up, I’d like to restructure source code. This will give me benefits which is not so obvious now. Simply move pages folder inside a new folder src/app. View the new folder structure.

Update Dependencies and Scripts

Update package.json like this. Remove node_modules, reinstall dependencies with yarn install.

Note that I moved next react react-dom into dev dependencies, because I will use next.js 8 build serverless target feature, which will bundle up everthing needed and there is no runtime dependencies needed from node_modules on next.js side.

The second thing to notice is I added engines configuration

1
2
3
4
5
...
"engines": {
"node": "8"
},
...

This is needed to tell firebase functions to use node.js 8 as running environment, otherwise the default version is 6. Currently only node.js 6 and 8 are supported. Google just made node.js 10 available as beta on GCP Cloud Functions, but not on firebase side, so hope this would be my option soon.

The third thing to notice is that I use npm-run-all to run sequential and parallel scripts, this is the technique really works for me to organize steps and tasks, keep things nice and clean.

Scripts For Release and Clean Up

Add these simple scripts scripts/release.js and scripts/clean.js. This may not make sense yet, but what it’s trying to do is pretty clear. The former copy some files into some locations under dist folder to get ready for deployment, the latter cleans those folders up.

Add dist/ folder to .gitignore as it’s produced by release process.

Firebase Setup

Create Project

Go to Firebase, sign up free account and create new project, note down the project ID: fwta-weiwio.

Add Configuration

Add .firebaserc in root folder with

1
2
3
4
5
{
"projects": {
"default": "fwta-weiwio"
}
}

This file is git ignored per my setting, so won’t appear in my repo.

Add firebase.json with this. This file defines how I’m going to use firebase services and how to deploy them. There are two parts hosting, and functions.

Hosting is used to host static web resources, traditional websites are static html with optional js and css, in which case hosting service is enough.

But since I need SSR for better performance, I use Functions to response real http requests, and use hosting rewrite rule to rewrite traffics to corresponding serverless functions.

Read more about those rules.

Add Dependencies

1
2
yarn add firebase-admin firebase-functions
yarn add -D firebase-tools

Serverless Next.js app

Add next.config.js to src/app/ with following

1
2
3
module.exports = {
target: 'serverless'
}

Try to build with

1
yarn build

Under the src/app, there is a new .next folder, which contains build output files. Inside this foler, sverless/pages contains serverless functions for each page, static folder contains static website resources (.js) which need to be put into dist/_next/static for firebase hosting to host them statically.

Read more about Next.js 8 Serverless.

Firebase Functions

Write Functions

Add index.js to new folder src/functions/ with

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const functions = require('firebase-functions')

let indexPage
let errorPage

if (!process.env.FUNCTION_NAME || process.env.FUNCTION_NAME === 'IndexPage') {
if (!indexPage) indexPage = require('./pages/index')
exports['IndexPage'] = functions.https.onRequest((req, res) => indexPage.render(req, res))
}

if (!process.env.FUNCTION_NAME || process.env.FUNCTION_NAME === 'ErrorPage') {
if (!errorPage) errorPage = require('./pages/_error')
exports['ErrorPage'] = functions.https.onRequest((req, res) => errorPage.render(req, res))
}

The code above looks more complex than the example on official documentation for firebase functions. It is because I uses a technique to optimize performance, which use a FUNCTION_NAME environment variable to avoid loading unnecessary resources when only one function is triggered. As serverless functions tends to shutdown environment (docker container) after idling for a certain amount of time. It will cold start after any function is triggered, the cold start is time consuming and it would be ideal to only load things that’s needed.

Deploy

Deployment is easy at this point, just run yarn deploy. After deployment, you can find the url from firebase console to access the web app. I also hooked my custom domain, so you can see it at fwta.weiw.io. The SSL is enabled on firebase by default, but using SSL on custom domain needs extra configuration.

I bought my domain from namecheap.com and used cloudflare.com for SSL, DNS and CDN. These services are awesome and the latter is free!

Performance

It make sense to do some measurements through the development process as to analyze performance and bottle neck as I implement more and more on this app.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
weiwio:FWTA weiw$ ls -ohSR dist/functions/pages
total 1296
-rw-r--r-- 1 weiw 323K Apr 19 15:28 _error.js
-rw-r--r-- 1 weiw 323K Apr 19 15:28 index.js

weiwio:FWTA weiw$ ls -ohSR dist/public/_next/static
total 0
drwxr-xr-x 4 weiw 128B Apr 19 15:28 runtime
drwxr-xr-x 3 weiw 96B Apr 19 15:28 -GOs-V3CpCRhUVnR5ckyh
drwxr-xr-x 3 weiw 96B Apr 19 15:28 chunks

dist/public/_next/static/runtime:
total 56
-rw-r--r-- 1 weiw 22K Apr 19 15:28 main-6d12b443ff1de0c04a2e.js
-rw-r--r-- 1 weiw 1.5K Apr 19 15:28 webpack-a79426b5e11f0ba5879d.js

dist/public/_next/static/-GOs-V3CpCRhUVnR5ckyh:
total 0
drwxr-xr-x 5 weiw 160B Apr 19 15:28 pages

dist/public/_next/static/-GOs-V3CpCRhUVnR5ckyh/pages:
total 32
-rw-r--r-- 1 weiw 7.7K Apr 19 15:28 _error.js
-rw-r--r-- 1 weiw 2.4K Apr 19 15:28 _app.js
-rw-r--r-- 1 weiw 363B Apr 19 15:28 index.js

dist/public/_next/static/chunks:
total 368
-rw-r--r-- 1 weiw 180K Apr 19 15:28 commons.3b8a204509533087c64f.js

Cold starts time 78 ms

Lighthouse report

  • Performance: 100
  • Accessibility: 70
  • Best Practices: 100
  • SEO: 80

Summary

To recap, I have setup cloud infrastructure using firebase, and successfully deploy my dummy web app to firebase hosting and serverless functions services. A snapshot commit could be found here.

Next

Setup TypeScript support, add some UI with Material-UI to make it more like an app rather than a web page …