Home > Web Front-end > JS Tutorial > Progressive Web Apps: A Crash Course

Progressive Web Apps: A Crash Course

Jennifer Aniston
Release: 2025-02-16 08:56:15
Original
734 people have browsed it

Progressive Web Apps: A Crash Course

Progressive Web Apps (PWAs) try to overlap the worlds of the mobile web apps and native mobile apps by offering the best features of each to mobile users.

They offer an app-like user experience (splash screens and home screen icons), they’re served from HTTPS-secured servers, they can load quickly (thanks to page load performance best practices) even in low quality or slow network conditions, and they have offline support, instant loading and push notifications. The concept of PWAs was first introduced by Google, and is still supported by many Chrome features and great tools, such as Lighthouse, an open-source tool for accessibility, performance and progressiveness auditing which we’ll look into a bit later.

Throughout this crash course, we’ll build a PWA from scratch with ES6 and React and optimize it step by step with Lighthouse until we achieve the best results in terms of UX and performance.

The term progressive simply means that PWAs are designed in a such a way that they can be progressively enhanced in modern browsers where many new features and technologies are already supported but should also work fine in old browsers with no cutting-edge features.

Key Takeaways

  • Progressive Web Apps (PWAs) bridge the gap between mobile web and native apps, offering high performance and user engagement features like offline support and push notifications.
  • PWAs are accessible through browsers without the need for app store downloads, and they can be progressively enhanced to operate across both modern and older browsers.
  • Google’s Lighthouse tool is essential for auditing and optimizing PWAs, helping developers improve accessibility, performance, and adherence to progressive standards.
  • Building a PWA involves setting up a service worker, which enables features like offline support and resource caching, significantly boosting app performance and reliability.
  • The PRPL pattern enhances PWA performance by optimizing resource loading and interaction readiness, ensuring a seamless user experience even in challenging network conditions.
  • Service workers play a crucial role in PWAs, handling background sync and push notifications, and allowing apps to load nearly instantly by caching key resources.
  • The future of PWAs looks promising, with continuous improvements in web technologies and growing support from major browsers and platforms, positioning them as a compelling alternative to native apps.

Native vs Mobile = Progressive

A native app is distributable and downloadable from the mobile OS’s respective app store. Mobile web apps, on the other hand, are accessible from within a web browser by simply entering their address or URL. From the user’s point of view, launching a browser and navigating to an address is much more convenient than going to the app store and downloading, installing, then launching the app. From the developer/owner’s point of view, paying a one-time fee for getting an app store account and then uploading their apps to become accessible to users worldwide is better than having to deal with the complexities of web hosting.

A native app can be used offline. In the case of remote data that needs to be retrieved from some API server, the app can be easily conceived to support some sort of SQLite caching of the latest accessed data.

A mobile web app is indexable by search engines like Google, and through search engine optimization you can reach more users. This is also true for native apps, as the app stores have their own search engines where developers can apply different techniques — commonly known as App Store Optimization — to reach more users.

A native app loads instantly, at least with a splash screen, until all resources are ready for the app to execute.

These are the most important perceived differences. Each approach to app distribution has advantages for the end user (regarding user experience, availability etc.) and app owner (regarding costs, reach of customers etc.). Taking that into consideration, Google introduced PWAs to bring the best features of each side into one concept. These aspects are summarized in this list introduced by Alex Russell, a Google Chrome engineer. (Source: Infrequently Noted.)

  • Responsive: to fit any form factor.
  • Connectivity independent: progressively-enhanced with service workers to let them work offline.
  • App-like-interactions: adopt a Shell Content application model to create appy navigations & interactions.
  • Fresh: transparently always up-to-date thanks to the service worker update process.
  • Safe: served via TLS (a service worker requirement) to prevent snooping.
  • Discoverable: are identifiable as “applications” thanks to W3C Manifests and service worker registration scope allowing search engines to find them.
  • Re-engageable: can access the re-engagement UIs of the OS; e.g. push notifications.
  • Installable: to the home screen through browser-provided prompts, allowing users to “keep” apps they find most useful without the hassle of an app store.
  • Linkable: meaning they’re zero-friction, zero-install, and easy to share. The social power of URLs matters.

Lighthouse

Lighthouse is a tool for auditing web apps created by Google. It’s integrated with the Chrome Dev Tools and can be triggered from the Audits panel.

You can also use Lighthouse as a NodeJS CLI tool:

<span>npm install -g lighthouse  
</span>
Copy after login
Copy after login
Copy after login

You can then run it with:

lighthouse https://sitepoint.com/
Copy after login
Copy after login
Copy after login

Lighthouse can also be installed as a Chrome extension, but Google recommends using the version integrated with DevTools and only use the extension if you somehow can’t use the DevTools.

Please note that you need to have Chrome installed on your system to be able to use Lighthouse, even if you’re using the CLI-based version.

Building your First PWA from Scratch

In this section, we’ll be creating a progressive web app from scratch. First, we’ll create a simple web application using React and Reddit’s API. Next, we’ll be adding PWA features by following the instructions provided by the Lighthouse report.

Please note that the public no-authentication Reddit API has CORS headers enabled so you can consume it from your client-side app without an intermediary server.

Before we start, this course will assume you have a development environment setup with NodeJS and NPM installed. If you don’t, start with the awesome Homestead Improved, which is running the latest versions of each and is ready for development and testing out of the box.

We start by installing Create React App, a project boilerplate created by the React team that saves you from the hassle of WebPack configuration.

<span>npm install -g lighthouse  
</span>
Copy after login
Copy after login
Copy after login

The application shell architecture

The application shell is an essential concept of progressive web apps. It’s simply the minimal HTML, CSS and JavaScript code responsible for rendering the user interface.

Progressive Web Apps: A Crash Course

This app shell has many benefits for performance. You can cache the application shell so when users visit your app next time, it will be loaded instantly because the browser doesn’t need to fetch assets from a remote server.

For building a simple UI we’ll use Material UI, an implementation of Google Material design in React.

Let’s install the package from NPM:

lighthouse https://sitepoint.com/
Copy after login
Copy after login
Copy after login

Next open src/App.js then add:

<span>npm install -g create-react-app
</span>create-react-app react-pwa
<span>cd react-pwa/
</span>
Copy after login
Copy after login

Next we need to fetch the Reddit posts using two methods fetchFirst() and fetchNext():

<span>npm install material-ui --save
</span>
Copy after login

You can find the source code in this GitHub Repository.

Before you can run audits against your app you’ll need to make a build and serve your app locally using a local server:

<span>import <span>React, { Component }</span> from 'react';
</span><span>import <span>MuiThemeProvider</span> from 'material-ui/styles/MuiThemeProvider';
</span><span>import <span>AppBar</span> from 'material-ui/AppBar';
</span><span>import <span>{Card, CardActions, CardHeader,CardTitle,CardText}</span> from 'material-ui/Card';
</span><span>import <span>FlatButton</span> from 'material-ui/FlatButton';
</span><span>import <span>IconButton</span> from 'material-ui/IconButton';
</span><span>import <span>NavigationClose</span> from 'material-ui/svg-icons/navigation/close';
</span>
<span>import logo from './logo.svg';
</span><span>import './App.css';
</span>
<span>class App extends Component {
</span>
  <span>constructor(props) {
</span>    <span>super(props);
</span>
    <span>this.state = {
</span>      <span>posts: []
</span>    <span>};
</span>  <span>}
</span>
  <span>render() {
</span>    <span>return (
</span>
      <span><MuiThemeProvider>
</span>        <span><div>
</span>          <span><AppBar
</span>            title<span>={<span >React PWA</span>}
</span>
            iconElementLeft<span>={<IconButton><NavigationClose /></IconButton>}
</span>            iconElementRight<span>={<FlatButton onClick={() => this.fetchNext('reactjs', this.state.lastPostName)} label="next" />
</span>            <span>}
</span>          <span>/>
</span>
          <span>{this.state.posts.map(function (el<span>, index</span>) {
</span>            <span>return <Card key={index}>
</span>              <span><CardHeader
</span>                title<span>={el.data.title}
</span>
                subtitle<span>={el.data.author}
</span>                actAsExpander<span>={el.data.is_self === true}
</span>                showExpandableButton<span>={false}
</span>              <span>/>
</span>
              <span><CardText expandable={el.data.is_self === true}>
</span>                <span>{el.data.selftext}
</span>              <span></CardText>
</span>              <span><CardActions>
</span>                <span><FlatButton label="View" onClick={() => {
</span>                  <span>window.open(el.data.url);
</span>                <span>}} />
</span>
              <span></CardActions>
</span>            <span></Card>
</span>          <span>})}
</span>

          <span><FlatButton onClick={() => this.fetchNext('reactjs', this.state.lastPostName)} label="next" />
</span>        <span></div>
</span>      <span></MuiThemeProvider>
</span>
    <span>);
</span>  <span>}
</span><span>}
</span>
<span>export default App;
</span>
Copy after login

This command invokes the build script in package.json and produces a build in the react-pwa/build folder.

Now you can use any local server to serve your app. On Homestead Improved you can simply point the nginx virtual host to the build folder and open homestead.app in the browser, or you can use the serve package via NodeJS:

  <span>fetchFirst(url) {
</span>    <span>var that = this;
</span>    <span>if (url) {
</span>      <span>fetch('https://www.reddit.com/r/' + url + '.json').then(function (response) {
</span>        <span>return response.json();
</span>      <span>}).then(function (result) {
</span>
        that<span>.setState({ posts: result.data.children, lastPostName: result.data.children[result.data.children.length - 1].data.name });
</span>
        <span>console.log(that.state.posts);
</span>      <span>});
</span>    <span>}
</span>  <span>}  
</span>  <span>fetchNext(url<span>, lastPostName</span>) {
</span>    <span>var that = this;
</span>    <span>if (url) {
</span>      <span>fetch('https://www.reddit.com/r/' + url + '.json' + '?count=' + 25 + '&after=' + lastPostName).then(function (response) {
</span>        <span>return response.json();
</span>      <span>}).then(function (result) {
</span>
        that<span>.setState({ posts: result.data.children, lastPostName: result.data.children[result.data.children.length - 1].data.name });
</span>        <span>console.log(that.state.posts);
</span>      <span>});
</span>    <span>}
</span>  <span>}
</span>  <span>componentWillMount() {
</span>
     <span>this.fetchFirst("reactjs");
</span><span>}
</span>
Copy after login

With serve, your app will be served locally from http://localhost:5000/.

Progressive Web Apps: A Crash Course

You can audit your app without any problems, but in case you want to test it in a mobile device you can also use services like surge.sh to deploy it with one command!

<span>npm run build
</span>
Copy after login

Next, run surge from within any directory to publish that directory onto the web.

You can find the hosted version of this app from this link.

Now let’s open Chrome DevTools, go to Audits panel and click on Perform an audit.

Progressive Web Apps: A Crash Course

From the report we can see we already have a score of 45/100 for Progressive Web App and 68/100 for Performance.

Under Progressive Web App we have 6 failed audits and 5 passed audits. That’s because the generated project already has some PWA features added by default, such as a web manifest, a viewport meta and a tag.

Under Performance we have diagnostics and different calculated metrics, such as First meaningful paint, First Interactive, Consistently Interactive, Perceptual Speed Index and Estimated Input Latency. We’ll look into these later on.

Lighthouse suggests improving page load performance by reducing the length of Critical Render Chains either by reducing the download size or deferring the download of unnecessary resources.

Please note that the Performance score and metrics values can change between different auditing sessions on the same machine, because they’re affected by many varying conditions such as your current network state and also your current machine state.

Why Page Load Performance and Speed Matter

According to DoubleClick (a Google advertising company), 53% of mobile sites visits are abandoned if it takes more than 3 seconds to load the page. By optimizing page load performance and speed, PWAs offer instant web experiences to users via a set of techniques and strategies that we’ll look at next.

Consider performance before you start building your PWA

The majority of client-side apps are built using some sort of JavaScript library or framework such as React, Preact, Angular, Vue etc. If you’re building a PWA you need to make sure you choose a mobile-first library or, in other words, a library that’s designed for the mobile web in the first place. Otherwise, optimizing your app for performance will be an impossible mission.

You need to use different testing tools, like Chrome DevTools, Lighthouse, Google PageSpeed and others, to test your app heavily under different and simulated network conditions, so you can successfully optimize your app page load performance.

PWA performance metrics you need to put on your radar

You can use Lighthouse for measuring and optimizing your app’s page load performance with different metrics, diagnostics, and opportunities.

Lighthouse uses different metrics. Let’s cover them one by one:

First meaningful paint

First meaningful paint is a measure that simply indicates the time at which the user can see meaningful or primary content on the screen. The lower this audit is, the better the perceived performance of your app.

Here’s this metric for our app.

Progressive Web Apps: A Crash Course

We see that from 1.3s the browser started to render the empty background, then from 2s the browser started rendering the header, 2.4s both the buttons on the header and the bottom are rendered. It’s not until the third second that the posts are rendered. The whole process took 3.4 seconds and the first meaningful paint equals 2.340ms — when the header without the next button is rendered.

A first meaningful paint is really dependent on what we can consider as meaningful, which can be different between different users. If a user is only interested in reading the posts, then the first meaningful paint for them is after the 3 seconds mark. You can see how Google calculates this metric from this document.

Progressive Web Apps: A Crash Course

This is another filmstrip for the same app where Lighthouse reported the FMP as 2.560ms at the last screenshot where post headlines are fully displayed in the above-the-fold area.

Secondly, you can see that the page is rendered progressively, not at once, which is a good indication of performance.

You can optimize this measure by optimizing the critical rendering path.

The critical rendering path

The critical rendering path is a concept related to how web browsers render pages — that is, from the first moment of receiving HTML, CSS and JavaScript assets to the step where the browser processes and renders actual meaningful content. To optimize the critical rendering path, you need to give higher priority to content that’s related to the user’s current action. That is, if they’re about to visit your app, you can start by first displaying the visible part of the UI, or what’s called the above-the-fold area.

For more details, you can read “Optimizing the Critical Rendering Path”.

You can also see this list of curated tools for inlining critical CSS assets. Also check these tools for inlining JavaScript and other assets:

  • inliner: a Node utility to inline images, CSS and JavaScript for a web page
  • inline-source: a tool for inlining flagged JS, CSS, and IMG sources in HTML
  • inline-source-cli: a CLI tool for inline-source.

Critical Request Chains

Critical Request Chains is a concept related to the Critical Rendering Path and can be represented by a diagram which breaks down critical resources to render a page, how much time each resource takes and how many bytes to download for each resource. You can use the Critical Request Chains diagram to get a better understanding of critical resources to eliminate, defer or mark as async. Here is a screen shot from our example PWA report:

Progressive Web Apps: A Crash Course

Now let’s try to resolve this issue using inline-source and inline-source-cli:

<span>npm install -g lighthouse  
</span>
Copy after login
Copy after login
Copy after login

We then navigate inside the build folder and open index.html, then add the keyword inline to and <script> elements we want to inline:</script>

lighthouse https://sitepoint.com/
Copy after login
Copy after login
Copy after login

Let’s inline these resources:

<span>npm install -g create-react-app
</span>create-react-app react-pwa
<span>cd react-pwa/
</span>
Copy after login
Copy after login

Progressive Web Apps: A Crash Course

By inlining CSS and JavaScript assets, we’ve reduced the Critical Request Chains to 2.

First Interactive and Consistently Interactive

These two metrics both indicate the time for the user to be able to interact with the app. Both metrics express engage-ability and usability, but there’s a difference between them. First Interactive measures when the page is minimally interactive, while consistently Interactive measures when the page is fully interactive.

You can optimize the time to be interactive by optimizing the critical rendering path.

Perceptual Speed Index

Perceptual Speed Index is a metric that measures the above-the-fold visual performance of a page while taking into consideration the layout stability (no sudden displacement of UI elements). It simply indicates how quickly the page contents are visibly populated.

PSI is a modified version of the SI or Speed Index metric, which is the average time at which the above-the-fold (visible) area is displayed without taking into account the visual stability.

You can also optimize this metric by optimizing the critical rendering path.

Estimated input latency

Estimated input latency is a metric that indicates when the main thread becomes ready to process input.

You can read more about this metric and how to pass it here.

Time to first byte (TTFB)

Wikipedia defines TTFB as:

Time to first byte (TTFB) is a measurement used as an indication of the responsiveness of a web server or other network resource. TTFB measures the duration from the user or client making an HTTP request to the first byte of the page being received by the client’s browser.

You can use tools like WebpageTest and Lighthouse to measure TTFB of your PWA. For more information see this link.

Let’s now see a set of concepts and common techniques used by developers to optimize these metrics.

Code Splitting and Route-based Chunking

The JavaScript ecosystem has changed drastically in recent years, with new tools such as module bundlers like WebPack and Browserify, which are used to bundle all scripts into one file. This is considered good practice, since it helps reduce network requests for multiple script files to just one request (for getting the whole bundle), optimizing the critical rendering path (no long-blocking JavaScript and CSS assets). But the problem is, for large apps, the bundle will have a larger size, making the process of downloading the bundle, processing it, then booting up the application very inefficient, which affects the instant web experience (increasing the time for the first meaningful paint and the time for the UI to become interactive).

As a solution for this problem, different apps use code splitting and route-based chunking (splitting code into chunks which are only required for each route). So the browser only needs to download the first chunk needed to render the first page/route, then lazy loads the remaining chunks when the user is navigating other routes.

Server-side Rendering

Server-side rendering is the process of rendering the initial content on the server instead of the browser — which may, in many situations, improve the page load performance, since the browser can display content (plain HTML) immediately after downloading it.

Server-side rendering alone won’t help much in optimizing the time for the user to be interactive, since JavaScript assets need to be downloaded and booted up.

PRPL Performance Pattern

PRPL is a performance pattern which makes use of concepts such as HTTP/2 Server Push, Preload headers, service workers, and lazy loading to improve the performance of PWA delivery and launch.

PRPL stands for:

  • Push critical resources for the initial URL route
  • Render initial route
  • Pre-cache remaining routes
  • Lazy load and create remaining routes on demand.

Source: Google Web Fundamentals

Optimizing performance via caching

Caching is the process of keeping frequently requested data in a close storage location. For the web, that’s the browser memory or database. A browser actually has a cache location specifically designed for caching network responses, but developers can also leverage other storage mechanisms such as the HTML5 Local Storage API and IndexedDB.

You can cache the application shell (assets responsible for rendering the UI), data, or ideally both. Caching the UI is crucial for achieving an instant web experience. But what about data?

We can consider two categories of apps here. Apps that only need a network connection for getting assets responsible for rendering the UI and/or need it for providing the core functionality. Think, for example, of an app that provides personal accounting for users, which only depends on algorithms and calculations (local CPU).

The second category are apps that depend on remote servers to get updated information. You may wonder why you need to cache data, given that it will soon become obsolete and users mostly need updated information. The thing is, in many parts of the world the problem is not the permanent interruption of the network connection, but the fluctuating state of the network between slow and good signals, and that’s what affects the user experience even if the app is already loaded.

The app can make use of data caching (taking advantage of the Background Sync API) to guarantee its service when users are navigating between pages, or even if they leave and come back to the app in a short period of time, by continuously watching the network state, then resume fetching/sending data without interrupting the user.

Now let’s resolve the failed issues for better scores.

Registering a Service Worker

The first failed audit is saying that the app does not register a service worker. Before changing that, let’s first understand service workers and related features.

A service worker is a modern browser technology that can be used as a client-side proxy that allows your app (by intercepting network requests) to implement caching for adding features such as instant loading and offline support etc.

Service workers can also be used for implementing updates and engaging with push notifications.

Service workers can’t access the page DOM, but can communicate with a client (a Window, Worker, or SharedWorker) via the postMessage() method.

Many browser APIs are available for use inside service workers, such as:

  • the Fetch API: for fetching content (sending requests and getting responses) from a remote server
  • the Cache API: for caching content (create cache stores of responses keyed by requests)
  • the Push API: for getting push notifications
  • the Background Sync API: allows the web app to defer actions until the user has stable connectivity.

A service worker has many lifecycle events that need to be handled properly.

  • an install event: you get install events when the app is first visited by a user and the service worker is downloaded and installed
  • an activate event: triggered after calling .register() (after download and install events)
  • a fetch event: you get fetch events in case of navigation within a service worker’s scope or any requests triggered scope pages.

The React project already contains a service worker. We can either use it or create a new one so we can get a better idea of how service workers work.

In the public folder, let’s create a new file named service-worker.js, then register it from the public/index.html file by adding the following code before

The above is the detailed content of Progressive Web Apps: A Crash Course. For more information, please follow other related articles on the PHP Chinese website!

Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Latest Articles by Author
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template