On a JAMStack/Isomorphic-Rendered Hybrid Application

One thing I learned in college from Business Intelligence—the study of how to transform raw data into useful information—is that each data doesn't have the same granularity. Hence, some database tables, also known as dimensions, cannot be updated at the same rate. There are slowly-changing dimensions you can update once a week or once a year (e.g your business' yearly profit), and fast-changing ones you need to change in real-time (heart rate monitoring, for example).

Similarly, web pages within the same website do not forcibly change at the same speed.

It is common, however, to build each website as if the change rate was constant across the entirety of it. It's a useful approximation because it simplifies the overall software architecture. Websites are either static-generated with frameworks like GatsbyJS or Hugo (JAMStack), or dynamically-generated on the server, the client, or both (isomorphic rendering).

Each paradigm has its pros and cons.

The JAMStack paradigm is incredibly performant at building fast websites that scale. The webpages are pre-rendered and don't rely on a web server to be brought to an end-user. The content can be updated in real-time using client-side code and APIs.

It doesn't sit well with data that needs to change frequently. For example, building a real-time news feed would imply the need to trigger builds at fixed intervals using cron jobs. In Gatsby's case, it's not even possible to change a small part of the website without re-building the whole, and it takes several minutes to deploy changes.

Fully static-generated websites are suitable for personal websites and blogs. If you build a social network, you need a more traditional approach.

Fast-changing web pages are better off with an offline-first isomorphic approach: server-side rendered for SEO, cached by service workers, and updated on the client-side.

Client-side rendering is awesome, but it hurts the discoverability of your content. Fortunately, every web application doesn't need SEO, but if you are Medium, you'll need search engines to see the latest content. Twitter is a counter-example of an app built with client-side rendering: you see a loading spinner for a brief moment, and then you are served the latest tweets.

Client-side rendering is characteristic of Single Page Applications: the navigation between the different pages is smoother, you don't need to redirect the user and its data between different parts of the website.

Server-side is the oldest approach. A browser asks a web server for a resource defined by a URL, and sends back HTML content. There is no additional loading time and you can directly read the content. Search engines love that, and if the loading time isn't too high, your users will love it too.

Isomorphic-rendered applications take the best of both worlds. Search engines can see content generated on the server-side, but each subsequent update will be performed for the user on the client-side and will smooth up the web experience. The issue is that operating on both sides can lead to additional code complexity and redundancy.

Some websites would benefit from a hybrid approach. I'm thinking of web apps like Product Hunt, Makerlog, Medium, or even The Co-Writers. Slowly-changing web pages would be generated according to the JAMStack paradigm, and fast-changing ones with isomorphic generation.

In those cases, makers shouldn't limit themselves to one paradigm and use frameworks that aren't forcing them into one. Full-stack web frameworks such as Laravel, Django, Ruby on Rails, or Symfony come to mind. This way, the added complexity is limited since you just work with one single monolithic framework. Don't use GatsbyJS and PHP in parallel, for example. Here is how I would proceed:

  1. Analyze the different web pages of your application and figure out how fast they are likely to change. Make a distinction between fast-changing and slow-changing web pages.
  2. Use an isomorphic approach on fast-changing web pages: generate app shells with the newest content on the server-side, cache them using service workers (Progressive Web App), and use hydration mechanisms (ReactDOM.hydrate for ReactJS, data-server-rendered="true" for VueJS) on the client-side. You'll probably have redundant code shared between both sides, but it's a necessary evil in my opinion.
  3. Use built-in templating engines, .htaccess rules, and binary file responses to serve authorized pre-rendered static web pages when the main content isn't likely to change (e.g: an article page). Use service workers for caching and client-side functions and APIs to load the non-core parts of the page (the comment section) or hydrate sections with rich Javascript features (Medium's toolbar when you select a part of an article). Rebuild the static pages one at a time when the database is updated or using a less frequent cron job.

The only pain point I have at the moment is managing user sessions between static and dynamic pages to perform authenticated actions because I can't pass data to static-generated pages the usual way. I might have found a solution though, inspired by how Auth0 does it. I would need to pass an access token in the URL, fetch it, and rewrite the URL on the client-side. The access token is then stored in a variable at a namespace-scope that is inaccessible from the console, updated depending on its expiry date, and used anytime the user needs to perform an action.

This is how I would build a high-performance web application for The Co-Writers 2.0.