Alex

Alex Monachino

Software Developer

Email
Astro logo

Pre-Rendering
+ Re-Rendering

Serve stakeholder needs by developing a cost-effective, performant, iterable website

What is it?

It is a concept for reusing code, to render an interface at compile-time as well as at run-time.

compile-time
run-time
server-side
client-side
pre-render
re-render

Why should I
care about it?

Reusing code in this way allows a website team to serve stakeholder needs; it enables the development and maintenance of a cost-effective, performant, and iterable website.

Serving stakeholders

Development leads / managers have a few stakeholders to serve. Three important stakeholders are usually:

  • the business that you are working for
  • potential customers/users
  • a content creation/management team

Generally, a business will need to see the lowest cost for website development and maintenance.

Generally, a potential customer/user will need a website to load quickly on their device.

Generally, a content team will need an iterable website, that supports frequent content updates.

Over time, techniques have been created for prioritizing low costs, fast loading, or fast iteration. These techniques serve only one or two of the described needs, and cannot serve all of them.

Pre-Rendering + Re-Rendering is a concept that enables the prioritization of low costs, fast loading, and fast iteration; in order to serve all of the described needs.

compile-time
run-time
server-side
client-side
potential costs
potential loading time
potential iteration / rendering

Context

Website development techniques focus on the function of a host server (server-side) and the function of a web browser (client-side).

Static Site Generation (SSG) and Server-Side Rendering (SSR) are common techniques for server-side function. Single-Page App (SPA) and Multi-Page App (MPA) are common techniques for client-side function.

When considering both server-side and client-side function, these techniques can be combined to create website configurations.

In terms of cost, loading speed, and iteration speed, each configuration has its benefits and drawbacks.

Static Site Generation
& Single-Page App

  • Costs: Low for compile and serving
  • Loading: Fast from the server-side, slow on the client-side
  • Iteration: Fast; from client-side rendering
  • Conclusion: Slow loading on the client-side is bad for users
compile-time
run-time
server-side
client-side
low cost
low cost
fast loading
slow loading
fast iteration

Static Site Generation
& Multi-Page App

  • Costs: Low for compile and serving
  • Loading: Fast from the server-side, fast on the client-side
  • Iteration: Slow; rendering only happens at compile-time
  • Conclusion: Slow iteration is bad for a content team
compile-time
run-time
server-side
client-side
low cost
low cost
fast loading
fast loading
slow iteration

Server-Side Rendering
& Single-Page App

  • Costs: High for serving
  • Loading: Slow from the server-side, slow on the client-side
  • Iteration: Fast; from server-side or client-side rendering
  • Conclusion: High costs are bad for business, and slow loading is bad for users
compile-time
run-time
server-side
client-side
high cost
slow loading
slow loading
fast iteration
fast iteration

Server-Side Rendering
& Multi-Page App

  • Costs: High for serving
  • Loading: Slow from the server-side, fast on the client-side
  • Iteration: Fast; from server-side rendering
  • Conclusion: High costs are bad for business, and slow loading on the server-side is bad for users
compile-time
run-time
server-side
client-side
high cost
slow loading
fast loading
fast iteration

Drawback remedy

In order to serve the needs of a business, of users, and of a content team, configurations can be modified to remove their drawbacks.

The Static Site Generation and Multi-Page App configuration includes low costs and fast loading, but does not include fast iteration for content updates.

A remedy for this drawback is called Client Re-Rendering (CRR).

Client Re-Rendering is a client-side technique for optionally re-rendering an interface. It allows a static website to display content updates without re-compiling its entire website bundle, and without on-demand server rendering costs.

Combining the Static Site Generation, Multi-Page App, and Client Re-Rendering techniques allows for the implementation of the Pre-Rendering + Re-Rendering concept, and for serving all stakeholders.

Interfaces are pre-rendered with Static Site Generation (compile-time), and are re-rendered, when needed, with Client Re-Rendering (run-time, on the client-side).

Static Site Generation
& Multi-Page App
& Client Re-Rendering

  • Costs: Low for compile and serving
  • Loading: Fast from the server-side, fast on the client-side
  • Iteration: Fast; from client-side rendering
  • Conclusion: Good for business, users, and a content team
compile-time
run-time
server-side
client-side
low cost
low cost
fast loading
fast loading
slow iteration
fast iteration

How do I
implement it?

The core requirement is to render an interface at compile-time and at run-time (on the client-side), using the same code.

Astro is a website framework that enables the rendering of HTML, CSS, and Javascript at compile-time. And, React is a website framework that enables the rendering of HTML, CSS, and Javascript at run-time, on the client-side.

Combining Astro and React enables the reusability of HTML, CSS, and Javascript. This reusability allows a component, or an entire page, to both pre-render (compile-time) and re-render (run-time, on the client-side).

Project setup

Starting a new Astro project, with React integrated, is quick and straight-forward.

First, you’ll want to make sure that node is installed. If you already have it installed, feel free to skip this step.

Open a new Terminal window, and run the following command to install nvm (node version manager):

Terminal

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash;

Close the Terminal window, and then open a new one, in order to start using nvm. In the newly-opened Terminal window, run the following command to install node:

Terminal

nvm install node;

Again, close the Terminal window, and then open a new one, in order to start using node.

Now that node is installed, an Astro project can be created. In the newly-opened Terminal window, run the following command to create your Astro project:

Terminal

npm create astro@latest;

A helpful Terminal interface is included, to walk you through the setup of a new project. Your project directory, a project template, dependency installation, Typescript support, and Git initialization can be configured. A setup for this article can look like the following:

  • Directory — Documents/Websites/astro-project
  • Template — Empty
  • Dependencies — Yes
  • Typescript — Yes/No (either answer works)
  • Git — Yes/No (either answer works)

Now that a new Astro project has been created, you can navigate to its directory to start your work.

Terminal

cd Documents/Websites/astro-project;

In order to add React to the project, run the following command:

Terminal

npx astro add react;

And, you can start Astro’s local development server with the following command:

Terminal

npm run dev;

Once started, you can open a web browser and navigate to http://localhost:3000/, in order to view your website.

And that’s it; your Astro project has been started, and it’s ready for the implementation of Pre-Rendering + Re-Rendering.

Pre-rendering
implementation

The primary file to consider is index.astro, because it is used to render the home page of an Astro website.

Astro

---
// components
import Feature from '../components/Feature/Feature.jsx';
// utilities
import { content } from '../utilities/content';
// set alias, for fetching this page's content
const alias = 'index';
// fetch data
const data = await content(alias);
---
<!doctype html>
<html lang='en-US'>
	<head>
		<meta charset='utf-8'>
		<title>{data.title}</title>
	</head>
	<body>
		{/* for each component... */}
		{data.components.map((component, index) => {
			{/* 'feature' design */}
			if (component.design === 'feature')
				return <Feature compileTime={data} alias={alias} index={index} client:load />;
			{/* ... */}
		})}
	</body>
</html>

The above code represents content being fetched for the page, and then a React component being rendered based on that content.

On line 9, page content is fetched using the imported content function, as well as the defined alias. The result is a JSON object.

HTML tags are added from lines 11 to 26, in order to define a standard web page.

On line 19, the components array, within the fetched page content, is mapped. The array map replaces each item with a React component, by its design name, and is rendered as HTML. Each React component is passed the page’s content, the page’s alias, and its index in the array of components.

This defines the compile-time rendering, or pre-rendering, of a page in a Pre-Rendering + Re-Rendering website.

Javascript

let cache = {};
let requested = {};
	
const url = alias =>
	`${origin()}/${alias}.json`;
	
const json = async url =>
	await fetch(url).then(a => a.json());
	
const wait = time =>
	new Promise(a => setTimeout(a, time));
	
const origin = () =>
	(import.meta.env.DEV)
	? 'http://localhost:3000'
	: 'https://alexandermonachino.com/pre-rendering-re-rendering';
	
export const content = async alias => {
	// if content for this alias has not yet been requested...
	if (!requested[alias]) {
		// mark this alias as requested
		requested[alias] = true;
		// fetch content for this alias, and add to cache
		cache[alias] = await json(url(alias));
		// return cached content
		return cache[alias];
	}
	// otherwise, if content for this alias has not yet been cached...
	else if (!cache[alias])
		// wait 100ms, then retry this function
		return wait(100).then(() => content(alias));
	// otherwise, return cached content
	else return cache[alias];
};

The content.js utility is written to fetch, wait for, cache, and return page content.

There is a simple three-condition logic within the content function.

If content for a given alias has not yet been requested, then it is marked as requested, fetched, added to the cache, and returned.

If content for a given alias has been requested, but it is not yet available in the cache, then the function is called again after waiting 100 milliseconds, and the result is returned.

If content for a given alias has been requested and is available in the cache, then it is returned.

The ability to wait until cache is available allows multiple components to reference this utility at the same time, with the same given alias, without fetching the same content more than once. This comes in handy for the implementation of re-rendering, when a page includes multiple components.

Re-rendering
implementation

Unlike Astro files, which only render at compile-time, React files render at both compile-time and run-time (on the client-side).

React

import React, { useState, useEffect } from 'react';
// styles
import * as styles from './styles.module.css';
// utilities
import { content } from '../../utilities/content';
	
const Feature = ({ compileTime, alias, index }) => {
	// set component data, using passed-in props
	const [data, setData] = useState(compileTime.components[index]);
	// on page load...
	useEffect(() => {
		// re-fetch content, then...
		content(alias).then(runTime => {
			// if run-time data is different than compile-time data...
			if (runTime['last-updated'] !== compileTime['last-updated'])
				// update data
				setData(runTime.components[index]);
		});
	}, []);
	// render component
	return (
		<section className={styles.Feature}>
			{/* title */}
			{data.title &&
				<h1>{data.title}</h1>}
			{/* text(s) */}
			{data.text && data.text.map((text,i) =>
				<p key={i}>{text}</p>)}
			{/* link(s) */}
			{data.links && data.links.map((link,i) =>
				<a key={i} href={link.uri}>{link.label}</a>)}
		</section>
	);
};
	
export default Feature;

Using passed-in props, React components can re-fetch page content at run-time, overwrite what was fetched at compile-time, and re-render their HTML.

At compile-time, the Feature component uses its passed-in compile prop to populate content. For use at run-time, the compile and index props are used, along with React’s useState hook, to define the data state variable.

At run-time, page content is re-fetched, using the passed-in alias prop and React’s useEffect hook. The last-updated property is then used to compare the compile-time and the re-fetched (run-time) content. If the property values are different, then the data state variable is updated with the re-fetched content.

On state-change, the return statement of a React component will re-render. This defines the run-time rendering, or re-rendering, of a component in a Pre-Rendering + Re-Rendering website.

JSON

{
	"last-updated": "2023-05-08T00:00:00.000Z",
	"title": "Apple",
	"components": [
		{
			"title": "iPhone 14 Pro",
			"text": [
				"Pro. Beyond."
			],
			"links": [
				{
					"label": "Learn more",
					"uri": "/learn_more/iphone_14_pro"
				},
				{
					"label": "Buy",
					"uri": "/buy/iphone_14_pro"
				}
			],
			"design": "feature"
		}
	]
}

Page content is structured as a JSON object, that includes an array of components. Each component takes the shape of a generic content structure, in order to enable team synchronization and the reusability of code. For more on this concept, see:

To quickly update a component’s content, simply update its page’s JSON file. As long as the last-updated property is different than it was at compile-time, the updated content will be used.

Deployment

To compile the distributable version of your Astro website, run the following command:

Terminal

npm run build;

The generated static site can be found in the /dist directory, inside of your project directory. The contents of /dist can be placed on your host server.

The content that was available at compile-time will show when viewing the site. If the content is adjusted after compile-time, those adjustments will become available without an additional compile, because components can re-render at run-time, on the client-side.

compile-time
run-time
server-side
client-side
low cost
low cost
fast loading
fast loading
static site 2023-05-08
static site 2023-05-08
static site 2023-05-08
content 2023-05-08
content 2023-05-08
content 2023-05-08
It’s
Monday
It’s
Monday
It’s
Tuesday

Afterword

By prioritizing low costs, fast loading, and fast iteration, a website team can serve the needs of a business, of potential customers/users, and of a content team.

Development leads / managers can benefit from this in scenarios where business expenses are under scrutiny, customer experience is of high importance, and website content is ever-changing.

Try it out, using the starter repository on GitHub.

Repository

Any questions?

Feel free to contact me

Email

Didn’t like this article?

Wow. Well maybe check out the others

Back