Progressive Web Applications (PWAs) have been one of the hottest topics in the tech community. They bring the best of both worlds — the reach of the web with the user experience of native apps.
A few months ago, WWWID launched a challenge: build a web app that can load and become usable in under 5 seconds on a slow 3G connection. The task was to create a simple RSS reader app based on WWWID’s Medium publication.
In this article, I’ll walk through the methods I used to build a PWA for that challenge using React JS (powered by Create React App). CRA comes pre-configured with a service worker, which is a great starting point for building PWAs.
Make sure your service worker is properly registered.
// CRA v1
import registerServiceWorker from './registerServiceWorker';
registerServiceWorker();
// CRA v2
import * as serviceWorker from './serviceWorker';
serviceWorker.register();src/index.js
Starting Loader

React JS is a powerful framework, but it’s relatively “heavy” due to its rich features. This can cause a delay in the initial load, leaving users staring at a blank white screen before the DOM renders — not a great user experience.
To fix this, we can add a Starting Loader, which appears during the first second while the app is loading.

You can add this loader directly in your index.html, then remove it in React’s componentDidMount() lifecycle method. You can also style it with CSS for a nicer look.
<head>
<style type="text/css">
#startingLoader {
...
}
</style>
</head>
<body>
<div id="root"></div>
<div id="startingLoader">
Wait a second…
</div>
</body>
src/components/App.jsx
componentDidMount() {
const elem = document.getElementById('startingLoader');
window.onload = () => {
if (elem) {
elem.remove();
}
};
}public/index.html
PWA Configuration on Android and iOS
Android
When you create a React app using CRA, it automatically generates a manifest.json file inside the public folder. This file controls how your PWA appears to users — splash screen, app icon, start URL, etc.
However, when testing with Lighthouse, you might see some warnings like these:
- Does not redirect HTTP traffic to HTTPS
- User will not be prompted to install the Web App
- Not configured for a custom splash screen

How to fix them:
- Redirect HTTP to HTTPS on your server.
- Add app icons of 192×192 and 512×512 pixels in addition to the smaller default ones.
- Change the
start_urlfrom"./index.html"to"/", since"./index.html"might route to a 404 page in your app.
{
"name": "React PWA WWWID",
"short_name": "REACT WWWID",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "/",
"theme_color": "#171717",
"background_color": "#171717",
"display": "standalone",
"orientation": "portrait"
}You can learn more about Web Manifest here.
iOS
For iOS, PWA setup requires adding several meta and link tags inside the <head> tag.
<meta name="apple-mobile-web-app-title" content="React PWA WWWID">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/apple-touch-icon.png">
<link rel="apple-touch-startup-image" href="%PUBLIC_URL%/assets/splash/launch-640x1136.png" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="%PUBLIC_URL%/assets/splash/launch-750x1294.png" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="%PUBLIC_URL%/assets/splash/launch-1242x2148.png" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="%PUBLIC_URL%/assets/splash/launch-1125x2436.png" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="%PUBLIC_URL%/assets/splash/launch-1536x2048.png" media="(min-device-width: 768px) and (max-device-width: 1024px) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="%PUBLIC_URL%/assets/splash/launch-1668x2224.png" media="(min-device-width: 834px) and (max-device-width: 834px) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="%PUBLIC_URL%/assets/splash/launch-2048x2732.png" media="(min-device-width: 1024px) and (max-device-width: 1024px) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait)"><link rel=”apple-touch-startup-image” /> defines the splash screen image for iOS devices. You can use media queries to adapt it to different screen sizes.
You can learn more about PWA configuration for iOS here.
Code Splitting
As web apps grow, so does their bundle size — and long load times can frustrate users, especially on slow mobile networks.
Code splitting helps by breaking your JavaScript into smaller chunks, so users only download the code needed for the current route or feature.
I used react-loadable, a library that supports preloading, delay, timeout, and loading components.
Other alternatives include loadable-components, react-async-component, and react-code-splitting.
Example of route-based code splitting with react-loadable:

Each JavaScript file is split into smaller chunks per route.
You can also use it on hidden components, like modals, to defer loading until needed.

Image Optimization
Images often make up the majority of a webpage’s size. Optimizing them reduces page weight, load time, and bandwidth — crucial for users on mobile data.
Lazy Loading Images
Like code splitting, lazy loading ensures that images are only loaded when they appear in the viewport.
I used the react-lazy library because it’s simple and uses the IntersectionObserver API (don’t forget to include a polyfill if needed).
import { Lazy } from 'react-lazy';
<Lazy ltIE9>
<img src="https://..." alt="..." />
</Lazy>WebP Support
WebP is a modern image format that provides high compression with minimal quality loss —
- Lossless WebP images are ~26% smaller than PNGs
- Lossy WebP images are ~25–34% smaller than JPEGs
You can easily convert images to WebP using services like Cloudinary.
However, note that WebP isn’t supported on iOS browsers and Firefox (as of writing).
Resource Hints (Preload, Prefetch, Preconnect)
These are HTML hints that tell the browser which resources to load early.
Preload
Used for high-priority assets like fonts, CSS, or critical JavaScript.
<link rel=”preload” href=”https://fonts.googleapis.com/icon?family=Material+Icons" as=”style”>Prefetch
Used for low-priority assets that may be needed soon.
Browsers download them in the background and cache them.
- Link Prefetching
Prefetching can make apps feel faster, but overusing it can slow down the current page.
<link rel="prefetch" href="/assets/image.png">“This technique has the potential to speed up many interactive sites, but won’t work everywhere. For some sites, it’s just too difficult to guess what the user might do next. For others, the data might get stale if it’s fetched too soon. It’s also important to be careful not to prefetch files too soon, or you can slow down the page the user is already looking at. — Google Developers“
2. DNS Prefetching
Resolves external domain DNS ahead of time.
<link rel=”dns-prefetch” href=”https://res.cloudinary.com">“DNS requests are very small in terms of bandwidth, but latency can be quite high, especially on mobile networks. By speculatively prefetching DNS results, latency can be reduced significantly at certain times, such as when the user clicks the link. In some cases, latency can be reduced by a second. — Mozilla Developer Network“
3. Prerendering
Preloads and renders an entire next page in the background.
<link rel="prerender" href="https://www.keycdn.com">“Theprerenderhint can be used by the application to indicate the next likely HTML navigation target: the user agent will fetch and process the specified resource as an HTML response. To fetch other content-types with appropriate request headers, or if HTML preprocessing is not desired, the application can use theprefetchhint. – W3C“
Preconnect
Opens network connections (DNS, TCP, TLS) early to reduce latency.
<link rel="preconnect" href="https://api.rss2json.com" crossorigin>Time to Audit!
1. Deploy the App
I used Firebase Hosting because it’s simple, supports HTTPS, HTTP/2, and Cache-Control.
Alternatives: Heroku, Netlify, GitHub Pages, Vercel, Surge, etc.
2. Audit with WebPageTest.org
You can test performance using WebPageTest and Lighthouse.

In my test using WebPageTest (Dulles, VA, Slow 3G mode),
the app loaded in 3.16 seconds, achieved a PWA score of 100, and a Performance score of 95.
React PWA WWWID Project
If you’re interested, feel free to explore or contribute to the project:
GitHub Repository :
Demo :
Thanks to the WWWIDPWA community — everyone’s collaboration and shared insights made this challenge both fun and educational.
I learned a lot from the process and hope this write-up helps you build faster, more reliable PWAs too.
Optimizing `Progressive Web App (PWA)` Load Time with `React JS` — Under 5 Seconds on a Slow 3G Connection
This article is part of the #WWWIDChallenge
