How we turned our legacy system into a Progressive Web App (PWA)
A complete how-to on going from a legacy system to a cutting edge Progressive Web App
A lot of people have been talking about Progressive Web Apps (PWA) in the past couple of years. People usually mention about its greatness, power and effectiveness when the goal is to provide an app-like experience to users when they access your website. But developers usually don’t give much attention to old/legacy systems when talking about PWA.
Yes, that’s right. I’m talking about that gigantic system full of jQuery, inline styling and every single example of code smell possible.
You might be thinking: “Could it get any worse?”. The answer is yes. But first, let’s give some attention to people who don’t know what PWA means yet.
If you are just a curious human being and got to this post because you heard or read the acronym PWA, one of the definitions I like best is:
Progressive Web Apps (PWA) are experiences that combine the best of the web and the best of apps.
In very simple words, having a PWA website allows you to install it to your phone’s home screen, have push notifications and even access content when offline. But guess what? The mobile app would still be your web app.
Ok… So you’d have a website that is just like an app?
The answer here is: Yes you would.
Now, if you don’t need more information about PWA, just keep reading, but if you need a little more to understand it better, I’d suggest you go ahead and read this article by Ciprian Borodescu and this article by GeekyAnts.
The Current System
Cool, so far so good. Let me explain you a little bit about the system we had to turn into a Progressive Web App. This is a huge ASP.NET MVC project, running the CMS (Umbraco), full of templates and accessible worldwide from over 40 countries. Now talking about the front-end structure. The template files are written using Razor syntax (Razor is an ASP.NET programming syntax used to create dynamic web pages with C#) and it simply has one huge file named
app.js with every single behaviour that lives on the website.
I’m guessing this is your reaction right now:
The company’s website’s been in the air for quite a while and, as it has to attend so many markets, it really is something crucial for them. But the day which someone would come and ask for a cutting edge solution would come. And it did.
One of the big bosses of technology — which is a great defensor of Progressive Web Apps and implemented several clients’ systems using this approach — contacted our team with this challenge. As you know, the shoemaker’s son always goes barefoot. The problem is that it was getting harder to make a point to clients that it was worth implementing it, while not having it on your own website. So the challenge began.
The main thought at that point was that PWAs are commonly seen on projects using React, Vue, Angular, etc. They are all Single Page Applications (SPAs) which makes it a lot easier to cache content as each page component will be loaded as requested. Most of the boilerplates available already are PWA compatible and also usually all set up, which means that all you need to do is build your app and enjoy.
As you probably know, on a SPA you have one single
index.html file and all you do is change its core. So the main content is changed, but stuff that appear in every single page like the header, footer and menu are loaded only once. The difference from our architecture is huge here as when we access a different URL, we get the same header, footer and menu for each route.
The Proof of Concept (PoC)
The first thing we had to make sure of was that it was possible to implement a PWA on our current environment. At first, everyone thought it would not work because of the way the system behaves. Even after some research we did not find any example of people turning a system like ours into a PWA. But after doing more research and testing, it ended up working! I will mention a few pros and cons now so you can decide if either you want to continue reading or stop right now. So here we go:
Having a working PWA already speaks for itself. The possibility to get closer to users by allowing them to have your website installed at their phone, send push notifications to enlighten them with new content and even show if they’re accessing offline is really worth it. Configuring the service worker properly also made our website faster as it started to consume some information from cache not having to download them every single time.
Getting it all to work as we planned at first was definitely a pain point. When it first started working, it was a cache hell. The service worker would just save everything it got through the network, including third party stuff. For example, when we opened the home page it instantly cached around 120MB in our application. Yea, that’s right and the cache storage is different for each device, which means that for some devices it wouldn’t even be able to cache the first page.
Cool, so you want to know more about what we did, huh? In this case let’s move on.
For the next examples I am going to be using Google Chrome and its DevTools to demonstrate the manifest and the service worker configuration.
Our first step was to configure a basic
manifest.json file. According to the Mozilla documentation, the web app manifest provides information about an application (such as its name, author, icon, and description) in a JSON text file. So the manifest’s job is to contain and inform general details for websites installed on the home screen of your phone.
It is important to have in mind a few properties that are important for the app-like experience of your website. A lot of them that can be used here, but I filtered a few to make our lives a little easier. In alphabetic order:
- background_color: Defines the expected “background color” for the website. This value repeats what is already available in the site’s CSS, but can be used by browsers to draw the background color of a shortcut when the manifest is available before the stylesheet has loaded. This creates a smooth transition between launching the web application and loading the site’s content.
- description: Provides a general description of what the pinned website does
- display: Defines the developers’ preferred display mode for the website.
- icons: Specifies an array of image files that can serve as application icons, depending on context. For this one, it is important you have at least one icon specified with the resolution of 512x512 pixels as it is used as a default for the splash screen.
- name: Provides a human-readable name for the site when displayed to the user.
- scope: Defines the navigation scope of this website’s context. This restricts what web pages can be viewed while the manifest is applied. If the user navigates outside the scope, it returns to a normal web page inside a browser tab/window.
- short_name: Provides a short human-readable name for the application. This is intended for when there is insufficient space to display the full name of the web application, like device home screens.
- start_url: The URL that loads when a user launches the application (e.g. when added to home screen), typically the index. Note that this has to be a relative URL, relative to the manifest url.
- theme_color: Defines the default theme color for an application. This sometimes affects how the OS displays the site (e.g., on Android’s task switcher, the theme color surrounds the site).
You can take a look at the full list here.
Ok, cool. Now you are aware of each attribute that will be contained in our
manifest.json. But if you are still wondering where they become visible to users, here’s a splash screen example:
Now let’s talk a little bit about code. For our application the manifest was configured as such:
The last thing you need to do is to add a
link tag to the file which you have your head tags. In our case, we built a shared file called
_Layout.cshtml which has our main html structure, including navigation, footer, etc that is used everywhere. So the line you need here is:
<link rel=”manifest” href=”./path/to/manifest.json” />
If you are configuring it in your SPA, the above line will be added to your
And we’re done! […] With the first part, of course. Just with the configured manifest, you are already able to give an app-like experience to users (considering they added it to their phone’s home screen), but still requiring they have access to the internet.
Wanna see it working for now? That’s ok! Just run the project and open Chrome DevTools. Click at the tab Application and then you’ll see the option Manifest at the left menu. It should look something like the following image:
Wait a minute… Didn’t you say we could have access to the content when offline?
Don’t worry, we’re getting there! The next thing we’re going to talk about is the service worker.
The Service Worker
There is one important thing to do if we want to access the website’s information offline: setup a service worker (SW). According to our friends at Google, “a service worker is a script that your browser runs in the background, separate from a web page, opening the door to features that don’t need a web page or user interaction”. To sum up, the service worker’s job is to cache our website’s content in order to provide it to users when offline. The cache is done every time you access a page, which means you’ll only have offline access to the pages you accessed online.
As you probably realized, there is no need to place the
manifest.json at the root of our system as we specify where it will be loaded from. But things are a little different with the service worker. The scope is really important for it as it determines which files it controls. I am not saying that you have to place your service worker at the root, but it makes your life a lot easier if you do. The default scope is the location of the service worker file and it extends to all directories below. Just to make sure you understand it perfectly, let’s take a look at a few examples.
In the above picture we can see that the file
service-worker.js is placed at the root of our project. So when we access something inside the
Assets folder, the service worker will be able to intercept the request and cache any accessed file. So far so good right? Let’s just take a look at one more image.
Now we placed our
service-worker.js inside the
Assets folder this will not be visible to our service worker, which means it won’t be cached, which means… ? Yes, that it will not be accessible offline!
Ok, but why is it of any importance right now?
Well, for young and cool systems it’s just fine. But now imagine a not well planned legacy system that you inherited from someone with no knowledge about how to best organize the file structure.
Either way, just keep in mind that it’s best to place the
service-worker.js at the root folder that will then be published.
I guess we are ready to start configuring our service worker, right? Let’s do it.
Why don’t we first create the
service-worker.js at the root of our project and write a simple, yet enthusiastic:
console.log('It God damn worked!'). Beware that you can actually name your service worker file anything you want. So if you think it’s best to call it
sw.js, feel free to do so. Another thing to keep in mind is that you need to run your code with a HTTPS protocol, even if a fake one. In our case when running it on Visual Studio the IIS is configured like that, so you can be sure it is registering and caching.
_Layout.cshtml I mentioned before? We’ll now add a script tag to it with the following code:
The scope property can be set to whatever scope you’d like to take control of. If you decide that you only want to cache stuff from inside the folder
/App, you can just change that.
Now if you save and run your project — and your browser is compatible with SWs- you should see the log “It God damn worked!” at the console of your browser! Also, if you open Chrome DevTools again, click at the tab Application and then click the option Service Workers you should see your first registered service worker!
Congratulations, you’ve successfully loaded your first service worker!
Ok, I know. So far it is just useless. We’re not doing any of the possible cache magic, right? So why don’t we fix it right now?
We now have two options to move on. The first is to write the service worker by hand and the second is to use Google’s Workbox. Let’s first write our own service worker, then we can move to Workbox, ok?
Your Own Service Worker
In our case, the first service worker was created using PWABuilder with some adaptations to the current environment. It really helped using a builder at first as we were all still understanding all these stuff. Ok, let’s go straight to the code.
You can just copy the above code to your
service-worker.js. Save it again and… That’s it! You have a fully working service-worker running at your page! Now everything you access will be cached and if you turn off the internet and refresh a previously cached page, you will be able to see it entirely.
To make sure everything is being cached, open Chrome DevTools, click at the tab Application > Cache Storage and select your app’s cache. You should see something like this:
Now, if that’s all you needed, you probably don’t need to read the next section. But if you still feel like you want more control over your service-worker, we’re going to talk about Google’s Workbox.
If you’re leaving now, thank you for your time and patience! I am really happy I could help you somehow, so don’t forget to leave it a clap!
Now, you curious human being that wants to go further. Sit back and enjoy the ride.
Workbox by Google
Sweet, you’ve made it this far! So you really are interested in PWA, huh? Let’s move on and talk about Workbox now.
First things first, Google’s Workbox is a set of libraries and Node modules that make it easy to cache assets and take full advantage of features used to build Progressive Web Apps. By using it your life can get much easier when you decide to precache, set an specific strategy or cache for an specific route, for example, as workbox comes with a bunch of plugins to help us do that. Sounds very interesting, right? And it actually is.
If you are going to implement a SW using Workbox, you can remove whatever code you had in your
Workbox is at the version 3.4.1 (at the moment of this post) and very consolidated. In order to get started, we first need to import the
workbox-sw.js file in our service worker. Following Google’s tutorial, you have this:
Wait, is that it?
Well, yea… That’s the starting point. But you are not wrong, just by doing that we already have a working service worker. The difference is that now we’ll start writing more professional rules to our caching machine.
One of the greatest benefits of using Workbox is to be able to write rules and easily specify the kind of information we want to cache and where this information is coming from. To make it a little easier to debug, let’s add a few lines to our code. Inside our
if statement, let’s add this:
First we are defining the
CACHE_PREFIX constant which will help us identify our cached files later. Second, we are setting three specific properties to Workbox. They are:
- clientsClaim: Instructs the latest service worker to take control of all clients as soon as it’s activated;
- debug: Makes Workbox perform extra checks on the inputs and outputs arguments, provides extra logging and come unminified;
- skipWaiting: Instructs the latest service worker to activate as soon as it enters the waiting phase.
The last line is used to set the log level of Workbox to only warning logs. We use that to avoid clogging up the console with logs.
You can read more about troubleshooting and debugging Workbox in here.
We can finally get started writing specific rules to our application! This is really important when implementing a PWA on a legacy system like ours. As I mentioned at the first part of this article we had a cache hell at first. Every single thing was cached even stuff we didn’t need to. So writing rules allows us to be precise about what we intend to save.
With that piece of code we are specifying type of files we are accepting (JS or CSS), the name of the cache group where we’re going to place them, setting the max number of entries to 20 files and to only accepted files that returned with a status code of 200 (OK). This last one is important as we don’t want any failed request that might break our code.
If you want to force and cache every JS/CSS it comes, you can change that array to
[0, 200]. That means everything will be cached.
That regular expression says we will accept everything that is a JS or a CSS file, coming from the root URL, but we can specify that we’ll be receiving them from a CDN. Pretty cool, right? Let’s change things a little bit.
Let’s say our CDN’s root URL is https://production-cdn-url.com. We can write a regular expression to make sure that the JS and CSS files will only be cached from that URL:
The same can be done for images and everything else. Would you like to write a separate rule for images? Let’s do it together:
The maximum number of entries depends on the amount of cache you want to have in your application. In our case, we had a huge amount of data transferred for each route, which means we would have a lot of cached information so that is a way to limit that.
We can see what our cache looks like by opening Chrome DevTools, going to Application > Cache > Cache Storage. There you’ll see each separate cache group as specified (e.g. MyApp-images).
You probably realized that we set up an specific strategy for the previous examples (staleWhileRevalidate for js/css and cacheFirst for images). Workbox has a few different strategies that can be used. It is good to read each of them and pick the best one for your scenario. This way you can be very straight forward and tell your service worker what to totally ignore from cache or what is definitely mandatory, for example. The strategies are:
Stale-While-Revalidate: It allows you to respond the request as quickly as possible with a cached response if available, falling back to the network request if it’s not cached. The network request is then used to update the cache.
Cache First: If there is a Response in the cache, the Request will be fulfilled using the cached response, the network will not be used at all. If there isn’t a cached response, the Request will be fulfilled by a a network request and the response will be cached so that the next request is served directly from the cache.
Cache Only: The cache only strategy ensures that requests are obtained from a cache. This is less common in workbox, but can be useful if you have your own precaching step.
Network First: For requests that are updating frequently, the network first strategy is the ideal solution. By default it will try and fetch the latest request from the network. If the request is successful, it’ll put the response in the cache. If the network fails to return a response, the caches response will be used.
Network Only: If you require specific requests to be fulfilled from the network, the network only is the strategy to use.
You can see a full explanation about the strategies here.
Making Sure You Are Ready For Production
If you want to deploy everything you just did to a Development environment in order to make sure everything is alright you’ll need to make sure you have two specific things: a SSL certificate and a HTTPS protocol. Yea, that’s right. For the service worker to be able to register and start caching information, you will need to have those two.
Once all the security protocols are attended and you have published your system to the development environment just go ahead and open it on your browser. You are now able to check a score for your PWA that goes from zero to 100. To make sure we have everything ready for production, we are going to use Lighthouse. To do so, open Chrome DevTools, go to audits and click “Perform an audit”. You can select only the Progressive Web App option and run. That process will give a realistic score and tell exactly what has to be fixed before you are finally ready for production without risks.
Let’s take a look at the report.
For this example I removed the 512x512 icon from the
manifest.json. As you can see, Lighthouse tells us exactly what is missing in the configuration for the custom splash screen. Also, on production we do redirect HTTP to HTTPS, that one is usually not attended by anyone on development mode. About the page load on 3G, Lighthouse has a full explanation about what took longer than expected to load and why it is important to load faster. When you run it, you can take a look at the full report.
Ok, I am pretty sure you are a (Jedi) PWA Master now! I'm really glad we’ve made this far together so congratulations on being persistent and reading everything!
Become a PWA Master you have.
I tried to cover as many things as possible in this article, but if you still have questions or simply a feedback, please leave a comment!
Feel free to share with your friends and leave it a clap! 👏