I Built A Progressive Web App Shell Which Loads Under 1 Second!
As we continue to see areas like Machine Learning, Artificial Intelligence, Data Science, Quantum Computing evolve, the area of Web Development is also not left behind. We have Progressive Web Apps (PWA) now. Gone are the days when your web app would stop working offline and show the “beautiful” dinosaur! Yes, I mean it. Dinosaur my friend, sorry if that hurt you, but it’s time for users to see something more “beautiful” now. That is how evolution is supposed to work, isn’t it? Anyways, let us dive into the main discussion.
What is an App Shell?
An App Shell is an architectural pattern for building Progressive Web Applications where you only ship the minimal critical resources in order to load your site and later lazy load other non-critical data or resources which gives a native app like feel to the users.
In other words, you can think of the App Shell as your page skeleton which needs to be there even when your app goes offline! That means you will be caching your App Shell and critical resources altogether for a powerful first-time load and later load the remaining data (lazy loading) required for your application. Popular frameworks like Angular and React still face a lot of issues due to heavy architecture and bundle sizing in this matter, although, these things can be taken care of with some very clever implementation of concepts.
Sample App Shell
Before we start digging, it is a good idea to know what an App Shell actually looks like and how the performance comes into picture. The following example will give a demonstration of this:
You can see how fast the shell loads and later on after the shell gets loaded, you can use it to load other parts of your application which may have data fetched asynchronously by use of lazy loading strategies. (In the demo, App Shell is not used to making requests for data as of now. I am currently building a version of Hacker News which will use this concept.)
Near to the end of the article I have also listed the performance benchmarks of page load time of the App Shell. But don’t miss the content presented in the next few sections which will help in achieving that sweet spot of 1 second page load time.
The Page Skeleton
The page skeleton forms the most important part of the App Shell concept. Here is an example of the index.html which was used for the above demo.
Here you will find I have broken down the above code into the following parts:
- Critical Inline Styles (Line 10)
- HTML section of the Application Shell (body section spanning from Line 17 to 54)
- Critical Scripts Section (End of body at Line 53)
Also in order to achieve a perfect PWA score, the manifest.json file will be required along with icons. You will find the GitHub link to the full source code at the end of this article.
Critical Inline Styles
The critical inline styles section is the place where all the important styles only related to the App Shell must go in. The major reason for this is because if you opted to keep the CSS as an external file, until and unless it is found in HTML file and gets loaded, the view of your page will be blocked from parsing (Some time is spent in downloading the CSS as well and then it gets parsed onto CSSOM).
This is how DOM (Document Object Model) and CSSOM (CSS Object Model) construction works under the hood and CSS is a render blocking resource this way. For more information on why CSS is render blocking, refer to the following articles in-depth:
The following article talks about the critical rendering path and is a highly important topic regarding optimizing the performance of load times.
Learn how the browser constructs the DOM and CSSOM trees.developers.google.com
The following article talks about CSS as a render blocking resource and how render blocking can be avoided:
By default CSS is treated as a render blocking resource. Learn how to prevent it from blocking rendering.developers.google.com
HTML Section Of The Application Shell
If you notice the HTML section, you will find that I have used a section for the navigation bar and a section for the main part of the application. If you also want you could add in the footer section below the main section.
You can use the main application section (lines 48–50) to dynamically load the main content of your app asynchronously. This way, the page load will not get hampered or blocked and your users will certainly feel the speed of evolution instantly (i.e., your users will always get feedback from your application).
I am currently using this pattern in my local development setup to build the Hacker News application from the ground up. Certainly, this has come a long way and is running great with the all the feed getting loaded lazily. I will definitely write regarding that application once I finish my research on it.
Critical Scripts Section
async helps to download scripts asynchronously and loads them as soon as it finishes the download. It does not depend on the order in which the script tags are found and it may cause issues if one of your scripts depend on another script. So how do we solve this problem in case we want to order?
defer comes to the rescue by helping us to download the scripts asynchronously and even after they do not get the download in order, they at least get executed in order of the script tags found in the HTML document. Pretty handy indeed!
Caching Critical Resources
The boom of Service Workers has definitely caused a lot of uproar in the area of offline first applications. Yes, our App Shell is going to be offline first. I will be writing an article later on regarding the lazy loading of resources and also dynamically caching them with the help of service worker, but for the time being this article will strictly concentrate only on the App Shell.
The following file is known as the service worker script:
This script runs parallel to the main browser thread and acts as a proxy which intercepts requests and gives back responses.
The main concepts from the above file can be broadly divided into 3 areas:
- Caching Static Resources During Install Event (Lines 8–23)
- Update Service Worker And Delete Old Cache (Lines 25–44)
- Cache Then Network Strategy (Lines-46–54)
A basic knowledge of service worker will be required for this. For more details regarding service worker and its usage, refer to the following article for in depth explanations:
Rich offline experiences, periodic background syncs, push notifications&mdash;functionality that would normally…developers.google.com
You can also refer to this amazing course by Udacity in collaboration with Google:
Learn how to develop offline-first web application using Service Workers and IndexedDB.in.udacity.com
Caching Static Resources During Install Event
It is very important in order to give the names of the files which need to be cached using the browser’s Cache API. This Cache API goes a long way into making sure that further requests get fetched from the cache when required.
The install event of the service worker is the perfect time to specify the file names to be cached. Lines 8–23 of the code help exactly in doing that. The files which get cached are index.html (Line 14’s ‘/’ caches the default HTML file directly), offline.html and app.js. Yes, offline.html does exactly what you think. It was showing the piece of offline content as soon as I switched to offline mode in the demo and that was all coming from the cache.
Update Service Worker And Delete Old Cache
The activate event of the service worker (lines 25–44) which is the activate event. After the service worker gets installed, the activate event gets fired immediately if no service worker was registered previously. We filter out the cache names accordingly such that it is not the same as the current static cache name which we will be using and delete all our previously cached content that way.
This part is very important if we want to regularly update the client’s browser with the latest cache data and also clean the unnecessary old cache.
For this every time, you will need to bump up the static cache name (Line 1) whenever you change any part of the service worker file content. Always remember a byte change in the service worker is equal to a new service worker getting installed.
Cache Then Network Strategy
This part of the code (lines 46–54) actually helps in first searching the data from the cache. The fetch event of service worker takes place every time an XHR request is fired by the client’s browser.
If the cache contains the response we immediately send it to the client without wasting any time, otherwise, we go to the network to fetch the data.
So, whenever there is a request for offline.html the browser will always get the item from the cache and show it when the app is in offline mode. The similar thing happens when you cache index.html ‘/’ as per line 14 and you refresh the page next time. All your requests will be fetched from the cache using service worker and not from an external network.
This way of handling responses dynamically really goes a long way into showing the power of service workers and the amount of data we can really hack.
For the unethical hackers out there, believe me, you can use service worker only with HTTPS and localhost for that matter. Hence, service workers were made in such a way so as to make sure that security considerations were already in place. Sorry unethical hackers!
So after all these ground breaking changes, you may still ask, “What do we get out of it? What is the impact created by it?”. I have listed the performance benchmarks below. That will answer all your questions.
Performance Benchmarks Using Lighthouse
The App Shell was tested in Chrome Browser’s Incognito mode using Lighthouse in Mobile device mode. Also, the test was carried out at http://localhost:5000/ which is and not HTTPS. Here are the results:
The PWA score can be made 100 once you deploy the app and use HTTPS in it. Best Practices score can be made 100 using HTTP/2 which also comes built in with Firebase and other popular hosting services.
But the most important part can be seen in the Performance of page load of the App Shell itself. We have hit a load time (First Meaningful Paint) of 600ms here with the Audit which is way less than the desired sweet spot of 1 second page load time.
The size of the application will grow in future, but if the App Shell is engineered correctly aimed at performance like it is done above along with Lazy Loading the rest of the application components, you will achieve these results too. Yes and always remember, #perfmatters (Performance Matters!).
Lazy Loading Data Dynamically
One of the main reasons why I am stressing on lazily loading the content of the application afterward is because you may have a lot of data which needs to be fetched from the server, during that time if the user still sees an empty page or nothing is happening it will impact the user experience heavily.
For example, after the App Shell gets loaded, you may want to show a loading spinner as a part of the shell on the page during which the application will load its first page data asynchronously in the grey space area. This way your users will have some feedback since the page shell has already been loaded and you will be using the idle time judiciously to load the dynamic data from the server as well. Neat stuff!
The front-end architecture is as important as the back-end architecture you design for your applications. We often tend to forget that most of the performance related issues related to page load takes place due to the front-end architecture. Yes, it is important to architect the front-end in case you want to avoid living in the stone ages (Sorry dinosaur!).
The following article inspired me to build my own App Shell from scratch.
Application shell architecture keeps your UI local and loads content dynamically without sacrificing the linkability…developers.google.com
If you guys found this article helpful do let me know in the comments section below and please do not forget to clap. I have more of such articles planned which I will be publishing with respect to the ongoing research of using all these concepts with a real-world application like Hacker News.
Here is the link to my GitHub source code:
This repository contains a basic app shell which uses the following concepts: critical resource rendering, service…github.com