Differential Serving on Firebase Hosting

Polymer 2.x/3.x brought the standards-compliant ES6 class-based syntax for defining Web Components. This works well for most modern browsers and ES6 has a lot of other nice features (like arrow functions) to make your JS code cleaner and more fun to write.

But if you need to support older browsers like IE 11 you will have to compile your code to ES5 which comes with performance drawbacks for modern browsers compared to running ES6 on them directly.

The ideal approach is to use differential serving to serve the ES6 version to modern browsers and a fallback ES5 version to older browser. prpl-server-node is a sample implementation of a Node server that uses this pattern. I took the ideas from this implementation and created a sample based on the polymer-starter-kit on how you can use differential serving on Firebase Hosting using Cloud Functions for Firebase for dynamically sending the right version to the user.


Prerequisites / Setup

This article assumes that you are using the app structure recommended by the PRPL pattern, i.e. you have a minimal index.html as entrypoint that loads an app shell which in turn loads all the other fragments/web components.

It also assumes that all URLs like https://my-app.firebaseapp.com/view1 will be rewritten to load the entrypoint and that you are using polymer build to build your app.

To serve the resources in your index.html from the right build, all the paths you specify will need to be relative, so for example

<link rel="import" href="src/my-app.html">

instead of

<link rel="import" href="/src/my-app.html">

Additionally you need to define a base tag in your <head>.

<base href='/'>

During the build process the polymer-cli will change this base tag to <base href='/modern-es6/'> and <base href='/fallback-es5/'> for their respective builds.

For hosting static resources and making sure your routing references the correct root you have to define the Polymer rootPath.

window.Polymer = {rootPath: '/'};

You can then use links like this in your app shell:

<a href="[[rootPath]]view1">View One</a>

All other references in your elements, especially imports of other elements have to be relative so they get served from the same build.

To tell polymer build to actually update the base tag, you have to add the "basePath": true setting to the builds section of your polymer.json

Additionally you will have to add the browserCapabilities where applicable which will be used in the Cloud Function to know which version to serve. (More information about this setting).

After building you should end up with a structure like this:

build/
modern-es6/
bower_components/
src/
index.html
  fallback-es5/
bower_components/
src/
index.html
  polymer.json

For hosting this on Firebase you will need to deploy the full build directory, including both builds and the polymer.json as shown in the firebase.json below.

Additionally the headers section adds the Service-Worker-Allowed header when requesting any of the service-worker.js file to make sure the service-worker can be registered on the root scope from the subfolder they are in.

All paths that are not static resources will be rewritten to call the Cloud Function serve where all the magic happens.


The Magic

Probably the biggest issue with using Cloud Functions for Firebase for this specific use case is that you don’t have direct access to the files deployed on Firebase Hosting since they are running in different environments.

So the first thing the serve function has to do is to fetch the polymer.json via a HTTP request.

It then checks the build section and compares the defined browserCapabilities to the capabilities of the browser. Depending on this it will fetch the best suited index.html from Firebase Hosting and return it as response, adding an additional header to control the caching behavior of Firebase.

And that’s all. With just a few changes to your original app and this simple function the app will work for modern and older browsers, and the function is only called once in the beginning for the entry point.


Potential improvements

  • To prevent the two extra HTTP requests it should be possible to deploy the built index.html files and the polymer.json together with the Cloud Function. This would need some small adjustments to the build process.
  • prpl-server-node also has HTTP/2 server push functionality which could be added to this function.
  • At the moment all resources including images are uploaded for each build which will result in a lot of duplicates. It should be possible to extract those static assets and only host them once, accessing them via absolute URLs using "[[rootPath]]/static/my-file.jpg".

The team behind prpl-server-node is apparently already working on similar functionality/samples so I probably won’t invest too much more time on this project. But I needed the differential serving functionality on Firebase for a project myself and I didn’t see any other working examples out there. This works with minimal adjustments to existing Polymer apps, so feel free to use it and send PRs 😄

The full example is available on GitHub.