Reduce static page size with smart Next.js components

At GRRR, we're delivering a system where the client can build pages with stacked blocks. Look at it like Legos. You can use them wherever you like. As developers we would call these blocks components.

The output of the API is used to generate a static website with Next.js. Read more about why we choose to create static websites within this blogpost.


The data for each block is coming from the CMS. Like a title and a bit of text.

// Example data
{
    blockName: "text",
    title: "Volgens ons kan de wereld schoner, eerlijker en kleurrijker.",
    text: "<p>Daarom combineren we de kracht van strategie, design en tech met Zinnige Zaken. Zo ruimen we ......"
},
Screenshot of a part of a website, showing a large title and three paragraphs of text

Possible output of the data shown above.

There are also blocks where we need a bit more data. Like “Show the latest 3 blog posts from a given subject”.

// Example data
{
    blockName: "latest-posts-from-subject",
    tag: "technical"
},
Screenshot of a part of a website, showing a large title and two related blog posts

Possible output of the data shown above.

The client can select a subject (like technical), but you don’t want the client to update this block every time there’s a new article out on this subject. So we need some more data here. But how?

Possible Solutions

We could just fetch this data from the client. There’s an API, so we could do it. But we don’t want to. Most pages are information-dense pages for Marketing websites. Not platforms with always changing states. So it would be overkill to load all of this data client-side. And don’t forget that bots are still better at parsing simple static pages.

Another solution could be to make all of the data available for the entire app. We tried this. And it works, but there’s a huge downside. With a medium-large website, your HTML files will become multiple MB’s in size. So don’t try that at home!

Screenshot of a index.html file with the information that the file-size is 5.1 MB

A huge HTML file!

A ServerSide solution

With all of those options out of the way, what’s left? The answer is the Server. We’re a big fan of statically generated websites, so technically it’s within a build step on the server.

Our solution to this problem is the prepare function. This is a function where you can get additional data and provide this data to a component as its props. This is great for our issue in that the client can place any block wherever they like. Therefore, this prepare function is connected to the component.

This function is executed on the server, so there’s no need for client-side fetching or overloading a site with data.

Can you use it?

The basic implementation is quite simple, but there’s a requirement from the API’s output. Because this code should run on the server, we want to execute this code somewhere within the pages folder. For our statically generated sites, its location is within the getStaticProps function.

Within this function, we’ve received an array with blocks from the API. Each block has a block name so we can render the correct block for each item within this array.

const data = [
	{
        blockName: "text",
        title: "Volgens ons kan de wereld schoner, eerlijker en kleurrijker.",
        text: "<p>Daarom combineren we de kracht van strategie, design en tech met Zinnige Zaken. Zo ruimen we ......"
    },
	{
        blockName: "latest-posts-from-subject",
        tag: "technical"
    },
	... And so it goes on.
]

Each component in our codebase has the option to be connected to a prepare function. Within the getStaticProps function, we’re looping over this array of blocks and checking if any of these corresponding components have a prepare function connected to them. If so, this function is executed. The output of this function is sent as props to the component.

The component is later on rendered somewhere within JSX with that additional data.

Technical implementation

Let’s take a look at the implementation!

Each component has a folder with the files

  • interface.ts -> The component’s TypeScript interface

  • styles.module.scss -> Scoped CSS

  • index.tsx -> The React component

    If you want to have a prepare function for this component, the file prepare.ts is added to this list. The export statement of the component is also changed (you will see why later on)

// index.tsx -> A default component

export default function ExampleComponent({ title, tag }: ComponentInterface) {
    return (
        <section>
            <h2>{title}</h2>
            // articles aren't available without the prepare function.
            <ul>
                {articles.map((article) => (
                    <li key={article.id}>
                        <ArticlePreview article={article} />
                    </li>
                ))}
            </ul>
        </section>
    );
}

to

// index.tsx -> A component with a prepare function

// Import the util
import withPrepare from "with-prepare";

// Import the matching prepare function
import prepare from "./prepare";

function ExampleComponent({
    title,
    tag,
    articles,
}: PreparedComponentInterface) {
    return (
        <section>
            <h2>{title}</h2>
            <ul>
                {articles.map((article) => (
                    <li key={article.id}>
                        <ArticlePreview article={article} />
                    </li>
                ))}
            </ul>
        </section>
    );
}

// Export the component like this
export default withPrepare<ComponentInterface, PreparedComponentInterface>(
    prepare
)(ExampleComponent);

The withPrepare util function will return an object with the shape:

{
	component: Component,
	prepare: prepare
}

This creates the possibility to execute the correct prepare function within the getStaticProps function and also render the correct Component later on within the JSX.

A prepare function would look something like this:

// prepare.ts

import type ComponentInterface from "./interface";
import type { PreparedComponentInterface } from "./interface";
import type { Page } from "../../../content/pages-interfaces";
import type { JsonApiObject } from "../../../content/api-interfaces";

// Function that's executed within the getStaticProps function
export default function prepare(
    // The initial props from the component (like the title)
    initialProps: ComponentInterface,
    // The data from the page it's on (we need this sometimes)
    pageData: Page
): PreparedComponentInterface {
    // Get the latest articles from the tag, no need to dive into this code here.
    const articles = getArticlesFromTag(initialProps.tag);

    return {
        // Spread the initialProps so they stay available
        ...props,
        // Add the articles to the props of the component
        articles,
    };
}

And within the getStaticProps function, we will execute every prepare function for the page data:

// getStaticProps.ts

export const getStaticProps: GetStaticProps = async (context) => {
    // Get the data from this specific page
    const pageData = getPageData(context);

    // Enhance the page data
    const fullPageData = await createFullPageData(page);

    // Return the enhanced page data to the page
    return {
        props: {
            pageData: fullPageData,
        },
    };
};

Where the createFullPageData function will do something like this

// createFullPageData.ts (a simplified version of it)

// The function is async so we can make some API calls here
export default async function createFullPageData(page: Page) {
    const content = await Promise.all(
        page.data.map(async (componentProps) => {
            // Get the component from a setup list with items like (text: Text)
            const Component = blockToComponent[componentProps.blockName];

            if ("prepare" in Component) {
                // Execute the prepare function
                const propsAfterPrepare = await Component.prepare(
                    componentProps,
                    pageData
                );

                // Return the enhanced props
                return propsAfterPrepare;
            }

            // Return the initial props if there isn't a prepare function for this component
            return componentProps;
        })
    );

    return content;
}

Now, the data for each component within the page-data is enhanced with the data from the prepare function. When those components are rendered somewhere in the JSX, the props are the output of the prepare functions.

Reacts Server Components

With Next.js 13 and React 18 there’s a new thing called Server Components. This is another solution to the problem described in this article. The sweet thing about Server Components is the mental model. it’s simpler than the mental modal of our prepare function. Therefore, we’re looking into this right now. It could be a nice replacement for our solution.

If we’re done looking, we will come back with a blog post about it. We’re curious if this will replace our prepare function or if there’s still a use case for it.