Rendering Patterns in Headless WordPress

9 minute read
Theo Despoudis
Theo Despoudis
Sr. Software Engineer

One of the most common pain points among developers is understanding the possible ways to render content when it comes to Headless WordPress. Those are the three letter acronyms that you might hear or read like SSG, CSR, SSR etc. in conversations and the like. If you find yourself reading about them and don’t still don’t really understand what they represent or how they affect the user experience or performance, you might not be optimizing your site as well as you could be.

To provide insight into that topic, this tutorial explains the four key rendering patterns in headless WordPress. We start by describing what the patterns are, then we provide practical examples of utilizing them using Faust.js which is the headless WordPress Framework for React.

Let’s get started.

Server Side Rendering (SSR)

In a traditional WordPress installation, when a user visits a site, the server will construct a full HTML response and send it back to the user’s browser. The nature of this request-response cycle is dynamic; the PHP server that powers WordPress will connect to the database to fetch the latest blog posts, it will determine which template to use based on the visited page, run any filters and callbacks and respond with the full HTML string.

Visiting the same page multiple times does not mean you will get the same results every time. However, because you receive the full content of the page, search engines like Google can crawl it easier and faster. This is what Server Side Rendering or SSR is all about.

Most traditional Web 1.0 and 2.0 applications including WordPress use Server Side Rendering for delivering dynamic content. SSR offers a reasonable balance of flexibility, simplicity and performance.

Faust.js fully supports Next.js SSR. This is achieved by exporting an async function called getServerSideProps (see docs), which will be called by the server on each request, and wrap it in getNextServerSideProps:

// File ssr.js
import { client } from 'client';
import { GetServerSidePropsContext } from 'next';
import { getNextServerSideProps } from '@faustjs/next';

export default function Page() {
  const { usePosts } = client;
  return (
    <>
      <main className="content content-single">
        <div className="wrap">
          <h2>Recent Posts</h2>
          {usePosts()?.nodes.map((post) => (
            <li key={post.id}>{post.title()}</li>
          ))}
        </div>
      </main>
    </>
  );
}

export async function getServerSideProps(context) {
  return getNextServerSideProps(context, {
    Page: Page,
    client,
  });
}

Note that in this case, the server needs to request data from WordPress using a GraphQL request. For that to happen, we need to pass the Page and the client as parameters to getNextServerSideProps. This way is specific to Faust.js and it uses the client perform the request to fetch the posts behind the scenes without having to think about batching/constructing queries, or data fetching.

If you inspect the response from the server, you will notice that the whole HTML string is returned showing the list of recent posts:

SSR Response
SSR in action

If you go back and change the title of one of the post what do you think will happen? Well the new change back to the response as the server will always request the updated content:

SSR updated content.
SSR Dynamic content

However, SSR is not without tradeoffs. Notice that by default, without any optimizations or caching, the server has to do a lot of work. When the request reaches the server via the WPGraphQL endpoint it has to connect to the database. It has to gather all the data from possibly multiple database tables and has to generate the whole HTML string. SSR does not scale well when many users are hitting the website. The server has to consume a lot of memory and bandwidth to perform well consistently. There are well-known mitigation techniques to reduce the chance of hitting the server like HTTP cache control headers, object caching or HTTP cache proxies but those are still not perfect solutions.

We explore some additional rendering patterns that are available to headless WordPress technologies that help improve the speed of serving content at scale.

Client Side Rendering (CSR)

If Server Side Rendering offloads the rendering effort to the server, Client Side Rendering (CSR) on the other hand places this responsibility on the client.

When you visit a page that does Client Side Rendering you won’t get back a full HTML document. Instead, you will have a bare skeleton component, and client-side JavaScript will be responsible for populating it with content. “Client” here refers to the web browser, such as Chrome, Firefox or Safari. This is how many Single Page Applications (SPAs) work.

In order to turn our component into CSR using Faust we will have to work a bit harder. We will need to move all the fetching of the Posts inside the component and remove the getServerSideProps. This is because by default Next.js performs automatic static optimization and pre-render the pages even if you didn’t specify any props:

// csr.js
import { useEffect, useState } from "react";
import { client } from 'client';
import { prepass, getArrayFields } from "gqty";


function Page() {
  const [posts, setPosts] = useState([]);
  useEffect(() => {
    const fetchPosts = async () => {
      const {
        client: {resolved, query},
      } = client;

      resolved(() => {
        return query.posts();
      }, {refetch: true}).then(res => {
        prepass(res?.nodes, 'id', 'title', '__typename');
        setPosts(getArrayFields(res?.nodes, 'id', 'title', '__typename'))
      })
    }
    fetchPosts();
  }, []);

  return (
    <>
      <main className="content content-single">
        <div className="wrap">
          <h2>Recent Posts</h2>
          {posts?.map((post) => (
            <li key={post.id ?? ''}>{post.title()}</li>
          ))}
        </div>
      </main>
    </>
  );
}

export default Page;

It uses the useEffect() hook to perform the request on the client side. The resolved method from the GQty client performs a GraphQL request and returns a promise object. It sets the state once the data is fetched from the WordPress instance and uses prepass and getArrayFields function helpers to force resolving the specified properties of the post values from the list and return a Value object instead of a Proxy object.

Once you have visited a page that does CSR and inspect the initial HTML you will see that it renders only the heading:

CSR initial render

Then you will notice that the page will load the posts in a quick succession. This is because there is a GraphQL request pending in the background:

Client side GraphQL request to fetch the posts

This prolonged initial load time makes CSR decreases the overall user experience. Many designers use loading indicators, skeleton components or prefetching to mitigate this side effect.

Additionally because the initial Page is rendered without meaningful content the SEO score will take a hit as the crawler robot has to be able to run JavaScript to evaluate the whole page.

However the benefits of CSR is that after the initial load, the page becomes more snappy and you have almost instant transitions as the pages are rendered within the client.

We next explore how Static Site Generation (SSG) works.

Static Site Generation (SSG)

Both SSR and CSR are responsible for delivering dynamic content. Their main difference is when the request happens and which component is responsible for rendering the response. What about pages that do not need to change and could benefit from building them once and serve using a CDN?

This is what Static Side Generation (SSG) does essentially. It collects all the static pages ahead of time creates static HTML files for each one of them. When a user visits a page, the server will just serve the file as it is. No dynamic requests happening there.

Faust.js leverages Next.js Automatic Static Site Generation. By default when you run the production build via npm run build it will create static pages for your pages whenever possible:

Next build SSG files

Each page generated will be served by the Next.js server which means that you still need an application server to run these. You also have the option to generate truly static HTML files when running the npm run export command.

Next static HTML export

SSG pages are served very fast, are very secure , it does not penalise their SEO score and you can leverage CDNs to literally host them for free.

However their main caveat is that the more pages it creates, the longer it takes to rebuild the whole site. If you have a WordPress site that does not change very often, it pays off to just statically export it into a Headless site and serve it with unlimited bandwidth and ultra fast speed without incurring any extra costs other than the WordPress hosting.

Incremental Static Regeneration (ISR)

Incremental Static Regeneration (ISR is a hybrid approach between SSG pages (serving static content) and SSR pages (serving dynamic content). It takes the ideas of those rendering patterns and tries to combine them together.

The way it works is like this:

  1. It builds pages using SSG.
  2. It adds a timer to revalidated certain pre-build pages instead of re-building the whole site.
  3. Once a timer is elapsed, then the next visit to that page will trigger a re-build for that page. The user will be served with an updated content just like SSR. The difference is that it will only revalidate after a certain amount of time.

With Faust.js you get automatic ISR support whenever you use the getNextStaticProps function:

export async function getStaticProps(context: GetStaticPropsContext) {
  return getNextStaticProps(context, {
    Page: MyPage,
    // Next.js will attempt to re-generate the page:
    // - When a request comes in
    // - At most once every 900 seconds
    client,
  });
}    

By default it sets the timer for 900 seconds (15 minute) and when a request comes in at most once during that time it will be served with an updated content. If you want to test it quickly yourself, try using a smaller value like 10 seconds and try to update a post in the WP Admin. You will see that when visiting the page after that 10 seconds have passed, you will see the new changes.

ISR is great if you want to be able to update content on a website but still enjoy the benefits of SSG. However it’s not great if you want your users to view updated data all the time without any delays.

Conclusion

Headless WordPress technologies allow for many rendering patterns each with different pros and cons. With Faust.js, developers can adopt a trusted framework for developing extremely flexible and scalable headless sites powered by WordPress. This article serves as a reference point regarding headless WordPress rendering patterns. Thanks for reading!