The Era Of Platform Primitives Is Finally Here<\/h1>\nAtila Fassina<\/address>\n 2024-05-28T12:00:00+00:00
\n 2024-10-15T23:05:45+00:00
\n <\/header>\n
This article is sponsored by Netlify<\/b><\/p>\n
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\u2019s shortcomings. We invented bundling, polyfills, and transformation steps to make things work everywhere with less of a hassle.<\/p>\n
Slowly, we moved towards some sort of consensus on what we need as an ecosystem. We now have TypeScript and Vite as clear preferences\u2014pushing the needle of what it means to build consistent experiences for the web. Application frameworks have built whole ecosystems on top of them: SolidStart<\/a>, Nuxt<\/a>, Remix<\/a>, and Analog<\/a> 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.<\/p>\nWith 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.<\/p>\n
Server Primitives<\/h2>\n
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> (a minimal Node.js server framework built with TypeScript), which enables Nitro<\/a> (a whole server runtime powered by Vite<\/a>, and H3<\/a>), that in its own turn enabled Vinxi (an application bundler and server runtime that abstracts Nitro and Vite).<\/p>\nNitro<\/a> is used already by three major frameworks: Nuxt<\/a>, Analog<\/a>, and SolidStart<\/a>. 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<\/strong>.<\/p>\nThis is not about taking a bigger slice of the cake. But making the cake bigger for everyone.<\/p><\/blockquote>\n
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<\/strong>.<\/p>\nServerless Rejoins Conversation<\/h2>\n
Such initiatives have probably been noticed by serverless platforms like Netlify. With Platform Primitives<\/a>, frameworks can leverage agnostic solutions for common necessities such as Incremental Static Regeneration (ISR), Image Optimization, and key\/value (kv<\/code>) storage.<\/p>\nAs the name implies, Netlify Platform Primitives<\/strong> 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.<\/p>\nMoreover, 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\u2019re 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.<\/p>\n
Serverless means server infrastructure developers don\u2019t need to handle. It\u2019s not a misnomer, but a format of Infrastructure As A Service<\/strong>.<\/p><\/blockquote>\nAs mentioned before, Netlify Platform Primitives<\/a> are three different features:<\/p>\n\n- Image CDN<\/strong>
\nA content delivery network<\/a> for images. It can handle format transformation and size optimization via URL query strings.<\/li>\n- Caching<\/strong>
\nBasic primitives for their server runtime that help manage the caching directives for browser, server, and CDN runtimes smoothly.<\/li>\n- Blobs<\/strong>
\nA key\/value (KV) storage option is automatically available to your project through their SDK.<\/li>\n<\/ol>\nLet\u2019s take a quick dive into each of these features and explore how they can increase our productivity with a serverless fullstack experience.<\/p>\n
Image CDN<\/h3>\n
Every image in a \/public<\/code> can be served through a Netlify function. This means it\u2019s possible to access it through a \/.netlify\/images<\/code> path. So, without adding sharp<\/a> or any image optimization package to your stack, deploying to Netlify<\/a> allows us to serve our users with a better format without transforming assets at build-time. In a SolidStart<\/a>, in a few lines of code, we could have an Image component that transforms other formats to .webp<\/code>.<\/p>\n\nimport { type JSX } from \"solid-js\";\n\nconst SITE_URL = \"https:\/\/example.com\";\n\ninterface Props extends JSX.ImgHTMLAttributes<HTMLImageElement> {\n format?: \"webp\" | \"jpeg\" | \"png\" | \"avif\" | \"preserve\";\n quality?: number | \"preserve\";\n}\n\nconst getQuality = (quality: Props[\"quality\"]) => {\n if (quality === \"preserve\") return\"\";\n return `&q=${quality || \"75\"}`;\n};\n\nfunction getFormat(format: Props[\"format\"]) {\n switch (format) {\n case \"preserve\":\n return\" \";\n case \"jpeg\":\n return `&fm=jpeg`;\n case \"png\":\n return `&fm=png`;\n case \"avif\":\n return `&fm=avif`;\n case \"webp\":\n default:\n return `&fm=webp`;\n }\n}\n\nexport function Image(props: Props) {\n return (\n <img\n {...props}\n src={`${SITE_URL}\/.netlify\/images?url=\/${props.src}${getFormat(\n props.format\n )}${getQuality(props.quality)}`}\n \/>\n );\n}\n<\/code><\/pre>\n<\/div>\nNotice the above component is even slightly more complex than bare essentials because we\u2019re enforcing some default optimizations. Our getFormat<\/code> method transforms images to .webp<\/code> by default. It\u2019s a broadly supported format that\u2019s significantly smaller than the most common and without any loss in quality. Our get quality<\/code> function reduces the image quality to 75% by default; as a rule of thumb, there isn\u2019t any perceivable loss in quality for large images while still providing a significant size optimization.<\/p>\n\n- Check our little component at play<\/a>.<\/li>\n
- Source code: SolidStart and Netlify Primitives<\/a>.<\/li>\n<\/ul>\n
Caching<\/h3>\n
By default, Netlify caching is quite extensive for your regular artifacts – unless there\u2019s 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\u2019s 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\u2019s some caching to be leveraged to reduce processing time (and expenses).<\/p>\n
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:<\/p>\n
{\n \"cache-control\": \"public, max-age=0, stale-while-revalidate=86400\"\n\n}<\/code><\/pre>\n\npublic<\/code>: Store in a shared cache.<\/li>\nmax-age=0<\/code>: resource is immediately stale.<\/li>\nstale-while-revalidate=86400<\/code>: if the cache is stale for less than 1 day, return the cached value and revalidate it in the background.<\/li>\n<\/ul>\n{\n \"cache-control\": \"public, max-age=86400, must-revalidate\"\n\n}\n<\/code><\/pre>\n\npublic<\/code>: Store in a shared cache.<\/li>\nmax-age=86400<\/code>: resource is fresh for one day.<\/li>\nmust-revalidate<\/code>: if a request arrives when the resource is already stale, the cache must be revalidated before a response is sent to the user.<\/li>\n<\/ul>\nNote<\/strong>: For more extensive information about possible compositions of Cache-Control<\/code> directives, check the mdn entry on Cache-Control<\/a>.<\/em><\/p>\nThe cache is a type of key\/value storage<\/strong>. So, once our responses are set with proper cache control, platforms have some heuristics to define what the key<\/code> 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.<\/p>\nThe Vary response header<\/a> is composed of a list of headers that will affect the validity of the resource (method<\/code> 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.<\/p>\nThe Vary<\/strong> response header is a foundational piece of a special header in Netlify Caching Primitive<\/a>. The Netlify-Vary<\/code> 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<\/strong> of the header.<\/p>\n\n- query<\/a>: vary by the value of some or all request query parameters.<\/li>\n
- header<\/a>: vary by the value of one or more request headers.<\/li>\n
- language<\/a>: vary by the languages from the
Accept-Language<\/code> header.<\/li>\n- country<\/a>: vary by the country inferred from a GeoIP lookup on the request IP address.<\/li>\n
- cookie<\/a>: vary by the value of one or more request cookie keys.<\/li>\n<\/ul>\n
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.<\/p>\n
Blob Storage<\/h3>\n
This is a highly-available key\/value store, it\u2019s ideal for frequent reads and infrequent writes. They\u2019re automatically available and provisioned for any Netlify Project.<\/p>\n
It\u2019s 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<\/a> would register a number of likes<\/strong> in store with SolidStart.<\/p>\nimport { getStore } from \"@netlify\/blobs\";\nimport { action } from \"@solidjs\/router\";\n\nexport const upVote = action(async (formData: FormData) => {\n \"use server\";\n\n const postId = formData.get(\"id\");\n const postVotes = formData.get(\"votes\");\n\n if (typeof postId !== \"string\" || typeof postVotes !== \"string\") return;\n\n const store = getStore(\"posts\");\n const voteSum = Number(postVotes) + 1)\n \n await store.set(postId, String(voteSum);\n\n console.log(\"done\");\n return voteSum\n \n});\n<\/code><\/pre>\n
\n 2024-10-15T23:05:45+00:00
\n <\/header>\n
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.<\/p>\n
Server Primitives<\/h2>\n
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> (a minimal Node.js server framework built with TypeScript), which enables Nitro<\/a> (a whole server runtime powered by Vite<\/a>, and H3<\/a>), that in its own turn enabled Vinxi (an application bundler and server runtime that abstracts Nitro and Vite).<\/p>\n Nitro<\/a> is used already by three major frameworks: Nuxt<\/a>, Analog<\/a>, and SolidStart<\/a>. 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<\/strong>.<\/p>\n This is not about taking a bigger slice of the cake. But making the cake bigger for everyone.<\/p><\/blockquote>\n 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<\/strong>.<\/p>\n Such initiatives have probably been noticed by serverless platforms like Netlify. With Platform Primitives<\/a>, frameworks can leverage agnostic solutions for common necessities such as Incremental Static Regeneration (ISR), Image Optimization, and key\/value ( As the name implies, Netlify Platform Primitives<\/strong> 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.<\/p>\n 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\u2019re 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.<\/p>\n Serverless means server infrastructure developers don\u2019t need to handle. It\u2019s not a misnomer, but a format of Infrastructure As A Service<\/strong>.<\/p><\/blockquote>\n As mentioned before, Netlify Platform Primitives<\/a> are three different features:<\/p>\n Let\u2019s take a quick dive into each of these features and explore how they can increase our productivity with a serverless fullstack experience.<\/p>\n Every image in a Notice the above component is even slightly more complex than bare essentials because we\u2019re enforcing some default optimizations. Our By default, Netlify caching is quite extensive for your regular artifacts – unless there\u2019s 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\u2019s 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\u2019s some caching to be leveraged to reduce processing time (and expenses).<\/p>\n 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:<\/p>\n Note<\/strong>: For more extensive information about possible compositions of The cache is a type of key\/value storage<\/strong>. So, once our responses are set with proper cache control, platforms have some heuristics to define what the The Vary response header<\/a> is composed of a list of headers that will affect the validity of the resource ( The Vary<\/strong> response header is a foundational piece of a special header in Netlify Caching Primitive<\/a>. The 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.<\/p>\n This is a highly-available key\/value store, it\u2019s ideal for frequent reads and infrequent writes. They\u2019re automatically available and provisioned for any Netlify Project.<\/p>\n It\u2019s 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<\/a> would register a number of likes<\/strong> in store with SolidStart.<\/p>\nServerless Rejoins Conversation<\/h2>\n
kv<\/code>) storage.<\/p>\n
\n
\nA content delivery network<\/a> for images. It can handle format transformation and size optimization via URL query strings.<\/li>\n
\nBasic primitives for their server runtime that help manage the caching directives for browser, server, and CDN runtimes smoothly.<\/li>\n
\nA key\/value (KV) storage option is automatically available to your project through their SDK.<\/li>\n<\/ol>\nImage CDN<\/h3>\n
\/public<\/code> can be served through a Netlify function. This means it\u2019s possible to access it through a
\/.netlify\/images<\/code> path. So, without adding sharp<\/a> or any image optimization package to your stack, deploying to Netlify<\/a> allows us to serve our users with a better format without transforming assets at build-time. In a SolidStart<\/a>, in a few lines of code, we could have an Image component that transforms other formats to
.webp<\/code>.<\/p>\n
import { type JSX } from \"solid-js\";\n\nconst SITE_URL = \"https:\/\/example.com\";\n\ninterface Props extends JSX.ImgHTMLAttributes<HTMLImageElement> {\n format?: \"webp\" | \"jpeg\" | \"png\" | \"avif\" | \"preserve\";\n quality?: number | \"preserve\";\n}\n\nconst getQuality = (quality: Props[\"quality\"]) => {\n if (quality === \"preserve\") return\"\";\n return `&q=${quality || \"75\"}`;\n};\n\nfunction getFormat(format: Props[\"format\"]) {\n switch (format) {\n case \"preserve\":\n return\" \";\n case \"jpeg\":\n return `&fm=jpeg`;\n case \"png\":\n return `&fm=png`;\n case \"avif\":\n return `&fm=avif`;\n case \"webp\":\n default:\n return `&fm=webp`;\n }\n}\n\nexport function Image(props: Props) {\n return (\n <img\n {...props}\n src={`${SITE_URL}\/.netlify\/images?url=\/${props.src}${getFormat(\n props.format\n )}${getQuality(props.quality)}`}\n \/>\n );\n}\n<\/code><\/pre>\n<\/div>\n
getFormat<\/code> method transforms images to
.webp<\/code> by default. It\u2019s a broadly supported format that\u2019s significantly smaller than the most common and without any loss in quality. Our
get quality<\/code> function reduces the image quality to 75% by default; as a rule of thumb, there isn\u2019t any perceivable loss in quality for large images while still providing a significant size optimization.<\/p>\n
\n
Caching<\/h3>\n
{\n \"cache-control\": \"public, max-age=0, stale-while-revalidate=86400\"\n\n}<\/code><\/pre>\n
\n
public<\/code>: Store in a shared cache.<\/li>\n
max-age=0<\/code>: resource is immediately stale.<\/li>\n
stale-while-revalidate=86400<\/code>: if the cache is stale for less than 1 day, return the cached value and revalidate it in the background.<\/li>\n<\/ul>\n
{\n \"cache-control\": \"public, max-age=86400, must-revalidate\"\n\n}\n<\/code><\/pre>\n
\n
public<\/code>: Store in a shared cache.<\/li>\n
max-age=86400<\/code>: resource is fresh for one day.<\/li>\n
must-revalidate<\/code>: if a request arrives when the resource is already stale, the cache must be revalidated before a response is sent to the user.<\/li>\n<\/ul>\n
Cache-Control<\/code> directives, check the mdn entry on Cache-Control<\/a>.<\/em><\/p>\n
key<\/code> 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.<\/p>\n
method<\/code> 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.<\/p>\n
Netlify-Vary<\/code> 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<\/strong> of the header.<\/p>\n
\n
Accept-Language<\/code> header.<\/li>\n
Blob Storage<\/h3>\n
import { getStore } from \"@netlify\/blobs\";\nimport { action } from \"@solidjs\/router\";\n\nexport const upVote = action(async (formData: FormData) => {\n \"use server\";\n\n const postId = formData.get(\"id\");\n const postVotes = formData.get(\"votes\");\n\n if (typeof postId !== \"string\" || typeof postVotes !== \"string\") return;\n\n const store = getStore(\"posts\");\n const voteSum = Number(postVotes) + 1)\n \n await store.set(postId, String(voteSum);\n\n console.log(\"done\");\n return voteSum\n \n});\n<\/code><\/pre>\n