The Era Of Platform Primitives Is Finally Here
Atila Fassina 2024-05-28T12:00:00+00:00
2024-05-30T21:04:55+00:00
This article is sponsored by Netlify
In the past, the web ecosystem moved at a very slow pace. Developers would go years without a new language feature or working around a weird browser quirk. This pushed our technical leaders to come up with creative solutions to circumvent the platform’s shortcomings. We invented bundling, polyfills, and transformation steps to make things work everywhere with less of a hassle.
Slowly, we moved towards some sort of consensus on what we need as an ecosystem. We now have TypeScript and Vite as clear preferences—pushing the needle of what it means to build consistent experiences for the web. Application frameworks have built whole ecosystems on top of them: SolidStart, Nuxt, Remix, and Analog are examples of incredible tools built with such primitives. We can say that Vite and TypeScript are tooling primitives that empower the creation of others in diverse ecosystems.
With bundling and transformation needs somewhat defined, it was only natural that framework authors would move their gaze to the next layer they needed to abstract: the server.
Server Primitives
The UnJS folks have been consistently building agnostic tooling that can be reused in different ecosystems. Thanks to them, we now have frameworks and libraries such as H3 (a minimal Node.js server framework built with TypeScript), which enables Nitro (a whole server runtime powered by Vite, and H3), that in its own turn enabled Vinxi (an application bundler and server runtime that abstracts Nitro and Vite).
Nitro is used already by three major frameworks: Nuxt, Analog, and SolidStart. While Vinxi is also used by SolidStart. This means that any platform which supports one of these, will definitely be able to support the others with zero additional effort.
This is not about taking a bigger slice of the cake. But making the cake bigger for everyone.
Frameworks, platforms, developers, and users benefit from it. We bet on our ecosystem together instead of working in silos with our monolithic solutions. Empowering our developer-users to gain transferable skills and truly choose the best tool for the job with less vendor lock-in than ever before.
Serverless Rejoins Conversation
Such initiatives have probably been noticed by serverless platforms like Netlify. With Platform Primitives, frameworks can leverage agnostic solutions for common necessities such as Incremental Static Regeneration (ISR), Image Optimization, and key/value (kv
) storage.
As the name implies, Netlify Platform Primitives are a group of abstractions and helpers made available at a platform level for either frameworks or developers to leverage when using their applications. This brings additional functionality simultaneously to every framework. This is a big and powerful shift because, up until now, each framework would have to create its own solutions and backport such strategies to compatibility layers within each platform.
Moreover, developers would have to wait for a feature to first land on a framework and subsequently for support to arrive in their platform of choice. Now, as long as they’re using Netlify, those primitives are available directly without any effort and time put in by the framework authors. This empowers every ecosystem in a single measure.
Serverless means server infrastructure developers don’t need to handle. It’s not a misnomer, but a format of Infrastructure As A Service.
As mentioned before, Netlify Platform Primitives are three different features:
- Image CDN
A content delivery network for images. It can handle format transformation and size optimization via URL query strings. - Caching
Basic primitives for their server runtime that help manage the caching directives for browser, server, and CDN runtimes smoothly. - Blobs
A key/value (KV) storage option is automatically available to your project through their SDK.
Let’s take a quick dive into each of these features and explore how they can increase our productivity with a serverless fullstack experience.
Image CDN
Every image in a /public
can be served through a Netlify function. This means it’s possible to access it through a /.netlify/images
path. So, without adding sharp or any image optimization package to your stack, deploying to Netlify allows us to serve our users with a better format without transforming assets at build-time. In a SolidStart, in a few lines of code, we could have an Image component that transforms other formats to .webp
.
import { type JSX } from "solid-js";
const SITE_URL = "https://example.com";
interface Props extends JSX.ImgHTMLAttributes<HTMLImageElement> {
format?: "webp" | "jpeg" | "png" | "avif" | "preserve";
quality?: number | "preserve";
}
const getQuality = (quality: Props["quality"]) => {
if (quality === "preserve") return"";
return `&q=${quality || "75"}`;
};
function getFormat(format: Props["format"]) {
switch (format) {
case "preserve":
return" ";
case "jpeg":
return `&fm=jpeg`;
case "png":
return `&fm=png`;
case "avif":
return `&fm=avif`;
case "webp":
default:
return `&fm=webp`;
}
}
export function Image(props: Props) {
return (
<img
{...props}
src={`${SITE_URL}/.netlify/images?url=/${props.src}${getFormat(
props.format
)}${getQuality(props.quality)}`}
/>
);
}
Notice the above component is even slightly more complex than bare essentials because we’re enforcing some default optimizations. Our getFormat
method transforms images to .webp
by default. It’s a broadly supported format that’s significantly smaller than the most common and without any loss in quality. Our get quality
function reduces the image quality to 75% by default; as a rule of thumb, there isn’t any perceivable loss in quality for large images while still providing a significant size optimization.
Caching
By default, Netlify caching is quite extensive for your regular artifacts – unless there’s a new deployment or the cache is flushed manually, resources will last for 365 days. However, because server/edge functions are dynamic in nature, there’s no default caching to prevent serving stale content to end-users. This means that if you have one of these functions in production, chances are there’s some caching to be leveraged to reduce processing time (and expenses).
By adding a cache-control header, you already have done 80% of the work in optimizing your resources for best serving users. Some commonly used cache control directives:
{
"cache-control": "public, max-age=0, stale-while-revalidate=86400"
}
public
: Store in a shared cache.max-age=0
: resource is immediately stale.stale-while-revalidate=86400
: if the cache is stale for less than 1 day, return the cached value and revalidate it in the background.
{
"cache-control": "public, max-age=86400, must-revalidate"
}
public
: Store in a shared cache.max-age=86400
: resource is fresh for one day.must-revalidate
: if a request arrives when the resource is already stale, the cache must be revalidated before a response is sent to the user.
Note: For more extensive information about possible compositions of Cache-Control
directives, check the mdn entry on Cache-Control.
The cache is a type of key/value storage. So, once our responses are set with proper cache control, platforms have some heuristics to define what the key
will be for our resource within the cache storage. The Web Platform has a second very powerful header that can dictate how our cache behaves.
The Vary response header is composed of a list of headers that will affect the validity of the resource (method
and the endpoint URL are always considered; no need to add them). This header allows platforms to define other headers defined by location, language, and other patterns that will define for how long a response can be considered fresh.
The Vary response header is a foundational piece of a special header in Netlify Caching Primitive. The Netlify-Vary
will take a set of instructions on which parts of the request a key should be based. It is possible to tune a response key not only by the header but also by the value of the header.
- query: vary by the value of some or all request query parameters.
- header: vary by the value of one or more request headers.
- language: vary by the languages from the
Accept-Language
header. - country: vary by the country inferred from a GeoIP lookup on the request IP address.
- cookie: vary by the value of one or more request cookie keys.
This header offers strong fine-control over how your resources are cached. Allowing for some creative strategies to optimize how your app will perform for specific users.
Blob Storage
This is a highly-available key/value store, it’s ideal for frequent reads and infrequent writes. They’re automatically available and provisioned for any Netlify Project.
It’s possible to write on a blob from your runtime or push data for a deployment-specific store. For example, this is how an Action Function would register a number of likes in store with SolidStart.
import { getStore } from "@netlify/blobs";
import { action } from "@solidjs/router";
export const upVote = action(async (formData: FormData) => {
"use server";
const postId = formData.get("id");
const postVotes = formData.get("votes");
if (typeof postId !== "string" || typeof postVotes !== "string") return;
const store = getStore("posts");
const voteSum = Number(postVotes) + 1)
await store.set(postId, String(voteSum);
console.log("done");
return voteSum
});
- Check
@netlify/blobs
API documentation for more examples and use-cases.
Final Thoughts
With high-quality primitives, we can enable library and framework creators to create thin integration layers and adapters. This way, instead of focusing on how any specific platform operates, it will be possible to focus on the actual user experience and practical use-cases for such features. Monoliths and deeply integrated tooling make sense to build platforms fast with strong vendor lock-in, but that’s not what the community needs. Betting on the web platform is a more sensible and future-friendly way.
Let me know in the comments what your take is about unbiased tooling versus opinionated setups!
(il)