Application and practice of PWA offline caching technology in large B-side sites

Original link: https://blog.mayandev.top/2022/09/03/tech/pwa/

The construction of an operation platform that our team is responsible for, with the continuous iteration of the project, the increase of various requirements and functions, there are currently more than hundreds of sub-modules, the problem of slow site loading is gradually emerging, and the relevant indicators tend to deteriorate. The research found that there are two main reasons:

  1. Page initialization relies on many interfaces, including permissions, regions, project configuration and other related interfaces, and due to historical reasons, these interfaces have a slow response speed and high renovation costs.

  2. The loading link based on the micro-frontend + SPA application architecture is long. After the main application is loaded, it needs to request the network to load the pages and resources of the sub-application, resulting in a long white screen time for the sub-application. The figure below shows the original loading link of the current site.

In order to speed up page loading and reduce the slow request rate of resources and interfaces, PWA offline caching technology is used to cache relevant pages, resources and requests offline, and improve the loading interaction, which improves the overall use of the site. experience, and help operation students improve work efficiency.

Technical solution selection

First consider the following three questions:

  1. Why choose to use PWA?
  2. Why do you need to do offline caching with HTTP caching?
  3. Are B-end products suitable for access?

In fact, PWA (Progressive web application) technology is not a novel concept. It has been widely used in recent years, so the technology is relatively mature. By operating the Service Worker API provided by the browser, interface data and static resources can be cached offline, which greatly reduces the request processing speed, shortens the loading time of the first screen, and achieves an experience close to that of native applications. Features like push notifications, notification features and add to home screen features are also widely supported. More importantly, we can implement the core features of PWA at a relatively small cost, and the benefits are very considerable.

For applications using PWA technology, as shown in the figure below, the browser will follow the priority order of ServiceWorker Cache > Disk Cache when requesting resources, and the loading speeds of these two caches are Disk Cache > ServiceWorker Cache respectively. In addition, some modern browsers also have Memory Cache, which has better priority and loading speed than Service Cache and Disk Cache.

.png)

Image credit: Web.Dev

So most of the resources of the site have been HTTP cached before, why do you need to do offline caching? (PS: Only children make choices, adults need them all.) In fact, the two are not in conflict. After investigation, it was found that the difference in the cache acquisition time between the two is milliseconds, which is almost negligible. In the case of a large number of concurrent requests In some cases, Service Worker Cache is faster than Disk Cache . We can also get the fastest resources through resource racing. The most important thing is that the most significant benefit that offline caching brings to us is to optimize the problem that the loading link of micro frontend + SPA is too long, and to initialize page requests and main sub-applications through more fine-grained and controllable caching App-shell and resources to speed up page loading and reduce white screen time.

In addition, as a B-end product, our user group is single and fixed, but the page access frequency is relatively high. The caching strategy requested by the Service Worker interface is very suitable for our interface optimization. At the same time, the transformation cost of PWA is relatively small, which brings us considerable benefits. In addition to cache optimization, we can also use the notification push provided by PWA and add the function of the home screen to further improve the user experience. This is why we choose PWA one of the main reasons.

Plugin comparison

The construction tool of our site is the internal tool of byte, which provides a matching pwa plug-in. Here is a comparison with the webpack plug-in provided by Google.

Internal plugin workbox-webpack-plugin
introduce Encapsulation of the workbox-webpack-plugin plugin Tool library that provides a series of APIs for Service Work operations
deploy Integrate the ability to deploy, without having to configure the route of deployment separately Need to add a sw.js route
configure The configuration is simple, but there are too few configuration items, which can only cover a small part of the scene, suitable for small applications The configuration is a bit complicated, and there are many handwritten things, but it can cover most scenes.
Bale The service-worker.js file is automatically generated, and the html is automatically injected into the script registered by the Service Worker The service-worker.js file is automatically generated, and service worker registration requires handwriting, so you can customize the downgrade scheme

According to the above comparison, it is found that the custom configuration items of workbox-webpack-plugin are richer, which can cover the scenarios required by this transformation, and can customize the downgrade scheme. Therefore, the workbox-webpack-plugin plug-in is selected for the configuration of PWA.

Service Worker Offline Cache

When the page is first loaded, the Service Worker is in an inactive state and cannot intercept page requests for caching. Only in the activated state will the page request be taken over. That is to say, the first time the page is opened, all resources will still be requested. After installing the service worker, the page needs to be refreshed to have the effect of caching.

caching strategy

Cache First Network First StaleWhileRevalidate
Prioritize the use of the cache, there is no request network in the cache Prioritize network requests, and use cache for network exceptions Priority is given to the use of the cache, if the cache does not exist, the network is used; if the cache exists, the cache is used first, and the network resources are requested to update the cache in the background
.jpg) Image credit: Google Developers -20220828114924137.(null)) -20220828114930732.(null))

Precache vs Runtime Cache

The cache in Service Worker is divided into Precache and Runtime Cache.

Precache is pre-cache, which generates a pre-cache list called workbox-precaching during the build phase, keeps the build product up-to-date, and uses the cache-first strategy to set up routes to provide pre-cached URLs. As soon as the page is opened, it will request and cache the resources in the list in the background, instead of waiting for the browser to take over the corresponding request before caching.

Runtime Cache is run-time pre-cache, that is, during the use of the page, as long as the set routing rule is hit, a record will be recorded in the Service Worker.

Generally speaking, when the page is opened for the first time, Precache resources are cached in the background, and can be used after the second refresh. If it is the cache of Runtime, it generally needs to wait until the third refresh before it can be used.

Service Worker Update Policy

By default, when a new Service Worker is detected, it will go through two stages:

  1. The intall event is triggered again, but if the page is not closed, the page is still hosted by the old service worker, and the new service worker is in the waiting state.

  2. When the user completely closes the page (all tabs of the browser do not open the site) and opens the page again, the activate event will be triggered. At this point the new Service Worker takes over the page and the new cache starts to take effect.

Considering that users have the habit of opening multiple tabs, this solution cannot simply refresh the page to see the latest effect, and needs to close all related tabs, which is not very friendly to the user’s mind.

Another way is to refresh directly through the skipWaiting method, but this may cause cache conflicts. Because registering a Service Worker is an asynchronous process, there may be a risk of problems when the same page passes through two Service Workers before and after. For example, after a new function is launched, the user refreshes the page, and some resources go to the old cache. After the new Service Worker is registered, if there is an asynchronous request in the old cache, the cache and online cannot be hit, resulting in resource request failure.

Default way skipWaiting
All tabs need to be closed for the new cache to take effect Refresh the current page, the cache will take effect immediately
Advantages: Safest, no risk Disadvantages: Not very user friendly Advantages: Unperceived by users Disadvantages: Potential problems that may cause asynchronous resource requests to fail

By comparing the plans, it can be seen that both methods are inappropriate. Technology is not omnipotent, and there are shortcomings that need to be combined with product logic to make up. We use the update method of postMessage + skipWaiting, the page listens to the message event, and refreshes the page in the callback. The specific manifestation is that when the page detects that a new Service Worker is registered, a prompt box will pop up on the page, prompting the user to refresh the page. When the user clicks the Load New button, a notification is sent through the postMessage API, and all pages are refreshed and the latest cache is obtained.

-20220828115029882.(null))

resource cache

Build packaged resources

The compiled product adopts the CacheFirst scheme for pre-storage (preCache), and the cache status is updated according to the file hash or file revision identifier each time it is updated.

The workbox-webpack-plugin will recognize a special variable __WB_MANIFEST , which will be replaced by the manifest array in the build result. With each package build, the generated files are updated, which in turn triggers service worker updates.

 1
2
3
4
5
6
7
8
9
10
11
 // self.__WB_MANIFEST refers to the file list of the build
[
{
'revision' : '23fadff0671aa' ,
'url' : '/common/137.ff0671aa3b.js'
},
{
'revision' : '6324c478f69e3' ,
'url' : '/common/632.4c478f69e3.js'
}
]
 1
2
3
4
5
6
7
 // generate sw.js config
precacheAndRoute(
// __WB_MANIFEST is a built-in variable of workbox-webpack-plugin, it will be automatically replaced with pre-cache list after build
self.__WB_MANIFEST.filter(
i =>i.url.endsWith( 'js' ) || i.url.endsWith( 'css' ),
),
);

Three-party static resources

  • Fonts, pictures, icons

  • SDK, the site includes monitoring, management, Oncall customer service SDK, etc.

For font files, images, and third-party SDKs with version numbers, the Cache First strategy is adopted, and the cache time is set for 30 days. For other static resources (excluding build products), such as external styles and sdk without version number, use the StaleWhileRevalidate strategy. While hitting the cache, the background requests the latest resources and updates them to ensure the real-time nature of resources.

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twenty one
twenty two
twenty three
 const staleWhileRevalidate = ( cacheName, maxEntries ) => new StaleWhileRevalidate()

const cacheFirst = ( cacheName, maxEntries, maxAgeSeconds = 60 * 60 * 24 * 30 ) => new CacheFirst()

const sdk = {
versioned: [ 'somesdks' ],
unversioned: [ 'somesdks' ],
}

const registerSDKCache = () => {
registerRoute(
({ request }) => sdk.versioned.some( r => request.url.includes(r)),
cacheFirst( 'asset-sdk_versioned' , 100 )
);
registerRoute(
({ request }) => sdk.unversioned.some( r => request.url.includes(r)),
staleWhileRevalidate( 'asset-sdk_unversioned' , 100 )
);
};

const registerStaticAssetsCache = () => {
registerRoute( ( { request } ) => [ 'image' , 'font' ].includes(request.destination), cacheFirst( 'asset-static' , 100 ));
};

ask

Page initialization depends on many interfaces, and these requests can be cached to speed up initialization. For example, xxx/xxxx/appInfo , this interface returns some project configuration information, such as permissions, Oncall and so on. According to monitoring, the proportion of this request exceeding 1s reaches 15%, and it will block page rendering. At present, the response data of this kind of request will not be updated for a long time, so choose to use StaleWhileRevalidate to cache it.

 1
2
3
4
5
6
7
8
 const requests = [
'/xxxx/xxxx/appInfo' ,
// others...
];

const registerRequestCache = () => {
registerRoute( ( { request } ) => requests.some( r => request.url.includes(r)), staleWhileRevalidate( 'requests' , 30 ));
};

App-shell

The site uses the micro-frontend architecture, and it takes a certain amount of time to load each sub-application. Therefore, the App-shell that caches the main and sub-applications is selected to reduce the network request time of the main and sub-applications and speed up the entire loading link.

page link describe
https://xxxx/index Main application index.html
https://xxxx/subappA Sub-app A index.html
https://xxxx/subappB Sub-app B index.html
https://xxxx/subappC Subapplication C index.html
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
 const registerNavigatorCache = () => {
registerRoute(
new NavigationRoute(staleWhileRevalidate( 'navigator' , 100 ), {
// match /path or /path/123
allowlist: [ '/' , '/index' , ...navigatePaths].map( path => new RegExp ( ` ${path} /?[0-9]*$` )),
})
);
};
const registerSubAppCache = () => {
const subAppShellRegex = new RegExp ( 'xxxxx' ); // app-shell router
registerRoute(
({ request }) => subAppShellRegex.test(request.url), staleWhileRevalidate( 'subapp-shell' , 10 )
);
};

The final cache structure of Service Worker is as follows:

 1
2
3
4
5
6
7
 - asset- static // image, font
- asset-sdk_versioned // asset with version number
- asset-sdk_unversioned // static assets without version
- workbox-precache // precache data (build product)
- request // request for configuration item
- navigator // SPA routing
- app-shell // app's index.html

In addition, we also used the open-screen connection logo to wait for the main application to be loaded to further enhance the sense of use. In order to prevent the strobe caused by the fast loading of the main application, a minimum logo delay display is set, and it is performed concurrently with the main application loading.

 1
2
3
4
5
6
7

const Layout = lazy( () =>
// for fallback lading, delay 500ms at least to show the main layout
Promise .all([ import ( '../layout/index' ), new Promise ( resolve => setTimeout(resolve, 500 ))]).then(
([moduleExports]) => moduleExports
)
);

Service Worker Registration

sw.js registered in the load event of the page, and record the registration.

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
 <script>
if ( 'serviceWorker' in navigator){
window .addEventListener( 'load' , function ( ) {
navigator.serviceWorker
.register( '/sw.js' )
.then( function ( reg ) {
// Dot record registration status
// ...
})
.catch( function ( e ) {
});
})
}
< /script>

Update detection

We encapsulate a native update detection popup component, which listens to service worker life cycle events (including updatefound, controllerchange, statechange) and message events, and refreshes the cache based on user interaction.

Service Worker update detection listener

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twenty one
twenty two
twenty three
 // sw.js
self.addEventListener( "message" , event => {
if (event.data === "skipWaiting" ) {
self.skipWaiting();
}
});
// update-found.js
button.addEventListener( 'click' , function ( ) {
registration.waiting.postMessage( "skipWaiting" );
})
navigator.serviceWorker.addEventListener( "controllerchange" , function ( event ) {
window.location.reload ();
});
// ...
registration.addEventListener( "updatefound" , function ( ) {
registration.installing.addEventListener( "statechange" , function ( event ) {
if (event.target.state === "installed" ) {
// pop up update prompt
callback();
}
});
});
// ...

Downgrade plan and grayscale release

In order to avoid some problems in the cache, causing the page to fail to load or a white screen, it is necessary to provide a service worker logout solution. Add a key to the dynamic configuration center as a switch, request configuration when the page is loaded, and choose to register and update Service Worker or log out of all Service Workers according to the configuration. In addition, the configuration also includes gray-scale areas. Only when the corresponding gray-scale area is selected will the Service Worker be registered.

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twenty one
twenty two
 // tcc api
FETCH_CONFIG: `/xxxx/getconfig/` ;
window .addEventListener( "load" , () => {
fetch(FETCH_CONFIG)
.then( function ( res ) {
return res.json();
})
.then( function ( res ) {
var configData = res && res.data && JSON .parse(res.data);
if (configData && (configData.pwaDisabled || !configData.pwaEnableRegion.includes(regionName))) {
// log out of the service worker
enableServiceWorker = false ;
}
})
.finally( function ( ) {
if (enableServiceWorker) {
// register service worker
}
})
});

// Dot record

benefits and feedback

Through the transformation of the PWA, the overall FCP, Load and slow request rate indicators of the site have been significantly improved, reaching the expected values.

FCP Load Main Resource Slow Request Rate
Before 2115.38ms 2.42s 11.29%
After 1388.06ms 1.16 5.37%
Percent ?52% ?240% ?52.4%

However, indicators such as LCP and TTI are still relatively high, and further optimization will be made at the code logic level and packaging level in the future to improve the interactive experience.

  • From the level of code logic, optimize page rendering, image loading, reducing long tasks, etc.

  • Packaging level, reasonable unpacking, removal of unnecessary packages, etc.

In addition, after the function was launched, we also received some positive feedback from PM, RD and related users, which is a great encouragement for us. We will also continue to pay attention to performance optimization and continue to improve.

Finally, an advertisement is inserted. The TikTok Creator Growth team is still recruiting front-end and back-end engineers. Both school and social recruitment are available. Interested students can add WeChat to chat ?.

This article is reproduced from: https://blog.mayandev.top/2022/09/03/tech/pwa/
This site is for inclusion only, and the copyright belongs to the original author.

Leave a Comment