PWA (Progressive Web Apps) is a new generation of Web applications proposed by Google. It aims to provide reliable, fast and similar service solutions to Native applications.

This article aims to share with you the practical experience of “Ele. me M Station” in the transformation of PWA. This includes preparation for online deployment of PWA, prerender optimization for multi-page applications, and potholes that were hit (and pushed through) in practice. Some basic information about PWA will not be wasted in this article. Those who are interested in further understanding can check out the extended reading section at the bottom of this article.

The preparatory work

Q: What is the first step in becoming a PWA?

A. Write A simple service-worker.js B. Find a reliable Service Worker library C. Copy a ready-made template D. Do a Webpack/Gulp/Grunt build e. open the fridgeCopy the code

From a technical point of view, all of the above options are fine. But from the ground rules of business stability, “providing degradation solutions, error monitoring, and statistics” is the first step in deploying a PWA in a production environment.

As the saying goes, “With great power comes great responsibility”. Since Service workers (hereinafter referred to as SW) work directly in the browser network layer, bugs in SW can be easily amplified:

  1. Due to the SW cache policy, bugs in the page code will be cached and cannot be fixed in time.
  2. If the SW cache strategy is buggy, the user may not be able to update the page without the developer being aware of it.
  3. Errors in the SW can cause all pages to fail, and the impact on the business is often catastrophic.

With a variety of browsers and fragmented systems, support for the SW API has blossomed, and unexpected compatibility issues have made it harder for Chinese developers to write bug-free code

If you don’t prepare well, you’re close to a P0 accident…

Demotion scheme

Business is about reliability, and the simplest and most brutal way to improve reliability is to provide a downgrade switch, once you find things are not simple, just turn on the downgrade.

Our demotion method is simple: the page first requests the switch interface, if demoted, do not install and log out of all SW. (I’m glad we did this before we started, otherwise…)


If (support SW) {fetch(switch interface). Then (() => {if (downgrade) {// cancel all installed SW} else {// register SW}})}Copy the code

A few points to note:

  1. Be sure to uninstall the SOFTWARE instead of simply not installing it. This is because some users may have visited the website before the downgrade, causing the SW to be installed, and the downgrade switch will not work for these users without logging out.
  2. The degrade switch needs to be instantaneous, so neither the server nor the SW should cache this interface.
  3. If a fault occurs and is degraded, troubleshooting may be affected. Therefore, you can add a debug mode that is hidden to users (for example, the URL is passed in a specific field) and ignore the degraded interface in the debug mode.

Monitoring errors

The SW runs in the worker thread, and the errors it throws cannot be caught on the page, so error monitoring needs to be introduced in the SW. (Thank you for providing the SW version of the error monitoring SDK)


Self. addEventListener('error', event => {  // event.message // event.filename // event.lineno // event.colno // event.error.stack }) Self. addEventListener('unhandledrejection', event => {// report error // event.reason})Copy the code
  1. Most OF the SW apis are promise-based, where unhandled errors in promises trigger unhandledrejection events instead of error events.
  2. Both events can only be registered in the initial life cycle of the worker thread. (Otherwise it will fail, console can see warning)

Data statistics

Data statistics can help us better understand users and provide data support for business growth. At the same time, its curve jitter can assist error monitoring and provide monitoring guarantee for production environment. Statistics in PWA follow normal flow, but here’s a focus on pWA-specific “add to home screen” events.


// The 'event. UserChoice' event is a Promise, Resolve event.userchoice. then(result => {console.log(result.outcome) // 'accepted': added to the primary screen // 'dismissed': The user ignores you and throws a cancel at you})})Copy the code

(See this article for more advanced postures added to the home screen, such as delay or cancel hints.)

Multi-page application of PWA transformation practice

Once you’ve done your homework, you’re ready to start writing code. If your site is a one-page structure, congratulations, you just have to:


  • Use several SW libraries, such as sw-Precach-webpack-plugin;
  • Find a manifest.json and copy it, like ours;
  • Run with Lighthouse and follow the prompts to improve;
  • Compare with Google’s PWA Checklist and make improvements as prompted;
  • Debug wechat /QQ browser;
  • Debug the UC browser.
  • Debug baidu browser;
  • Debug 360 browser
  • Debug cheetah browser
  • Find 10 Android machines and Debug their built-in browsers.

It’s basically OK.

However, if your site is multi-page like ours, you may encounter some additional complications. You can consider refactoring to a single page app, because a single page app can provide a better interactive experience in many scenarios. However, a single page app also has its own disadvantages. Whether it is worth transforming for its advantages depends on your needs.

Single page and multiple pages have always been the front end of the “struggle”, in fact, “hungry me M station” was once a single page, then why do we turn to multiple pages?

From the perspective of the company’s business, M station only provided Web delivery service at the beginning, and gradually evolved into a collection of various micro-services. These services are relatively independent and can be independently provided to all kinds of entrances (TWO-DIMENSIONAL code, wechat push, various App access, etc.), so the idea of “servitization” of M station is chosen.

From a development pattern perspective, the multi-page architecture implies weak coupling, where different pages (i.e., services) do not affect each other and can be developed and upgraded independently. For example, in the process of Vue2 migration and Weex access, we can iterate each individual service one by one while retaining the original version for degradation and A/B Test, which meets the requirements of iteration speed and stability required by our business.

Problems with multi-page structures

In the process of transforming the PWA, the multi-page application also presents some problems:

Switching between multiple pages is expensive. Even if all resources are cached and network latency is eliminated, the time for the browser to destroy pages, parse HTML, execute JS, render new pages and other actions is still high and almost unavoidable.

In order to improve the user experience, we decided to optimize the rendering process of the page to maximize the speed of the first screen rendering.

Improve first screen rendering speed with App Shell

One of the main ways to improve initial rendering is to use “App Shell”, which is a lightweight interface framework that can be cached. It is usually pure HTML fragments, including only inline CSS and Base64 images, and does not rely on the JS framework. Rendering can be done before loading, parsing, and executing JS, virtually eliminating white screen time and greatly improving the user experience.

So, how to write an App Shell elegantly? Since rendering is required before JS loads, does that mean you have to write DOM instead of Vue?

Fortunately, Vue 2 introduced the noble Server Side Rendering, SSR (not SSR in mobile games), which can render Vue components in Node.js and output them as HTML fragments. So we can call Vue SSR during the build phase for App Shell rendering, which is called prerendering. See vue-server-renderer and Vue-HackerNews for details.

App Shell rendering optimization

In practice, however, we have found that App Shell rendering is slower than expected: it always renders after synchronous JS parsing is complete. The following Demo shows this problem:


<! DOCTYPE HTML >< HTML >< head></head> <body> <h1>Hello, shell</h1> <script> i < 1000000000; i++); </script> </body> </html>Copy the code

As you can see from the profile analysis, although the HTML fragment appears before JS, the browser still renders after JS execution. As a matter of fact, different browsers render HTML differently, and optimizations like defer/ Async loading JS don’t necessarily work, so I won’t go into detail here, but a simple hack that works: Delay time-consuming operations to the task queue in the Event Loop until the main call stack is empty.


SetTimeout (() => {// put initial render in setTimeout new Vue()}, 0)Copy the code

It’s just a few lines of hack, but the improvement in App Shell rendering is huge. The following is the performance comparison of M station before and after hack:

It can be seen that after the addition and optimization of App Shell, the rendering time of App Shell on the first screen on mainstream mobile devices is less than 500ms, plus the CACHE of HTML by SW, the page switching experience can be closer to single-page application.

Experience on pit

In the process of PWA transformation, another headache was various browser bugs. Due to the wide variety of browser kernel versions in China and the instability of the new API specification used by PWA, we stepped on all kinds of strange pits, and these bugs often appeared in unexpected places. This results in a blank screen in some browsers (which illustrates the importance of the downgrading scheme). However, we did not only step on the pit, but also actively communicated with Google, Tencent X5 and UC teams to promote the solution of many bugs. Here are the pits and solutions we encountered:

  1. The UserAgent in the Android WebView is incorrect, and the cookies are lost

After our experimental online PWA, the colleagues of Big Data reported to us that some “abnormal UA” flooded into their statistics. According to source analysis, this part of UA should be the custom UA of “Ele. me APP”, but the statistical data was the default WebView UA of Android system.

At the same time, we also observed an abnormal increase in the 401 status of some interfaces in the service monitoring. The 401 status means that the user failed to authenticate, so we infer that the cookies are lost due to the SW.

We degraded the PWA in time, worked with Google to identify the source of the bug, and submitted the bug to the Chrome team: 698175 – User agent string not set correctly when Service Worker makes a fetch request – chromium – Monorail

Until the WebView fixes it, you can avoid this bug by avoiding requests (usually API requests) that require UA and cookies in the SW proxy.

  1. The X5 kernel part requests to send the Q-SID header

Both wechat and QQ browsers displayed a blank screen after opening the SOFTWARE. We used debugging tools to observe that there was an extra Q-SID header in some resource requests, which caused the browser to send OPTIONS request to the CDN server and was rejected, so the page could not be opened.

We reported the issue to the X5 kernel team and quickly received technical support: the X5 kernel will fix the issue in new version (4311), and until then we can circumvent the issue by setting up custom headers on the server that allow q-SID.

  1. The 301 redirect problem occurs in the UC browser

Similarly, a blank screen appeared on our page in UC browser, but the reason for the bug was different: we found that resource requests with 301 redirect always failed in resources captured by SW. After feedback to the UC team, we got confirmation of the bug, which was caused by the kernel’s implementation of FETCH API based on the early imperfect specification. The UC team will actively promote the kernel version upgrade and bug repair. Until the fix is made, temporary solutions can be adopted: the server can avoid 301 redirect, or the SW can make special treatment for resources with 301 redirect.

  1. Other details:
  • Older Versions of Chromium don’t support cache.addAll, so consider introducing a library with Polyfill;
  • UC browser does not support cache.add. Use cache.put instead.
  • In some lower-version wechat browsers, UA is Chrome 30+ but there is navigator.serviceWorker, so do not rely on IsServiceWorkerReady to use version check instead of feature check.

It is inevitable that we will encounter many bugs when trying new technologies. However, as front-end developers, we should embrace the open spirit of Web and not only pursue workaround, but also actively contact various parties to jointly promote bug repair and make a little work for the Web.

This is not an end, but a new beginning

We have completed the core work of PWA: building the SW, optimizing the App Shell, and improving the interaction, but these are just the beginning. There is still much more PWA can do:

  • Android 4.4 and later uses Chromium as the WebView kernel, and Chromium has supported Service workers since version 40. This means that traditional Hybrid apps can also benefit from it. Optimization of WebView in App, such as preloading, caching, background synchronization, etc., can be completed by SW.

  • Server Push and Service Worker of HTTP/2 are a natural partner. Their mutual cooperation can not only greatly improve the speed of first and second page loading, but also provide more powerful prefetch function.

  • PWA can also be progressively improved to AMP.

For us, PWA is still in the present, and we will continue to explore more possibilities for PWA and continue to improve the product experience in both technology and business.

read

  • Progressive Web Apps
  • PWA Introduction: Write a very simple PWA page
  • PWA Getting started: Understand and create Service Worker scripts
  • Progressive Web App, the next generation Web application model