webpack / webpack-pwa

Example for a super simple PWA with webpack.
https://webpack.github.io/webpack-pwa/page-shell/dashboard.html
810 stars 52 forks source link

Feedback #9

Open addyosmani opened 7 years ago

addyosmani commented 7 years ago

First, it's awesome that you've been exploring different architecture patterns here @sokra. Thanks for reaching out for some thoughts.

Patterns đź–Ś

Here are the different patterns for PWAs that I'm aware of:

In general, the Chrome team encourage optimizing for getting a page interactive really quickly (vs showing UI a user can't interact with), but YMMV depending on the metrics you care about.

App Shell

Pro: instantly load your UI on repeat visits, only fetching minimal payloads from the network as you navigate to different routes. Avoids refetching UI from page to page.

Con: For many, this requires a site-wide rearchitecture or shipping a new app. Can push out first contentful paint / first meaningful paint for first load as you're waiting on the network to fetch JS bundle which will then fetch your JSON data.

screen shot 2017-02-12 at 1 53 31 pm

In general, this approach works best for optimizing time-to-first-paint (sometimes meaningful paint) and structures your app a lot more like a native app. On repeat visits the whole UI gets loaded locally (from Service Worker) without touching the network and it becomes straight-forward to keep caching any JSON data or static resources the page needs to be useful. Pages only download the content they need instead of re-fetching pieces of UI, like toolbars and footers.

The downside of this approach is that at its most basic, you're giving a user a skeleton user-interface without any real content for the first load and then populating it using JavaScript. This can be less optimal on spotty networks where a delay of your Webpack bundles can mean the user is just waiting looking at the skeleton screen for a while.

The advice we've been giving folks is use code-splitting and route-based chunking (see PRPL) to keep your Webpack bundles for a route very small and hopefully that makes it easier to fetch both your JSON payloads and JS without too much of a wait on the network.

Page Shell

Pro: doesn't require a site-wide rearchitecture. Full-age caching possible and caching of static resources should also be fine. Could give you a faster time to first meaningful paint. Con although page shells can be loaded on repeat visits from the SW Cache, each route needs to fetch the UI skeleton itself (toolbars, footers etc) meaning that it isn't quite as optimal for repeat visits as the App Shell model might be.

If you're working on a CMS or classic content site, it might be really hard to rearchitect for the App Shell model. You might find adding a simple SW for caching individual pages easier initially although we try to encourage AppShell's perf benefits where possible.

With Page Shell, you have a a bundle per route/page and are SW caching those, you can still give users the benefit of not having to refetch scripts for repeat visits but the HTML won't be cached quite as optimally. It's also harder to manage atomic updates when you're only updating smaller pieces of the UI.

Hybrid Shell

A hybrid model between App Shell and Page Shell offers an interesting combination of the benefits:

screen shot 2017-02-12 at 1 56 14 pm

Comparisons

Thanks to Jake for putting the below demos together a while back. We’re going to quickly walk through a comparison of some of the above models.

Server render - 3G

image

image

http://www.webpagetest.org/video/compare.php?tests=160112_VA_KFA-r%3A8-c%3A0&thumbSize=200&ival=100&end=visual First render: 0.8s First content render: 1.7s

Repeat visits will load fully cached pages from the SW cache. However, each network fetch will re-request common “shell” UI blocks like headers and footers as they’re being served in the same page. The “app-shell” pattern doesn’t suffer from this problem.

App Shell render - 3G

image

image

http://www.webpagetest.org/video/compare.php?tests=160112_VA_KFA-r%3A4-c%3A1&thumbSize=200&ival=100&end=visual First render: 0.4s First content render: 3.7s

Repeat visits now of course don’t have to re-fetch the application shell or UI pieces that have already been fetched from the network, unlike the pure server-rendered version.

However, this demonstrates a flaw in the app shell approach. The shell loads from the cache, getting a quicker first render, then the JS fetches the content, then it writes it to the page. We have no access to the streaming parser from the page, so the content has to be fully downloaded before it can be displayed. The larger the content, the more you lose vs a streamed server render.

There are some hacks going on already to reduce the issue. The service worker will start fetching the content as soon as it serves the shell, so it starts the fetch earlier than the page's JS would. But there's another hack that helps…

App shell + partial content write, 3g

image

image

http://www.webpagetest.org/video/compare.php?tests=160112_ZW_KVN-r%3A4-c%3A1&thumbSize=200&ival=100&end=visual First render: 0.2s First content render: 2.5s

This hack streams the main content inside the page's JS. There's no access to the streaming parser, but this kind-of fakes it by streaming content until 9k is available (post-unzip), then writes the partial content to innerHTML. Once the rest of the content is fetched it writes to innerHTML again. This results in some elements being created twice, but the performance improvement is > 1s over 3g. Still not as fast as a server render though.

Jake hacked the same page together using streams, where the top & tail of the page are streamed from the cache, but the middle is streamed from the server…

Stream from service worker, 3g

image

image

http://www.webpagetest.org/video/compare.php?tests=160112_7B_M9B-r%3A6-c%3A1&thumbSize=200&ival=100&end=visual First render: 0.3s First content render: 1.5s (1.7 for full above-the-fold content)

So now we've got the quick first render, but without any cost to the content render, perhaps even faster, and it'll become even faster as more of the primitives land in more browsers (transform steams & piping). This approach also means that parsing/execution rules are as you'd expect when it comes to <script> etc.

In a streams-based model:

In the case the SW effectively becomes your server, requiring very few changes on the client.

Further thoughts

With any of these models, there's going to be nuance. A lot of PWAs are fine with the App Shell approach, however if you're a content heavy side like a News publisher I could see the Hybrid model or Page Shell model being appealing. Some folks on our team are also hopeful that newer APIs like Streams will offer even better support for progressively rendering content in these types of models.

I personally suggest folks think about what metrics they are trying to optimize for and choose their architecture patterns accordingly :)

screen shot 2017-02-12 at 1 58 58 pm

sokra commented 7 years ago

Thanks for your feedback @addyosmani.

ezekielchentnik commented 7 years ago

'micro-apps' are another approach for high-availability. Perhaps not exactly what you are going for in this project, but maybe this comment will provide some insight from a real world project.

'micro-apps' offer autonomy & ownership across teams. stateless, like micro-services. Another term may be: MPAs 'multi-page-apps' https://gist.github.com/ezekielchentnik/4dd04df7094d59e80e7a

Perhaps much like 'page shell' but each micro-app acts independent and is only aware of it's self. Some obvious downsides (similar to @addyosmani cons on page shell). The pros are 'autonomy' and disposability.

Think of each page (app) as its own SPA, SSR, or whatever the app needs to do. The experience is cohesive by stitching together a shared header/footer via micro service. A micro-app can contain other micro apps (stitched via nginx server side includes). Each micro-app is containerized.

Attributes

rules on sharing between micro-apps

image