Building a Blog in TanStack (Part 2 of 2)

Adam Rackis Adam Rackis on

We left off this series with a functional blog in TanStack Start. We set up Shiki for code syntax highlighting and created server functions to inspect the file system, discover blog posts (in Markdown files), and build the final blog pages for all posts.

Article Series

But we’ve still got work to do.

Performance Issues

It turns out that the Shiki setup, which lives in the top-level async function getMarkdownIt() method takes no small amount of time to set up. It’s not that the function is slow to call; it’s quite fast. But the initial parsing of this module is extremely slow. On my own modern MacBook Pro, it takes about 2 seconds (2000ms) to parse. This is because a lot of WASM is being loaded, which is what powers the parsing and formatting of any code we pass in.

This means that when your web server first spins up and processes the import graph, this particular function will block the process for about two seconds. You might think, for a blog, this is an unimportant cost: it’s just spin-up time, after all, which happens only once.

Cold Starts

Or does it? What if you deploy this site to Netlify, Vercel, or any other serverless platform, like AWS Lambda. With that runtime model, cloud functions will constantly be spinning up, to process requests. This spin-up time is called a “cold start,” and is a well-known issue with Serverless. Usually, cold start times are reasonable, and modern platforms like Netlify and Vercel will “pre-warm” serverless functions to minimize this cost from happening at all.

Going Static

Rather than debate the importance of minimizing cold starts for a blog that likely has few readers, let’s take a step back: do we even need a server? Blogs are inherently static. Any modern web framework provides a way to pre-render content statically: this is exactly what we need. Why not just pre-render our blog pages so we can render them anywhere without any server processing? We could even just toss the built static assets onto a CDN.

Pre-Rendering Pages

To start, let’s get into the vite.config.ts file and add a setting to the TanStack plugin:

tanstackStart({
  prerender: {
    enabled: true,
  },
}),Code language: TypeScript (typescript)

This enables pre-rendering. Now, during the build process, TanStack will crawl routes and the links within them. So it will start with our home / route, build the page with all our blog posts, and then find each <Link> tag and crawl those. If those pages had links, they’d be crawled as well.

When we run our build, we can see this in action during the build process:

We can look at the output of this build by peeking inside the .output folder, which is where the Nitro plugin (the default deployment adapter) creates our output:

The /public folder will contain everything that can be routed to directly. Our index.html is in there, as well as paths to both blogs, and the images from blog post-2.

Running Content as a Static Website

Uploading these files to an S3 bucket would be a bit over the top for this post. To test true static rendering more simply, I’ve put together these two scripts on this post’s repo.

"generate-static-site": "npm run build && rm -rf static-site && mkdir -p static-site && cp -r .output/. static-site",
"start-static-server": "npx tsx static-server.ts"Code language: JavaScript (javascript)

This is a script to build and copy the contents to a folder called static-site. Then another to run static-server.ts, which looks like this, in its entirety.

import express from "express";
import path from "path";
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const app = express();
const PORT = 3003;

// Serve static files from the static-site directory
app.use(express.static(path.join(__dirname, "static-site/public")));

app.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});Code language: TypeScript (typescript)

This script fires up an Express web server and points the static middleware at /public inside the static-site folder we just copied our build output into.

When we run this app, all of our pages work when we browse directly to them.

index page
blog post page

These pages work if we navigate directly to them in our browser’s URL bar. But if we click around in our app, these pages fail.

Looking in the network tab makes this even clearer.

As we navigate, our server function is being called. Didn’t we pre-render these pages?

How TanStack Start Does Pre-Rendering

Our pre-rendered HTML file is indeed rendered by our Express server. But when it is, script tags containing the normal TanStack app will spin up and take over. At the end of the day, TanStack Start generates the same kind of application either way, except that, in this case, the initial render is served from a pre-generated HTML file rather than being server-rendered.

From there on, Link tags trigger normal client-side loading, which triggers the server functions, as usual.

TanStack Start does not try to morph itself into a full MPA framework just to handle static web apps. Instead, it gives you the primitives to achieve this yourself.

We already saw the first, which was static pre-rendering. Now let’s look at the other.

Static Server Functions

Our client navigation will run either way, but the real problem is our server functions. To solve this, TanStack provides static middleware that we can apply to server functions. This causes our server functions, during the build, to record invocations and results, then save those payloads to simple JSON files in the build output.

Let’s try it! Install it:

npm i @tanstack/start-static-server-functionsCode language: Bash (bash)

Then import it:

import { staticFunctionMiddleware } from "@tanstack/start-static-server-functions";Code language: JavaScript (javascript)

Then apply it to our server functions:

export const getPostContent = createServerFn()
  .inputValidator((data: { slug: string }) => data)
  .middleware([staticFunctionMiddleware])
  .handler(async ({ data }) => {Code language: TypeScript (typescript)

Now, when we run our build, we see something new in there.

The __tsr folder naming refers to TanStack Router, and the staticServerFnCache contains the 3 server function calls from parsing our blog: one for the index page, and one each for the two posts we have.

The plugin recorded those invocations and results, and more importantly, replaced those call sites with fetches to the JSON files in the __tsr folder.

If we run our blog again, we can navigate and see much simpler fetches to those JSON files.

Concluding Thoughts

TanStack Start is a superb framework. It’s better known for features like strong static typing and flexible data loading. But as we saw here, it also offers creative ways to support static generation.

Article Series

Wanna take your React skills to the next level?

Leave a Reply

Your email address will not be published. Required fields are marked *

$966,000

Frontend Masters donates to open source projects through thanks.dev and Open Collective, as well as donates to non-profits like The Last Mile, Annie Canons, and Vets Who Code.