Building a hybrid-rendered PWA

Andre Bandarra
Dev Channel
Published in
5 min readAug 18, 2017

PWA Directory was initially implemented as a purely Server Side Rendered (SSR) PWA. SSR has the advantage of making pages indexable by crawlers that don't support Javascript without any extra work.

Doing SSR and pushing the CSS with HTTP/2 gets the 1st render to happen under 3s on a 3G network

When we started to work on PWA Directory, one of our objectives was to create an Exemplary PWA according to the PWA Checklist. The checklist advises that, on a PWA, page transitions should not feel like they block on the network.

One of the reasons for this is that, when a PWA is opened from the home screen, it opens in standalone mode, the browser affordances that indicate to the user that a new page is loading are lost, and the user may feel that the application is frozen or that it didn’t receive the tap on a link.

Although it is possible to improve transitions with SSR, it may be hard to properly coordinate the transitions between routes, and it doesn’t allow for the use of more complex transition strategies, such as FLIP animations.

Using Javascript to take over control of the navigation

By moving the implementation to a pure client-side-rendered approach, where an empty App Shell is rendered on the initial load and the content is requested and rendered by the shell, it would be possible to improve the transitions.

But that would result in losing some of the advantages of the SSR approach, such as indexability and progressive rendering, besides being slower to render the content on the initial load, as an extra request has to be made for the content.

In order to have the best of both worlds, we decided to move the project towards a hybrid approach. The idea is to make the application use SSR on the first load, take over control of the navigation with Javascript, and use Client Side Rendering (CSR) to implement the subsequent navigations.

To achieve this objective without having to implement the rendering code twice (once for the server side, and once for the client side), we drew inspiration from the App Shell and PRPL pattern.

On the first load, the App Shell is rendered, but instead of the content being empty, it is inlined by the server in the HTML (the Push in PRPL).

Once the first load finishes, a Javascript Router takes over control of the navigation.

When an internal link is clicked, instead of doing a full navigation, the App Shell that has been loaded in the first load is re-used and the pre-rendered content is retrieved from the server, this time without the App Shell, and the content section of the page is replaced.

Navigating inside the application shell

After the initial view, the router handles the navigations and, effectively, does Client Side Rendering. This makes it possible to implement better coordination when navigating between different routes, and implement more complex transition strategies.

The Service Worker

The hybrid strategy used by PWA Directory to load the content doesn’t depend on the Service Worker, so even browsers that don’t support Service Workers will benefit from the implementation.

However, for browsers that do support Service Workers, a few optimisations are possible. The first one is that, once the Service Worker is installed, a version of the application shell without content is added to the cache, along with the supporting resources such as the CSS and Javascript.

So, when a user returns to the application, by navigating to a full content URL, the response is replaced with the cached shell, which in turn will trigger the client-side rendered flow to load the content. Check the PWA Directory: Loading content faster in the Application Shell for a more complete explanation of the implementation currently being used.

The second main optimisation is that, whenever a user triggers a navigation inside the application, the Service Worker intercepts the request on the fetch event and adds the response to the Cache so that, on the next time a navigation to that same content is triggered the Service Worker can load the content from the cache and delivers it instantly, without going to the network.

Thanks to the Service Worker, the application shell is loaded from the cache, and the first render happens almost instantly.

Using the Service Worker cache with the regular browser cache gives us more control and allows us to create more complex caching strategies. An example would be a strategy that returns the cached result if the result was added less than 1 hour ago, and tries the network otherwise. But, if the network fails, it can still use the cached result.

Conclusion

By implementing a hybrid rendering strategy it is possible to get the best of both worlds: Pages that load quick on the first load and play well with crawlers, while taking advantage of the Application Shell to speed up further navigations and to create nicer transitions.

As most things, there are a few tradeoffs. When navigating in the Application Shell, the page structure is being downloaded with the data, so the download size is larger than just fetching data from an API.

Also, when doing CSR, the ability to progressively render the page is lost, and long pages may actually take longer to show the content. Jake Archibald has a great blogpost with a hack to improve this. We explored this in PWA Directory, but the pages are small enough (about 4k) that there's no difference.

Further Improvements

When viewing a specific page, a limited set of links is presented to users at a given time. In order to have an even faster navigation, it would be possible to pre-cache the internal links from the page, so that, when a user clicks on it, the corresponding page renders instantly. This is something that we are currently working on. Stay tuned for updates!

PWA Directory is an Open Source project. Want to report an issue, contribute or just read the code? Check out the source code on Github.

--

--