Chidume Nnamdi I'm a software engineer with over six years of experience. I've worked with different stacks, including WAMP, MERN, and MEAN. My language of choice is JavaScript; frameworks are Angular and Node.js.

PRPL pattern: Solutions for modern web app optimization

6 min read 1793

The CSS logo.

What is the PRPL pattern?

PRPL is a pattern used to build scalable, fast modern web apps with great user experience.

PRPL is an acronym for:

  • Push (or preload) the most important resources
  • Render the initial route as soon as possible
  • Pre-cache remaining assets
  • Lazy load other routes and non-critical assets

PRPL architecture was conceived by the Google Chrome team seeking to make the web faster.

This PRPL is an individual optimization trick developed over the years that aims to facilitate a faster web experience. This came with the advent of Service Workers, Background sync, Cache API, Priority hints, and pre-fetching.

Specifically, PRPL works for phones with a low network when the phone is offline or in data-saver mode.

In this post, we will look at each unit in the PRPL.

P – Push (or preload)

This tells the browser to fetch a resource beforehand and store it in the browser. So when we need a resource, the resource is retrieved from the browser quickly without fetching the resource over the network. This is done with the use of the rel="preload".

Preload is a declarative fetch request that tells the browser to request a resource as soon as possible. Preload critical resources by adding a <link> tag with rel="preload" to the head of your HTML document – Houssein Djirdeh

To preload a resource, we use the link tag and add the rel='preload' attribute to it.

<link rel="preload" as="style" href="style.css">

This will preload the resource style.css as a Stylesheet. The as='style' tells the browser that the resource to pre-load is a stylesheet and should be loaded as such.

Resources which can be preloaded include:

  • Web pages
  • JS files
  • CSS files
  • Media files (Audio, Image, Video, Documents, and Web fonts)
  • DNS lookup
<link rel="preload" href="/your/webpage/link">
<link rel="preload" href="/your/js/file/link">
<link rel="preload" href="/your/css/file/link">
<link rel="preload" href="/your/audio/file/link">
<link rel="preload" href="/your/audio/file/link">
<link rel="preload" href="/your/video/file/link">
<link rel="preload" href="/your/image/file/link">
<link rel="preload" href="/your/document/file/link">
<link rel="preload" href="/your/webfont/file/link">

Pre-loading and pre-fetching are basically the same.

Pre-fetching involves a process whereby the browser fetches the resources of a <link> tag and stores it in its local cache. When the user eventually requests the page via the <link> tag, the browser serves the user the cached page. This speeds up both the loading and rendering of the webpage.

To pre-fetch a resource, we use the rel="prefetch" attribute.

<link rel="prefetch">

This will pre-fetch whatever resource is there, just like pre-loading.

R – Initial render

This is a rule that states that the initial route of a web app should be rendered as quickly as possible, and that the initial route should not be lazy-loaded.

We call it the Initial Contentful Paint. No matter what the app does, it must produce a First Paint on the browser quickly. In order to optimize for a First Contentful Paint, we must eliminate render-blocking resources, optimize CSS delivery, and use SSR.

This is mostly applicable to JS frameworks because they render their payload in the browser/client-side, so if the app has a heavy payload, you would see it will have to load the JS/CSS assets before rendering its content.

So SSR on your web app will help render the web app in the server and produce the first paint before the rest of the JS/CSS arrives.
For example, in React, we can build a server-side rendered app using the ReactDOMServer.

Let’s see an example:

import React from "react" import ReactDOM from "react-dom" import App from "App"

ReactDOM.hydrate(<App />, window.root)

We are using ReactDOM.hydrate instead of render because we want the React DOM renderer to know that we are rehydrating the app after a server-side render. This means that the React DOM renderer would expect a render from a server. It would display this first. Then, the app component would then be by React from the browser.

More great articles from LogRocket:

Next, our Express server would be this:

import fs from "fs" import React from "react" import App from "./App" import ReactDOMServer from "react-dom/server"


app.get("/", (req, res, next) => { // This would render the <App /> and return it as string. const app = ReactDOMServer.renderToString(<App />)

fs.readFile("./build/index.html", (err, data) => {
    // We read the index.html file, replace the `div#root` with the rendered App component and send it to the browser.
    return res.send(data.replace("<div id='root'></div>", "<div id='root'>" + app + "</div>"))


With the above code, our React app is now SSR-enabled. It would render the First Paint before the whole load would be rendered.

CSS delivery involves inlining the important CSS to the HTML, so it is delivered quickly.

All these will help your web app render the initial route quickly and become interactive with the users before the rest of the payload arrives. It improves performance and user experience.

P – Pre-cache

This is a technique whereby assets are cached in the browser so that when the requests for the asset are made, they are served from the cache without interrupting user experience. This is very helpful when the phone is offline.

So instead of the user seeing a network failure, the cached assets are served. When the phone comes online, the assets are then refreshed from the network.

Pre-caching is done using service workers, and is mostly used in PWAs.

Pre-caching in service workers have different strategies:

  • Stale-while-revalidate: This strategy checks for the response in the cache. If it is available, it is delivered, and the cache is revalidated. If it is not available, the service worker fetches the response from the network and caches it.
  • Cache first: This strategy will look for a response in the cache first. If any response is found previously cached, it will return and serve the cache. If not, it will fetch the response from the network, serve it, and cache it for next time.
  • Network first: This strategy will try to fetch the response from the network. If it succeeds, it will cache the response and return the response. If the network fails, it will fall back to the cache and serve the response there.
  • Cache only: This strategy responds from the cache only. It does not fall back to the network.
  • Network only: This strategy uses the network solely to fetch and serve a response. It does not fallback to any cache.

There is a library called Workbox from Google. It aims to provide tools for maintaining cache in service workers, and also provides the caching strategies we just saw above to choose from.

Let’s see an example of how to use Workbox for pre-caching.

Using Workbox, we set the route and the caching strategy we want to use. Workbox will listen for the route request and determine how the request will be cached and responded to.

import { registerRoute } from 'workbox-routing'; import { NetworkFirst } from 'workbox-strategies';


// We are caching the style resources.
({request}) => request.destination === 'style',

// We are setting the style cahcing to use the
// StaleWhileRevalidate strategy.
// Use cache but update in the background.
new StaleWhileRevalidate()

The above is a simple example of how to cache our style resources in Workbox to use the StaleWhileRevalidate strategy. This will cache all our style files for faster fetching on reload. They are quietly updated from the background when there is network.

registerRoute( ({request}) => request.destination === 'script', new NetworkFirst() );

Here, all our JavaScript files are cached using the NetworkFirst strategy. They are fetched from the cache when fetch over network fails.

L – Lazy load

This is defers the loading of routes and assets in a web app to some other time.

The most important concepts of application performance are response time and resources consumption. It is inevitable that they are going to happen. A problem can arise from anywhere, and it is important to find and address problems before they happen.

Lazy loading helps reduce the risk many web app performance problems to a minimum. Lazy loading checks the concepts we listed above:

  • Response time: This is the amount of time it takes the web application to load and the UI interface to be responsive to users. Lazy loading optimizes response time by code splitting and loading the desired bundle.
  • Resources consumption: Humans are impatient creatures. We don’t like when a website takes more than 3 seconds to load. In that time, 70 percent of us will give up. Web apps should not take this long to load. So, to reduce the amount of resources loading, lazy loading loads only the code bundle necessary at the time.

Lazy loading can be done natively in Chrome without the help of external libraries. In other words, lazy loading will be supported natively by the browser.

It is as simple as adding the loading attribute to resources we want to be lazy loaded. For example, we have an image:

<img src="./big-image.jpg" />

To natively lazy load the above image, we just add the loading attribute with value "lazy":

<img src="./big-image.jpg" loading="lazy" />

The loading attribute is the key. It tells the browser that this resource is not to be eagerly loaded unless told otherwise. This attribute can be included in image, iframe, video, and audio tags to lazy load them.

The loading attribute has different values we can choose from:

  • lazy: This value makes the browser defer the loading of the resource until it comes within/into the viewport of the browser.
  • auto: This indicates no lazy loading of the resource.
  • eager: This loads the resource immediately without lazy loading it.

Lazy loading can also be done programmatically using the Intersection Observer API. Libraries in Angular, React, and Vue use Intersection Observer to lazy load components and resources.


In this post, we covered what PRPL is. It is a pattern or collections techniques set by modern web standards that dictates how to optimize our web app for efficiency. We went on to look at its different techniques, from pre-loading and pre-fetching to Lazy loading.

I’ll say the standards have been set. We just have to follow them to make blazingly-fast modern web apps.

The future is here.

If you have any questions regarding this or anything I should add, correct, or remove, feel free to comment, email, or DM me.

Is your frontend hogging your users' CPU?

As web frontends get increasingly complex, resource-greedy features demand more and more from the browser. If you’re interested in monitoring and tracking client-side CPU usage, memory usage, and more for all of your users in production, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording everything that happens in your web app, mobile app, or website. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.

Modernize how you debug web and mobile apps — .

Chidume Nnamdi I'm a software engineer with over six years of experience. I've worked with different stacks, including WAMP, MERN, and MEAN. My language of choice is JavaScript; frameworks are Angular and Node.js.

Leave a Reply