Mainline is an online clothing retailer that offers the biggest designer brand names in fashion. The UK-based company entrusts its team of in-house experts, blended strategically with key partners, to provide a frictionless shopping experience for all. With market presence in over 100 countries via seven custom-built territorial websites and an app, Mainline will continue to ensure the ecommerce offering is rivalling the competition.
Challenge
Mainline Menswear's goal was to complement the current mobile optimized website with progressive features that would adhere to their 'mobile first' vision, focusing on mobile-friendly design and functionality with a growing smartphone market in mind.
Solution
The objective was to build and launch a PWA that complemented the original mobile friendly version of the Mainline Menswear website, and then compare the stats to their hybrid mobile app, which is currently available on Android and iOS.
Once the app launched and was being used by a small section of Mainline Menswear users, they were able to determine the difference in key stats between PWA, app, and Web.
The approach Mainline took when converting their website to a PWA was to make sure that the framework they selected for their website (Nuxt.js, utilizing Vue.js) would be future-proof and enable them to take advantage of fast moving web technology.
Results
139%
More pages per session in PWA vs. web.
161%
Longer session durations in PWA vs. web.
10%
Lower bounce rate in PWA vs. web
12.5%
Higher average order value in PWA vs. web
55%
Higher conversion rate in PWA vs. web.
243%
Higher revenue per session in PWA vs. web.
Technical deep dive
Mainline Menswear is using the Nuxt.js framework to bundle and render their site, which is a single page application (SPA).
Generating a service worker file
For generating the service worker, Mainline Menswear added configuration through a custom
implementation of the nuxt/pwa
Workbox module.
The reason they forked the nuxt/pwa
module was to allow the team to add more customizations to the
service worker file that they weren't able to or had issues with when using the standard version.
One such optimization was around the offline functionality of
the site like, for example, serving a default offline page and gathering analytics while offline.
Anatomy of the web app manifest
The team generated a manifest with icons for different mobile app icon sizes and other web app
details like name
, description
and theme_color
:
{
"name": "Mainline Menswear",
"short_name": "MMW",
"description": "Shop mens designer clothes with Mainline Menswear. Famous brands including Hugo Boss, Adidas, and Emporio Armani.",
"icons": [
{
"src": "/_nuxt/icons/icon_512.c2336e.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#107cbb"
}
The web app, once installed, can be launched from the home screen without the browser getting in the
way. This is achieved by adding the display
parameter in the web app manifest file:
{
"display": "standalone"
}
Last but not the least, the company is now able to easily track how many users are visiting their
web app from the home screen by simply appending a utm_source
parameter in the start_url
field of
the manifest:
{
"start_url": "/?utm_source=pwa"
}
Runtime caching for faster navigations
Caching for web apps is a must for page speed optimization and for providing a better user experience for returning users.
For caching on the web, there are quite a few different approaches. The team is using a mix of the HTTP cache and the Cache API for caching assets on the client side.
The Cache API gives Mainline Menswear finer control over the cached assets, allowing them to apply complex strategies to each file type. While all this sounds complicated and hard to set up and maintain, Workbox provides them with an easy way of declaring such complex strategies and eases the pain of maintenance.
Caching CSS and JS
For CSS and JS files, the team chose to cache them and serve them over the cache using the
StaleWhileRevalidate
Workbox strategy. This strategy allows them to serve all Nuxt CSS and JS files fast,
which significantly increases their site's performance.
At the same time, the files are being updated in the background to the latest version for the next visit:
/* sw.js */
workbox.routing.registerRoute(
/\/_nuxt\/.*(?:js|css)$/,
new workbox.strategies.StaleWhileRevalidate({
cacheName: 'css_js',
}),
'GET',
);
Caching Google fonts
The strategy for caching Google Fonts depends on two file types:
- The stylesheet that contains the
@font-face
declarations. - The underlying font files (requested within the stylesheet mentioned above).
// Cache the Google Fonts stylesheets with a stale-while-revalidate strategy.
workbox.routing.registerRoute(
/https:\/\/fonts\.googleapis\.com\/*/,
new workbox.strategies.StaleWhileRevalidate({
cacheName: 'google_fonts_stylesheets',
}),
'GET',
);
// Cache the underlying font files with a cache-first strategy for 1 year.
workbox.routing.registerRoute(
/https:\/\/fonts\.gstatic\.com\/*/,
new workbox.strategies.CacheFirst({
cacheName: 'google_fonts_webfonts',
plugins: [
new workbox.cacheableResponse.CacheableResponsePlugin({
statuses: [0, 200],
}),
new workbox.expiration.ExpirationPlugin({
maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year
maxEntries: 30,
}),
],
}),
'GET',
);
Caching images
For images, Mainline Menswear decided to go with two strategies. The first strategy applies
to all images coming from their CDN, which are usually product images. Their pages are image-heavy so
they are conscious of not taking too much of their users' device storage. So through Workbox, they
added a strategy that is caching images coming only from their CDN with a maximum
of 60 images using the
ExpirationPlugin
.
The 61st (newest) image requested, replaces the 1st (oldest) image so that no more than 60 product images are cached at any point in time.
workbox.routing.registerRoute(
({ url, request }) =>
url.origin === 'https://mainline-menswear-res.cloudinary.com' &&
request.destination === 'image',
new workbox.strategies.StaleWhileRevalidate({
cacheName: 'product_images',
plugins: [
new workbox.expiration.ExpirationPlugin({
// Only cache 60 images.
maxEntries: 60,
purgeOnQuotaError: true,
}),
],
}),
);
The second image strategy handles the rest of the images being requested by the origin. These images tend to be very few and small across the whole origin, but to be on the safe side, the number of these cached images is also limited to 60.
workbox.routing.registerRoute(
/\.(?:png|gif|jpg|jpeg|svg|webp)$/,
new workbox.strategies.StaleWhileRevalidate({
cacheName: 'images',
plugins: [
new workbox.expiration.ExpirationPlugin({
// Only cache 60 images.
maxEntries: 60,
purgeOnQuotaError: true,
}),
],
}),
);
Providing offline functionality
The offline page is precached right after the service worker is installed and activated. They do this by creating a list of all offline dependencies: the offline HTML file and an offline SVG icon.
const OFFLINE_HTML = '/offline/offline.html';
const PRECACHE = [
{ url: OFFLINE_HTML, revision: '70f044fda3e9647a98f084763ae2c32a' },
{ url: '/offline/offline.svg', revision: 'efe016c546d7ba9f20aefc0afa9fc74a' },
];
The precache list is then fed into Workbox which takes care of all the heavy lifting of adding the
URLs to the cache, checking for any revision mismatch, updating, and serving the
precached files with a CacheFirst
strategy.
workbox.precaching.precacheAndRoute(PRECACHE);
Handling offline navigations
Once the service worker activates and the offline page is precached, it is then used to respond to offline navigation requests by the user. While Mainline Menswear's web app is an SPA, the offline page shows only after the page reloads, the user closes and reopens the browser tab, or when the web app is launched from the home screen while offline.
To achieve this, Mainline Menswear provided a fallback to failed
NavigationRoute
requests with the precached offline page:
const htmlHandler = new workbox.strategies.NetworkOnly();
const navigationRoute = new workbox.routing.NavigationRoute(({ event }) => {
const request = event.request;
// A NavigationRoute matches navigation requests in the browser, i.e. requests for HTML
return htmlHandler.handle({ event, request }).catch(() => caches.match(OFFLINE_HTML, {
ignoreSearch: true
}));
});
workbox.routing.registerRoute(navigationRoute);
Demo
Reporting successful installs
Apart from the home screen launch tracking (with "start_url": "/?utm_source=pwa"
in the web
application manifest), the web app also reports successful app installs by listening to the
appinstalled
event on window
:
window.addEventListener('appinstalled', (evt) => {
ga('send', 'event', 'Install', 'Success');
});
Adding PWA capabilities to your website will further enhance your customers experience of shopping with you, and will be quicker to market than a [platform-specific] app.
Andy Hoyle, Head of Development
Conclusion
To learn more about progressive web apps and how to build them, head to the Progressive Web Apps section on web.dev.
To read more Progressive Web Apps case studies, browse to the case studies section.