Skip to main content
Version: 3.x

Prerender

Prerendering is great for providing a faster experience for your end users. Your pages will be rendered at build-time, saving your user's browser from having to do that job.

We thought a lot about what the developer experience should be for route-based prerendering. The result is one of the smallest APIs imaginable!

How's Prerendering different from SSR/SSG/SWR/ISSG/...?

As Danny said in his Prerender demo at our Community Meetup, the thing all of these have in common is that they render your markup in a Node.js context to produce HTML. The difference is when (build or runtime) and how often.

Redwood currently supports prerendering at build time. So before your deploy your web side, Redwood will render your pages into HTML, and once the JavaScript has been loaded on the browser, the page becomes dynamic.

Prerendering a Page

Prerendering a page is as easy as it gets. Just add the prerender prop to the Route that you want to prerender:

Routes.js
<Route path="/" page={HomePage} name="home" prerender/>

Then run yarn rw build and enjoy the performance boost!

Prerendering all pages in a Set

Just add the prerender prop to the Set that wraps all Pages you want to prerender:

Routes.js
<Set prerender>
<Route path="/" page={HomePage} name="home" />
<Route path="/about" page={AboutPage} name="hello" />
</Set>

Not found page

You can also prerender your not found page (a.k.a your 404 page). Just add—you guessed it—the prerender prop:

-      <Route notfound page={NotFoundPage} />
+ <Route notfound page={NotFoundPage} prerender/>

This will prerender your NotFoundPage to 404.html in your dist folder. Note that there's no need to specify a path.

Private Routes

For Private Routes, Redwood prerenders your Private Routes' whileLoadingAuth prop:

<Private >
// Loading is shown while we're checking to see if the user's logged in
<Route path="/super-secret-admin-dashboard" page={SuperSecretAdminDashboard} name="ssad" whileLoadingAuth={() => <Loading />} prerender/>
</Private>

Rendering skeletons while authenticating

Sometimes you want to render the shell of the page, while you wait for your authentication checks to happen. This can make the experience feel a lot snappier to the user, since they don't wait on a blank screen while their credentials are checked.

To do this, make use of the whileLoadingAuth prop on <Private> or a <Set private> in your Routes file. For example, if we have a dashboard that you need to be logged in to access:

// This renders the layout with skeleton loaders in the content area
const DashboardLoader = () => <DashboardLayout skeleton />


const Routes = () => {
return (
<Router>
<Route path="/" page={HomePage} name="home" prerender />
<Set
private
wrap={DashboardLayout}
unauthenticated="login"
// 👇 tell the router to render the shell until the user has been authenticated
whileLoadingAuth={DashboardLoader}
prerender
>
<Route path="/dashboard" page={DashboardPage} name="dashboard"/>
{/* ... */}

Dynamic routes & Route Hooks

Let's say you have a route like this

<Route path="/blog-post/{id}" page={BlogPostPage} name="blogPost" prerender />

To be able to prerender this route you need to let Redwood know what ids to use. Why? Because when we are prerendering your pages - at build time - we don't know the full URL i.e. site.com/blog-post/1 vs site.com/blog-post/3. It's up to you to decide whether you want to prerender all of the ids, or if there are too many to do that, if you want to only prerender the most popular or most likely ones.

You do this by creating a BlogPostPage.routeHooks.js file next to the page file itself (so next to BlogPostPage.js in this case). It should export a function called routeParameters that returns an array of objects that specify the route parameters that should be used for prerendering.

So for example, for the route /blogPost/{Id:Int} - you would return [ {id: 55}, {id: 77} ] which would tell Redwood to prerender /blogPost/55 and /blogPost/77

A single Page component can be used for different routes too! Metadata about the current route will be passed as an argument to routeParameters so you can return different route parameters depending on what route it is, if you need to. An example will hopefully make all this clearer.

For the example route above, all you need is this:

BlogPostPage.routeHooks.js
export function routeParameters() {
return [{ id: 1 }, { id: 2 }, { id: 3 }]
}

Or, if you wanted to get fancy

BlogPostPage.routeHooks.js
export function routeParameters(route) {

// If we are reusing the BlogPostPage in multiple routes, e.g. /odd/{id} and
// /blogPost/{id} we can choose what parameters to pass to each route during
// prerendering
if (route.name === 'odd') {
return [{ id: 1 }, { id: 3 }, { id: 5 }]
} else {
return [{ id: 2 }, { id: 4 }, { id: 6 }]
}
}

With the config above three separate pages will be written: web/dist/blog-post/1.html, web/dist/blog-post/2.html, web/dist/blog-post/3.html. A word of warning - if it's just a few pages like this, it's no problem - but this can easily and quickly explode to thousands of pages, which could slow down your builds and deployments significantly (and make them costly, depending on how you're billed).

In these routeHooks scripts you have full access to your database using prisma and all your services, should you need it. You use import { db } from '$api/src/lib/db' to get access to the db object.

BlogPostPage.routeHooks.js
import { db } from '$api/src/lib/db'

export async function routeParameters() {
return (await db.post.findMany({ take: 7 })).map((post) => ({ id: post.id }))
}

Take note of the special syntax for the import, with a dollar-sign in front of api. This lets our tooling (typescript and babel) know that you want to break out of the web side the page is in to access code on the api side. This only works in the routeHook scripts (and scripts in the root /scripts directory).


Prerender Utils

Sometimes you need more fine-grained control over whether something gets prerendered. This may be because the component or library you're using needs access to browser APIs like window or localStorage. Redwood has three utils to help you handle these situations:

  • <BrowserOnly>
  • useIsBrowser
  • isBrowser
Heads-up!

If you're prerendering a page that uses a third-party library, make sure it's "universal". If it's not, try calling the library after doing a browser check using one of the utils above.

Look for these key words when choosing a library: universal module, SSR compatible, server compatibleall these indicate that the library also works in Node.js.

<BrowserOnly/> component

This higher-order component is great for JSX:

import { BrowserOnly } from '@redwoodjs/prerender/browserUtils'

const MyFancyComponent = () => {
<h2>👋🏾 I render on both the server and the browser</h2>
<BrowserOnly>
<h2>🙋‍♀️ I only render on the browser</h2>
</BrowserOnly>
}

useIsBrowser hook

If you prefer hooks, you can use the useIsBrowser hook:

import { useIsBrowser } from '@redwoodjs/prerender/browserUtils'

const MySpecialComponent = () => {
const browser = useIsBrowser()

return (
<div className="my-4 p-5 rounded-lg border-gray-200 border">
<h1 className="text-xl font-bold">Render info:</h1>

{browser ? <h2 className="text-green-500">Browser</h2> : <h2 className="text-red-500">Prerendered</h2>}
</div>
)
}

isBrowser boolean

If you need to guard against prerendering outside React, you can use the isBrowser boolean. This is especially handy when running initializing code that only works in the browser:

import { isBrowser } from '@redwoodjs/prerender/browserUtils'

if (isBrowser) {
netlifyIdentity.init()
}

Debugging

If you just want to debug your app, or check for possible prerendering errors, after you've built it, you can run this command:

yarn rw prerender --dry-run

We're actively looking for feedback! Do let us know if: everything built ok? you encountered specific libraries that you were using that didn’t work?


Images and Assets

Images and assets continue to work the way they used to. For more, see this doc.

Note that there's a subtlety in how SVGs are handled. Importing an SVG and using it in a component works great:

import logo from './my-logo.svg'

function Header() {
return <logo />
}

But re-exporting the SVG as a component requires a small change:

// ❌ due to how Redwood handles SVGs, this syntax isn't supported.
import Logo from './Logo.svg'
export default Logo
// ✅ use this instead.
import Logo from './Logo.svg'

const LogoComponent = () => <Logo />

export default LogoComponent

Cell prerendering

As of v3.x, Redwood supports prerendering your Cells with the data you were querying. There's no special config to do here, but a couple of things to note:

1. Prerendering always happens as an unauthenticated user

Because prerendering happens at build time, before any authentication is set, all your queries on a Route marked for prerender will be made as a public user

2. We use your graphql handler to make queries during prerendering

When prerendering we look for your graphql function defined in ./api/src/functions/graphql.{ts,js} and use it to run queries against it.

Common Warnings & Errors

Could not load your GraphQL handler - the Loading fallback

During builds if you encounter this warning

  ⚠️  Could not load your GraphQL handler.
Your Cells have been prerendered in the "Loading" state.

It could mean one of two things:

a) We couldn't locate the GraphQL handler at the usual path

or

b) There was an error when trying to import your GraphQL handler - maybe due to missing dependencies or an error in the code

If you've moved this GraphQL function, or we encounter an error executing it, it won't break your builds. All your Cells will be prerendered in their Loading state, and will update once the JavaScript loads on the browser. This is effectively skipping prerendering your Cells, but they'll still work!

Cannot prerender the query {queryName} as it requires auth.

This error happens during builds when you have a Cell on a page you're prerendering that makes a query marked with @requireAuth in your SDL.

During prerender you are not logged in (see point 1), so you'll have to conditionally render the Cell - for example:

import { useAuth } from '@redwoodjs/auth'

const HomePage = () => {
const { isAuthenticated } = useAuth

return (
<>
{ isAuthenticated ? <MyPrivateCell /> : <NoAccess /> }
</>

Optimization Tips

Dynamically loading large libraries

If you dynamically load third-party libraries that aren't part of your JS bundle, using these prerendering utils can help you avoid loading them at build time:

import { useIsBrowser } from '@redwoodjs/prerender/browserUtils'

const ComponentUsingAnExternalLibrary = () => {
const browser = useIsBrowser()

// if `browser` evaluates to false, this won't be included
if (browser) {
loadMyLargeExternalLibrary()
}

return (
// ...
)

Configuring redirects

Depending on what pages you're prerendering, you may want to change your redirect settings. Keep in mind your redirect settings will vary a lot based on what routes you are prerendering, and the settings of your deployment provider.

Using Netlify as an example:

If you prerender your `notFoundPage`, and all your other routes

You can remove the default redirect to index in your netlify.toml. This means the browser will accurately receive 404 statuses when navigating to a route that doesn't exist:

[[redirects]]
- from = "/*"
- to = "/index.html"
- status = 200

This makes your app behave much more like a traditional website, where all the possible routes are defined up front. But take care to make sure you are prerendering all your pages, otherwise you will receive 404s on pages that do exist, but that Netlify hasn't been told about.

If you don't prerender your 404s, but prerender all your other pages
You can add a 404 redirect if you want:
[[redirects]]
from = "/*"
to = "/index.html"
- status = 200
+ status = 404

This makes your app behave much more like a traditional website, where all the possible routes are defined up front. But take care to make sure you are prerendering all your pages, otherwise you will receive 404s on pages that do exist, but that Netlify hasn't been told about.

Flash after page load

You might notice a flash after page load. Prerendering pages still has various benefits (such as SEO), but may seem jarring to users if there's a flash.

A quick workaround for this is to make sure whatever page you're seeing the flash on isn't dynamically loaded i.e. prevent code splitting. You can do this by explicitly importing the page in Routes.js:

import { Router, Route } from '@redwoodjs/router'
// We don't want HomePage to be dynamically loaded
import HomePage from 'src/pages/HomePage'

const Routes = () => {
return (
<Router>
<Route path="/" page={HomePage} name="hello" prerender />
<Route path="/about" page={AboutPage} name="hello" />
<Route notfound page={NotFoundPage} />
</Router>
)
}

export default Routes