Single-Page, Server-Side, Static… say what?

An emoji-filled learning journey about the trade-offs of different website architectures, complete with gifs, diagrams, and demo apps.

If you’ve been hanging around the internet, trying to build websites and apps, you may have heard some words in conversation like static site or server-side rendered (SSR) or single-page app (SPA).

But what do all of these words mean? How does each type of application architecture differ? What are the tradeoffs of each approach and which one should you use when building your website?

Gif of Janelle Monáe in Hidden Figures movie, sitting at a classroom desk with a notebook preparing to take notes with a pencil (source).

The main difference between each of these types of architecture is where rendering decisions are made, or what part of the stack is in charge of deciding what content the user should see when they load your website.

  • A static site serves exact files (like HTML, JavaScript, CSS, images, etc) without any changes. It doesn’t make any rendering decisions!
  • A server-side rendered site handles rendering decisions on the server. The server determines what each page should look like, and what data should fill it, and returns the completed HTML to the browser to display.
  • A single-page app handles rendering decisions in the browser. The server returns the same base HTML page for every URL. In the browser, that page loads JavaScript that will determine what view to show, make API calls for data, and swap out the HTML content as needed.

That seems a little abstract, doesn’t it? Let’s check out some real working examples and dig in!

I wrote a fun little app with Glitch called emojis according to marie. The app lists different emojis and the name I call them in my head. You can click on an emoji link to see more details about it.

Screenshots of “emojis according to marie” app (single-page app version). Displays a table with a list of emojis and titles, and a page describing the 🎉emoji.

I liked this website so much, I wrote it three times! Once as a static site, once as a server-side rendered site, and once as a single-page app.

Let’s say that you go to each of my three app implementations. You first open the index page, then click the 🎉 link on each table because you are curious about the TADA emoji.

These sequence diagrams give a brief overview of how the work is split between the browser and the server in each application!

Side-by-side sequence diagrams of user visiting the same two pages on a static site (plain text description), a server-side rendered site (plain text description), and a single-page app (plain text description).

Don’t understand those sequences? Not sure what all these terms mean anyway? Not to worry!

In the rest of this blog post, we’re going to look at static sites, server-side rendered sites, and single-page apps in greater detail. For each type of application architecture we will:

  • Explain how it works
  • Link to a fully-functioning Glitch app that you can demo or edit yourself
  • Review the source code at a high-level
  • Discuss the advantages and disadvantages (plus possible mitigations!)
  • Suggest use cases that might be best suited for that architecture

Use this resource in whatever way works best for your brain—whether that’s jumping straight to source code and ignoring the rest of this article, skimming the bullet points in each section, or reading the whole thing in one sitting from start to finish. Or bail out now if the sequence diagrams answered your questions!

Gif of NeNe Leakes from the Real Housewives of Atlanta show with a bored expression, waving her hands and saying “Here we go” (source).

Sequence diagram of a static site (plain text description).

Static Sites

A static site is an application that shows the same content to everyone, on every page load. Nothing changes — it’s not dynamic. There’s not much data involved.

In some ways, a static site is like a glorified file finder. Every URL maps directly to an asset, like an HTML page or an image. When a user requests that URL in their browser, the server finds the file that matches the path and returns it, as is.

A blog is a great example of a static site. The content is created by the author once, then uploaded and deployed. If you’ve ever used GitHub Pages, then you’ve written a static site!

Embedded Glitch app showing emojis by marie as a static site.

Let’s take a look at the emojis by marie Glitch app, implemented as a static site.

When we navigate around the live app, we see that there are four possible URLs:

  • index.html, a landing page with a list of emojis
  • about.html, a page with information about the app
  • ✨.html, an information page that describes the ✨ emoji
  • 🎉.html, an information page that describes the 🎉 emoji

When we look at the source code, we find the following files:

- public/
- index.html
- about.html
- ✨.html
- 🎉.html
- server.js

In the public/ directory, there is an HTML file for every page in the app! Pages like ✨.html and 🎉.html, which look very similar, have a lot of duplicate code between them.

The server.js file contains a very simple Express.js server. The most important line is:


That tells the server that whenever a GET request comes in for a given path, to look up that path in the public/ directory and return any matching files.

So if you request /about.html, the server will look for a file named public/about.html and return it! This means that you have to know the exact path you want to request. But it also means that when you’re writing this app, you have to include a file for every single possible page you might want to write.

Gif of actress Phoebe Cates in Gremlins movie, looking distressed and saying “All of them?” (source)

Question: If you’re writing a static site, does that mean you have to hand-write every single page, and copy/paste a bunch of duplicate code? No, not at all!

There are plenty of frameworks that help you generate static sites from templates and dynamic content. For example, GitHub Pages uses Jekyll, a Ruby Gem which lets you write content in Markdown and define reusable layouts. When you are ready to build and deploy your static site, Jekyll runs a script that transforms all your Markdown into the full HTML pages that the static site server can return.

What makes this a static site is that all the templating and dynamic content happens at build time, not runtime. At runtime, the server doesn’t know how the files were generated, and it doesn’t do any work to generate them — it just returns them, exactly the way they are, with no modifications.

Gif of actor Russel Brand in Get Him to the Greek movie, frowning and saying “I don’t like change. I don’t like it when things change.” Actor Jonah Hill stands behind him scratching his head. (source)

Advantages of a Static Site

Simplest and cheapest option! The server isn’t doing a whole lot of work here, just returning files. That means it doesn’t need much memory and it’s pretty cheap to run! There’s plenty of options that let you host static sites for free.

Disadvantages of a Static Site

Can only change content with a deploy. There’s no databases involved with static sites, no way to have a “logged in” state or display different content for different users.

Good Use Cases for a Static Site

  • You want to host your own blog site.
  • You want a separate set of marketing or documentation pages to display to people who aren’t logged into your app.
  • You are building a simple application that doesn’t need any state, or that stores all its state in the browser. (e.g. a calculator website)

Sequence diagram of a server-side rendered site (plain text description).

Server-Side Rendered Sites

A server-side rendered site is an application that renders dynamic content to users in the backend.

In some ways, the output of a server-side rendered site will look very similar to a static site — everything you need to render the page will be included inline in the HTML. However, the server is deciding at runtime how to build the HTML file and what content to include.

Embedded Glitch app showing emojis by marie as a server-side rendered app.

Let’s take a look at another version of the emojis by marie Glitch app, this time implemented as a server-side app.

When we navigate around the live app, we see that there are five basic types of URLs:

  • / (index), a landing page with a list of emojis
  • /about, a page with information about the app
  • /new, a form that lets us create a new emoji
  • EMOJI, an information page that describes the EMOJI emoji
  • EMOJI/edit, a form that lets us edit the EMOJI emoji

Like the static site version of the app, we start out with the ✨ and 🎉 emojis in the index table, and we can go to those URLs to see information for those emojis.

But we can also add other emojis to the app, and they instantly display in the table and have links that work!

Gif of actress Millie Bobby Brown silently saying “boom” as she explodes her hands away from her face and the camera pans away (source).

When we look at the source code, we find the following files:

- views/
- index.pug
- about.pug
  - emojis/
- index.pug
- edit.pug
- new.pug
- server.js

All the files in the views/ directory end with .pug, which is the file extension for Pug, a template engine for Node.js. These templates will be used to build HTML files!

Note: I’m using Pug for this app, but there are lots of template engines out there for every kind of server! To name just a few, there’s also Mustache and Dust for Node.js, Action View/embedded Ruby (ERB) for Rails, Java Server Pages (JSP) for Java, etc.

Instead of having a specific file path for each emoji we want to render, we have a set of templates under the views/emojis/ directory.

This time in our server.js file, we do a bit more work! First, we instruct our server to use pug as the view engine, which will translate templates into full HTML files.

app.set("view engine", "pug");

Then, we set up route definitions! Let’s start with the simplest examples. When the URL is /about, we’ll turn views/about.pug into HTML and return it. This doesn’t take any data.

app.get("/about", (req, res) => res.render("about"));

When the URL is / (or the index route), we’ll fetch all the current emoji data and use it to fill out the views/index.pug template.

app.get("/", (req, res) => res.render("index",
{ emojis: getAllEmojis() }

If we look inside that pug template, we iterate over the list of emojis passed by the server to render a row in the table for each emoji.

th emoji
th marie's name for it
each emoji in emojis
td= emoji.icon

That means we can create more emojis, and as long as our server has access to them, they’ll automagically show up here in the table!

Gif of actress Uzo Aduba in The Wiz movie, saying “The magic is inside you” (source).

Take a look at the dynamic segments (the :emoji part) in these route definitions in server.js.

The route /💛 will match /:emoji, and req.params.emoji will be 💛.

The route /🌵/edit will match /:emoji/edit, and req.params.emoji will be 🌵.

.get((req, res) =>
res.render("emojis/edit", { emoji: getEmoji(req.params.emoji) })
app.get("/:emoji", (req, res) =>
res.render("emojis/index", { emoji: getEmoji(req.params.emoji) })

This means that we don’t need to know every possible combination of URLs we’ll ever need when we write this code! We just need to know:

  • the general pattern of the URLs we want to support
  • how to fetch the right data for each type of URL
  • the shape of the page for each URL, and where data should be inserted into it
Gif of stop-motion animation of a game of tetris, where the pieces are players in colored shirts sitting in an auditorium (source).

Advantages of a Server-Side Rendered Site

Initial page load can be very fast! The server is still returning an HTML file with everything the browser needs to render as the initial response to a request. Granted, the server will need time to fetch the data and fill out the template before it can return the file, but as soon as the browser gets the HTML back it can display the page to the user with all the relevant data immediately.

Can still handle more complex interactions with JavaScript after the page has loaded. The initial HTML is rendered by the server, but you can have that page load additional JavaScript that will render other content or update the page as needed. For example, you might render a form on your server, but then handle form validation with jQuery. Or, you might use a React component to render a complicated video player after the rest of your page loads.

Search engines can more easily load and index your site. Since the initial HTML response contains everything the user will see, web crawlers for search engines will see the full content of your site (if it’s available for the public) and can use that when building search results.

Disadvantages of a Server-Side Rendered Site

Every page has to be loaded from scratch, from the server. When a user clicks a link to see a new page, the browser leaves the current page and loads a brand new one with the updated URL. All JavaScript context is lost, and if the user is on a slow connection they may see a white flash or loading spinner while waiting for the next page to load. This can feel very slow and non-native (where “native” means the way a mobile app behaves on phones, or a desktop app behaves on desktops).

Mitigation: There are some libraries which attempt to help with this problem. For example, Turbolinks can be used to fetch the new HTML page behind the scenes and then swap out the <body> once the new content is loaded. This means your server-side rendered site can feel smoother without actually having to move your rendering logic.

For complex applications, UI logic and responsibility is split across technologies. If your initial HTML is rendered in your server and then loads JavaScript that makes major changes to the page or does very complex rendering, you may not have a single source of truth for what is in charge of a page. If your server-side template and your JavaScript templates are written in different formats, this may mean you have to write the same output in two different languages.

Good Use Cases for a Server-Side Rendered Site

  • Your application doesn’t have a lot of complex visual changes after the page loads, and can largely be written as plain HTML.
  • You want users to see the content on your pages as fast as possible.
  • You don’t need a website experience that feels like a native app.
  • You want to optimize for search engine discoverability.

Sequence diagram of a single-page app (plain text description).

Single-Page Apps

A single-page app is an application that renders dynamic content to users in the client, or browser.

The server does very minimal rendering work in a traditional single-page app. It returns the exact same HTML file for every request. That file is responsible for loading a bunch of JavaScript, which will then decide what content to render based on the URL of the browser. Code running in the browser, not the server, determines what the user sees at any given point.

Since the JavaScript running in browser has all the logic it needs to display all pages in the application, it can also render new pages instantly. Instead of asking the server for the next page, a single-page app can just swap out the relevant HTML on the current page.

Embedded Glitch app showing emojis by marie as a single-page app.

Let’s take a look at another version of the emojis by marie Glitch app, this time implemented as a single-page app.

The live app looks very similar to the server-side rendered app! The same route patterns apply here. We have all the same features and URLs. We can create new emojis and navigate to their links immediately.

When we look at the source code, we see a similar file structure:

- app/
- app.jsx
  - views/
- index.jsx
- about.jsx
  - emojis/
- index.jsx
- edit.jsx
- new.jsx

- public/
- base.html

- server.js

Our app/ directory has a bunch of JavaScript files. Since this app is implemented in React, all those files are written in JSX.

The app/views/ directory has the same files as the views/ directory in our server-side rendered app — but they’re written in a client-side templating engine instead of a server-side templating engine!

Gif of Tia and Tamera Mowry in Sister Sister show opening titles, with animated text reading “We look alike but, we’re different” (source).

Before we dive into how that code works, let’s take a peek at the server first.

The logic in server.js is much simpler than the server-side rendered app. Instead of listing out every possible route pattern, we just return the public/base.html file for every single request.

app.get('*', function(request, response) {

That’s right… the exact same HTML file is returned for every request. This has got be some magic file.

It’s… actually pretty empty. It doesn’t display anything, it just adds an empty div to the body and then loads /bundle.js.

<div id="root"></div>

<script type="text/javascript" src="/bundle.js"></script>

Notice that we didn’t define a bundle.js script. When the server starts running for the first time, it uses Webpack to bundle all the JavaScript files in app/ into a single file called public/bundle.js.

(Remember how our static site set up a definition to serve files from public/? We use that exact same behavior here in our single-page app server, because even dynamic applications have static content they want to serve!)

So the server always returns public/base.html, which loads bundle.js, which contains all of the JavaScript for our entire app.

Gif of a woman with a Target shopping cart, dropping an entire row of snack boxes into her cart, with the caption “I want all of the things” (source).

Once the JavaScript in bundle.js loads, React takes over in the browser and starts rendering content! The entry point for our app is in app/app.jsx.

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

React finds the empty <div> that was added to public/base.html and then replaces it with the contents of the <App /> component. That component uses React Router to define what component to display based on the URL.

<Route exact path="/" component={IndexView} />
<Route exact path="/about" component={AboutView} />
<Route exact path="/new" component={EmojiNewView} />
<Route exact path="/:emoji" component={EmojiIndexView} />
<Route path="/:emoji/edit" component={EmojiEditView} />

These route definitions should look similar to the routes defined in the server.js for the server-side rendered app! The distinction is that the routing logic is being handled by JavaScript running in the browser, instead of by the server.

The components for each page handle fetching the data required to render each page, and then swapping out the HTML in the page with the correct content based on that data.

We can still use regular links in our app.

<a href="/about">About</a>

This tells the browser to open a brand new page at the URL /about. It will fetch the public/base.html page from the server again, but this time the React Router will render the AboutView component based on the URL.

However, we can also use a special <Link> component defined by the React Router to swap out the content inline!

<Link to="/about">About</Link>

This tells React Router to update the URL to /about and swap out the current page content with the AboutView component. From the user’s perspective, this can feel much faster and smoother, since we’re just replacing part of the page instead of loading an entirely new one.

Gif of Elsa from Frozen movie singing as her dress magically changes around her (source).

Note: I’ve described how this app works with React and React Router, but the same general principles apply for any other single-page app framework, like Ember.js, Vue.js (with the Vue Router), Angular, etc.

You don’t technically have to use a framework to build a single-page app—you can write it yourself in regular JavaScript. But you’ll be spending a lot of time solving problems for which multiple open-source communities have spent years building elegant and efficient solutions… and a lot less time on your actual app.

Advantages of a Single-Page App

User experience of the app can feel very responsive and interactive, like a native app. Transitions between pages can appear much smoother when most of the content stays the same. Even with a slow API call, a loading spinner can be displayed in the exact part of the app that’s changing instead of having a blank white screen while waiting for the server to reply with any HTML. Most websites that feel like a native application are probably some variation of a single-page app.

Clearer division between responsibility for views/rendering and data. All the logic for rendering content is in a single place — the JavaScript that runs in the browser once the page is loaded. That includes both the initial render as well as any updates. The server is mainly responsible for providing the initial HTML file, and a robust set of API calls to return data. If you’re using a single-page app, you can even try deploying two servers: one that solely handles returning the single-page app and its assets, and another that solely handles data calls.

Disadvantages of a Single-Page App

Users may end up loading a lot of JavaScript for pages they never visit. Let’s say your single-page app contains code to render 50 different pages. If a user only visits two of those pages, they still had to load all the code as part of the initial page load. That’s a lot of unnecessary code that’s going over the network, and being parsed by the browser — and that’s even worse for mobile devices on slower networks. Large single-page apps can end up being painfully slow.

Mitigation: Some code bundlers now support code splitting, which is the process of building separate JavaScript files with the source code for distinct parts of an application. For the emojis app, we might end up with bundles like:

- application.js // React source code + App component with React Router
- index.js // just the IndexView component
- about.js // just the AboutView component
- emojis.js // the EmojiIndexView, EmojiEditView, and EmojiNewView components

Once we split out the distinct parts of our app into separate bundles that only include the code they need, we can try lazy loading, where a single-page app only fetches the source code for a page when a user requests to load it.

We could change our initial page load to only fetch application.js, which is the code we want on every single page. Then, when the app decides which route to display, it would fetch the bundle for that page and then execute it.

This can speed up the initial page load and decrease network request size, but it does mean that page transitions can be slower since the browser may need to fetch and then parse the code that actually renders the page, if it hasn’t been loaded already!

Bundlers like Webpack and Browserify support code-splitting, and frameworks like React, Vue, and Ember Engines support lazy-loading your split bundles.

Initial page load and paint can take a long time. In order to render the content a user will actually see when they first visit the site, the browser has to :

  • fetch the initial HTML file
  • fetch the JavaScript that renders the site
  • execute the JavaScript

That’s a lot more work just to load the page, and the load time can be significant if you have a lot of JavaScript to send over the wire. Even though every other page transition may feel fast and native, users may perceive your app as slow or weighty if it takes a long time before they can start interacting with it.

Mitigation: Many front-end frameworks now support isomorphic apps, or apps which utilize the same code to return a server-side rendered page that loads a single-page app. The idea is that instead of returning an empty HTML page, the server will render all the HTML for the initial page load so the user can see the app as soon as possible. But that initial page also loads the JavaScript for a single-page app that can take over the rest of the routing and make the other page transitions feel snappy.

It’s like the best of both worlds, right? But there is a cost: you have to be able to use the same rendering code in both the server and the client to make sure the user sees the right content. Check out the guides on server-side rendering for your framework of choice (like React, Vue, or Ember Fastboot) for more information and details on the tradeoffs!

By default, many single-page apps are inaccessible for screenreaders and other forms of assistive technology. Accessibility (a11y) is an important part of any website, because all people should be able to access your app in a beautiful, engaging, and understandable manner.

When a browser navigates to a new page, screenreaders can announce the change in an expected way. Traditional links using <a> tags also include helpful content for screenreaders to know that a link is clickable and where it goes. But in a single-page app, content is often swapped out on an existing page.

This can create a very confusing and inaccessible experience for screenreader users. Imagine this scenario: a user clicks a link that loads a new page, but nothing tells them that the content has changed, and their focus is still on the old button. They have to manually navigate the entire page and try to guess what changed and when.

Or worse: since single-page apps can use custom code to transition pages, a developer may have made a <div> that just looks like a button visually. The user has no way of knowing that they can press that element to see a new page, and it doesn’t handle keyboard events.

Mitigation: single-page apps can be made accessible, but it requires a bit more attention to detail!

Websites should follow the Web Content Accessibility Guidelines (WCAG) to ensure that everyone can access and interact with the app. Some specific concerns for single-page apps include:

  • ensure that focus is changed to new content after a page transition
  • ensure that all links have proper markup to indicate they are interactive and what they do
  • ensure that all interactive elements handle keyboard events as well as mouse events

There are tons of learning resources out there (for example, the Web Accessibility Checklist from The A11y Project or this podcast on Accessibility in Single Page Apps from The Frontside). In addition, some frameworks have specific tooling and libraries that help make accessibility a top priority!

Here are a few blog posts outlining the available a11y tooling for Ember, React, and Vue.

Good Use Cases for a Single-Page App

  • Your application has a lot of complex interactions.
  • You want your application to feel like a native app, especially when navigating between pages.
  • You are willing to put in a bit of extra effort to handle focus changes and ensure your application is accessible.
  • You either are not concerned about initial page load time and bundle size, or you have the time and resources to spend on solutions like isomorphic apps and code splitting/lazy loading.

Gif of Judd Nelson in The Breakfast Club movie, walking across an empty football field and pumping his fist (source).

That was a lot of information! Kudos to you if you made the whole way through.

To summarize:

  • a static site serves files without making changes. It’s fast and cheap, but pretty limited.
  • a server-side rendered site makes rendering decisions on the server. Initial page load time can be very fast, but it doesn’t feel like a native app since you have to request a new page every time you make a transition.
  • a single-page app makes rendering decisions in the browser. They feel much more like a native app but can be slow to load unless you use advanced techniques (like isomorphic apps that combine server-side rendering with a single-page app, or code-splitting and lazy-loading). Additionally, single-page apps are usually less accessible out of the box than most server-side rendered apps.

There’s no one perfect architecture! Every approach has advantages and disadvantages. But if you understand how each type of application works, you can make an informed decision about the best choice for you, based on your needs, priorities, and preferences.

Now go forth and build great apps! 🎉

Did you like this post? I’ll be sporadically publishing more content like this to the git checkout -b idk-what-im-doing blog in the future! Tweet me @mariechatfield to let me know what kind of resources you’d like to see next.

This post is also available at, which has a handy table of contents and quick links to each section for reference!

P.S. a big thank you to my wonderful coworkers at Pingboard for reviewing and giving feedback on this article, especially Kelsey Huse and Ryan Schutte! 🙏🏻