Best Practices for JAMStack Projects in Production at Scale
More and more developers are embracing JAMStack for building and shipping web apps that perform fast. The frontend tooling around building websites have become complex and powerful over the years. Static sites are not new. In fact the early web had pure static sites where developers could just drop html files on to a server via FTP. Server side rendering dominated the next wave of websites as data requirements became more dynamic. Once the server became the bottleneck, single page apps came into the foray and has been predominatly used for any web apps that doesn't require search indexing.
In comes JAMStack (JavaScript, APIs, Markup) architecture that leverages the best of both worlds. Static first to perform fast and dynamic on the client to cater to any complex interactions or data requirements.
Hasura fits into JAMStack by being the data source via its unified GraphQL API endpoint.
It's easy to say build static sites, deploy and be done with it. But with the frontend tooling that's available, there are certain nuances to take care of. In this post, we will look at the best practices for using JAMStack in production and at scale. Most of the fundamental best practices are listed on the official JAMStack site and this post is inspired by that, albeit a detailed breakdown of that.
CDN
The first and the most popular - The entire project lives on a CDN.
The idea behind this is to distribute serving of the web app to be as close as possible to the user's region (edge). The HTML files are pre-compiled and available already. There is no server dependency. This vastly improves performance and user experience. But there are other side benefits you get by using a CDN for your project.
- Automatic Gzip/Brotli compression
- HTTP/2
- SSL
- Setting up redirects, restricting access
- Caching of static assets
Purging of Cache
Caching of static assets sounds prudent. But as we keep deploying changes to our web apps often, it is important to configure cache settings correctly to avoid stale or buggy webpages from being served to the user. CDNs typically maintain a cache that can be purged via their UI/API. But then there is browser caching. With JAMStack apps, browsers
- Should never cache HTML resources - The
cache-control
header should becache-control: public, max-age=0, must-revalidate
1 - Should cache JS and CSS resources. The
cache-control
header should becache-control: public, max-age=31536000, immutable
. This is because the JS and CSS resources with modern frontend tooling are hashed with unique names on every build and hence you need not worry about stale files in subsequent deployments. Of course this doesn't apply to you if you are NOT using webpack or other build tools that does this automatically for you. - Should always cache static assets like images. Again the
cache-control
header should becache-control: public, max-age=31536000, immutable
.
For example, this Hasura Blog is built using Gatsby.js and leverages some of the cache settings for optimal performance and frequent deployments. It is served by Cloudflare, leveraging the CDN benefits.
You might also want to cache your API layer for the dynamically fetched portions of data on the client side. Hasura Cloud's caching layer for GraphQL APIs significantly improves performance for frequently accessed queries.
Git - Version Control
The second, The entire project lives on git / version control
. This way any body can clone and develop the site locally on their machine without complex dependencies. Typically, for web apps, this means an npm install
should suffice. But what if your apps require custom logic to function for some interactive elements on the page? You will probably have some Javascript functions (serverless) that can also run in parallel with your application with dependencies installed.
Automated Builds and Deploys
Since HTML files are pre-compiled, there would be a build step to (re)-compile again whenever there is a change. This change could be cosmetic UI changes or even content on the page sourced from a data source (like Hasura) being updated. The build step can be automated through
- git push triggers on a branch
- Webhook API calls
- CMS triggers
depending on how your project is structured. For example, with Hasura, you can trigger a netlify build whenever there is a mutation made in Hasura. This way, you can use API calls to automate your build / deploy step.
Atomic Deploys
At scale, when there are a large number of files any changes should reflect at the same time and not inconsistently file by file. Back in the day, these would be issues when files are "uploaded" in batches. With modern deployment tooling, changes go live only when the entire deployment is ready to go live. This way you ensure consistency for what users see on the page, irrespective of where they are from.
Performance and User Experience
This particular one is probably applicable for most web apps. But specifically for JAMStack because they can make the most out of it through the tooling. Google has guidelines around Core Web Vitals which will start impacting search rankings from May 2021. Some of the metrics being tracked are
First Contentful Paint
FCP measures the point at which text or graphics are first rendered to the screen. Take a look at this post for reference. Modern tooling can be leveraged to ensure critical CSS and fonts are loaded quickly on priority for the above the fold content.
Time to first byte
Since static-sites are precompiled, it doesn't need to "wait" for the server to compile on the fly and respond back to the browser. A CDN can typically return the HTML from it's cache and serve it through the edge. This will vastly improve the time to first byte.
- Add
loading=“lazy”
attribute to all images below the first fold. - Add
<link rel=“preload”>
to your font assets - Use tools like purgecss to remove unused styles
- Preconnect to critical origins.
- Self host your fonts. Note: Google fonts typically have 2 roundtrips to load and render your font.
Headless CMS
Don't be afraid to use Headless CMS solutions for the backend. This is different from the WYSIWYG editors that do SSR. The way Headless CMS works is that, any changes you make via the CMS can be accessed via APIs. The build tooling can then integrate with your CMS APIs to recompile your HTML files. Or your client can also make dynamic data fetching through the APIs for the not-so important content.
An example at Hasura: Our Blog uses the Ghost CMS for content creation. We like it for the ease of use, self-hosting capabilities and team management. Having said, using the website served by Ghost is not recommended. It is server-side rendered and hence slow and unoptimized for blogs. So we use a simple pipeline where our blog posts get created on Ghost using their CMS UI.
Having said all of these, JAMStack fits in as a good architecture for specific use cases only. Single Page Apps (SPAs) have a clear use case to exist and those apps most likely do not need most of these recommendations to work better.