Faster Web App Delivery with PRPL

October 17, 2020

PRPL is a pattern for structuring and serving web applications and Progressive Web Apps (PWAs) with an emphasis on improved app delivery and launch performance. The letters describe a set of ordered steps for fast, reliable, efficient loading:

  • Push all resources required for the initial route – and only those resources – to ensure that they are available as early as possible
  • Render the initial route and make it interactive before loading any additional resources
  • Pre-cache resources for additional routes that the user is likely to visit, maximizing responsiveness to subsequent requests and resilience under poor network conditions
  • Lazy-load routes on demand as the user requests them; resources for key routes should load instantly from the cache, whereas less commonly used resources can be fetched from the network upon request

The PRPL pattern loading sequence

The server and Service Worker together work to precache the resources for the inactive routes. When the user switches routes, the app lazy-loads any required resources that haven't been cached yet, and creates the required views.

Twitter.com has used the PRPL pattern in production since 2017. Below we can see their use use of granular code-splitting for critical scripts and <link rel=preload> to push them so that they are available as early as possible:

The PRPL pattern: preloading critical scripts

Additional routes are lazy-loaded on demand, with Twitter serving 40+ chunks on-demand throughout the user-experience. Twitter also (offline) precaches assets for additional routes using Service Workers, to improve responsiveness to subsequent navigations:

The PRPL pattern: offline-caching resources

Their application shell (skeleton UI) is also offline cached, instantly loading even when users load the site on a slow or spotty network connection:

The PRPL pattern: application shell

Why PRPL?

Apps built using PRPL strive to be reliable, fast and engaging. Beyond these basic goals, PRPL aims to:

  • Improve an app's interaction readiness. It does this by ensuring that no extraneous resources are sent to the browser before the first view renders and becomes interactive.
  • Increase an app’s caching efficiency, especially over time. It does this by sending resources to the browser at high granularity. When resources are unbundled or bundled less aggressively, each change to your code invalidates less of your cache.
  • Reduce the complexity of development and deployment. It does this by relying on the app’s implicit dependency graph to map each entry point to the precise set of resources required, reducing or eliminating the need for manual management of bundling and delivery.

This is a useful mindset because today’s typical app is far heavier than it needs to be. To help deliver better experiences on the mobile web, we need to start by making apps lighter overall. This implies understanding and carefully considering the weight of everything we include – both our own code and our dependencies.

But that’s not enough. We’re also delivering our apps in an inefficient fashion, typically serving an all-in-one bundle containing an app’s full set of resources. This bundle must be received and processed on the client in its entirety before the user can do anything. Going forward, we need to structure and serve our apps such that:

  • Upon a user’s initial request, we deliver and process only those resources necessary to support the requested route, ensuring that our app is interactive as quickly as possible
  • Once interactive, we begin opportunistically delivering the additional resources required to ensure that our app is instantly responsive to follow-on user requests.
  • Future updates to our app are maximally efficient, consuming as little bandwidth and time as possible

What technologies does PRPL prescribe?

PRPL is a conceptual pattern that might be implemented in various ways, but it is most easily and effectively realized by utilizing some combination of the following modern web features:

  • A modern module system like JavaScript Modules, so that tools can easily construct a complete dependency graph
  • Service Workers to precache ("install") resources for subsequent app views (enables offline-first architecture)
  • Preload, for delivering required resources as quickly as possible. You can also leverage preload link headers which can be intercepted by cooperating servers and upgraded into HTTP/2 Server Push responses. It’s important to keep in mind that while powerful, Push has known challenges, however PRPL's use of Service Worker's can side-step the over-pushing problem (only push for the initial load).

How do you implement the PRPL pattern?

A big part of PRPL is turning the JS bundling mindset upside down and delivering resources as close to the granularity in which they are authored as possible (at least in terms of functionally independent modules). How do you achieve granularity?

Route-based or component-based code-splitting and lazy-loading

You're already writing things as components. Maybe you're using ES modules. For Webpack, we use dynamic import and code-splitting to split your codebase into chunks that are loaded on demand.

Meta-frameworks such as Next.js and Nuxt.js implement route-based code-splitting by default. If you are using a tooling boilerplate like create-react-app you would rely on dynamic import with a router like React Router to add route-based or component-based code-splitting to your application.

For the push/preload portion of PRPL, Webpack also supports preload as a magic comment for preloading critical scripts.

Pre-caching

Pre-caching your remaining routes can be achieved using service workers. It’s not uncommon to leverage a service worker library such as Workbox to simplify the process of precaching routes and chunks for your application.

What application structure does PRPL use?

PRPL encourages a single-page app (SPA) architecture with the following structure:

  • The main entrypoint of the application which is served from every valid route.
    • This file should be very small, since it will be served from different URLs therefore be cached multiple times. All resource URLs in the entrypoint need to be absolute, since it may be served from non-top-level URLs.
  • The shell or app-shell, which includes the top-level app logic, router, and so on.
  • Lazily loaded fragments of the app.
    • A fragment can represent the code for a particular view, or other code that can be loaded lazily (for example, parts of the main app not required for first paint, like menus that aren't displayed until a user interacts with the app). The shell is responsible for dynamically importing the fragments as needed.

The application shell skeleton pattern

The app should call dynamic import to lazy-load fragments as they're required. For example, when the user changes to a new route, it imports the fragment(s) associated with that route. This may initiate a new request to the server, or simply load the resource from the cache.

Conclusions

Beyond targeting the fundamental goals and standards of PWAs, PRPL strives to optimize for:

  • Minimum interaction readiness - specially on first use (regardless of entry point)
  • Maximum caching efficiency, especially over time as updates are released
  • Simplicity of development and deployment

PRPL is a pattern that has been used at scale since its introduction in 2016 and continues to be a worthwhile approach to loading worth considering for your app.

Further reading

Credit to Gray Norton, Kevin Schaaf, Alex Russell and many others for their contributions to PRPL and PRPL guidance over time. With special thanks to Twitter.com for their approvals to use the screenshots in this post.