Rendering models that scale. Whatever your framework

A presentation at MMT Tech Meetup in November 2021 in by Phil Hawksworth

Slide 1

Slide 1

Rendering models that scale Whatever your framework Phil Hawksworth Director of Developer Experience, Netlify

Slide 2

Slide 2

Finding the right approach for a project

Slide 3

Slide 3

Mo clients, mo problems (Mo clients, mo architectures) @philhawksworth unsplash.com/photos/Bli_WDC2oU0

Slide 4

Slide 4

Web development is not Highlander

Slide 5

Slide 5

Finding the simplest solution

Slide 6

Slide 6

Finding the simplest solution (implementation, cost, support, confidence, robustness)

Slide 7

Slide 7

Jamstack @philhawksworth

Slide 8

Slide 8

Jamstack A way of thinking about how to build for the web. The UI is compiled, the frontend is decoupled, and data is pulled in as needed. @philhawksworth

Slide 9

Slide 9

from Generating responses to every request on demand @philhawksworth

Slide 10

Slide 10

to Generating responses to requests in advance @philhawksworth

Slide 11

Slide 11

to Generating responses to requests in advance ( You know… like we did in the 90s ) @philhawksworth

Slide 12

Slide 12

BUZZW ORD BIN Pre-rendering @philhawksworth GO

Slide 13

Slide 13

? N E H W Build time @philhawksworth VS Request time

Slide 14

Slide 14

Front-end code is no longer limited to being a product of a back-end system @philhawksworth

Slide 15

Slide 15

? E R E WH Application server @philhawksworth VS Serverless functions B UZZW O RD B INGO

Slide 16

Slide 16

Front-end code is no longer limited to being a product of a back-end system @philhawksworth

Slide 17

Slide 17

…I love these discussions!

Slide 18

Slide 18

@ Phil Hawksworth Director of Developer Experience, Netlify

Slide 19

Slide 19

findthat.at/jamstack/book

Slide 20

Slide 20

findthat.at/interesting

Slide 21

Slide 21

Let’s talk 1 2 Benefits and limits Breaking the ceiling 3 Removing the ceiling

Slide 22

Slide 22

1 Benefits and limits @philhawksworth

Slide 23

Slide 23

Security Scale Speed How susceptible our infrastructure is to attack How well we can handle high volumes of traffic How quickly we can deliver content to the users @philhawksworth Workflow Developer e xperie nce Sperience How efficiently and confidently we can build, release, and maintain sites

Slide 24

Slide 24

Benefit Simplicity @philhawksworth

Slide 25

Slide 25

TRADITIONAL STACK LOAD BALANCERS WEB SERVERS DATABASES JAMSTACK ASSETS ON CDN @philhawksworth BUILD CODE & CONTENT

Slide 26

Slide 26

TRADITIONAL STACK LOAD BALANCERS WEB SERVERS DATABASES JAMSTACK ASSETS ON CDN BUILD AHEAD OF TIME @philhawksworth CODE & CONTENT

Slide 27

Slide 27

Benefit Immutable, Atomic deploys @philhawksworth

Slide 28

Slide 28

Live Deploy 88 Deploy 87 Deploy 86 @philhawksworth Deploy 86

Slide 29

Slide 29

Deploy 89 Live Deploy 88 Deploy 87 Deploy 86 @philhawksworth Deploy 86

Slide 30

Slide 30

Live Deploy 89 Deploy 88 Deploy 87 Deploy 86 @philhawksworth Deploy 86

Slide 31

Slide 31

Deploy 89 Live Deploy 88 Deploy 87 Deploy 86 @philhawksworth Deploy 86

Slide 32

Slide 32

Deploy 89 Deploy 88 Deploy 87 Live Deploy 86 @philhawksworth Deploy 86

Slide 33

Slide 33

Deploys are immutable and atomic @philhawksworth

Slide 34

Slide 34

JAMSTACK ASSETS ON CDN BUILD AHEAD OF TIME @philhawksworth CODE & CONTENT

Slide 35

Slide 35

Challenge Very large sites @philhawksworth

Slide 36

Slide 36

Challenge User generated content @philhawksworth

Slide 37

Slide 37

We reach a ceiling @philhawksworth unsplash.com/photos/unIuWzRFaSM

Slide 38

Slide 38

2 Breaking the ceiling @philhawksworth

Slide 39

Slide 39

vlolly.net findthat.at/lollytricks @philhawksworth

Slide 40

Slide 40

Thousands of unique pages and user-generated content @philhawksworth

Slide 41

Slide 41

New content rendered on-demand as a fallback @philhawksworth

Slide 42

Slide 42

New content rendered on-demand as a fallback @philhawksworth

Slide 43

Slide 43

New content rendered on-demand as a fallback @philhawksworth

Slide 44

Slide 44

@philhawksworth

Slide 45

Slide 45

vlolly.net/lolly/abc7A7VxP @philhawksworth

Slide 46

Slide 46

New content Rebuild and fill the gaps 404 Render page on demand @philhawksworth

Slide 47

Slide 47

functions/showLolly.js const faunadb = require(‘faunadb’); const pageTemplate = require(‘./lollyTemplate.js’); // setup and auth the Fauna DB client const q = faunadb.query; const client = new faunadb.Client({ secret: process.env.FAUNADB_SERVER_SECRET }); // Create a handler function to export const handler = async(event) =>” { // get the lolly ID from the request let lollyId = event.path.split(“lolly/”)[1]; // find the lolly data in the DB client.query( q.Get(q.Match(q.Index(“lolly_by_path”), lollyId)) ).then((response) =>” { // if found, return a view return { statusCode: 200, headers: { “Content-Type”: “text/html”, }, body: pageTemplate(response.data) } }).catch((error) =>” { // not found or an error, send to the generic error page }); } // export the handler function exports.handler = handler; @philhawksworth const faunadb = require(‘faunadb’); const pageTemplate = require(‘./lollyTemplate.js’);

Slide 48

Slide 48

functions/showLolly.js const faunadb = require(‘faunadb’); const pageTemplate = require(‘./lollyTemplate.js’); // setup and auth the Fauna DB client const q = faunadb.query; const client = new faunadb.Client({ secret: process.env.FAUNADB_SERVER_SECRET }); // Create a handler function to export const handler = async(event) =>” { // get the lolly ID from the request let lollyId = event.path.split(“lolly/”)[1]; // find the lolly data in the DB client.query( q.Get(q.Match(q.Index(“lolly_by_path”), lollyId)) ).then((response) =>” { // if found, return a view return { statusCode: 200, headers: { “Content-Type”: “text/html”, }, body: pageTemplate(response.data) } }).catch((error) =>” { // not found or an error, send to the generic error page }); } // export the handler function exports.handler = handler; @philhawksworth // setup and auth the Fauna DB client const q = faunadb.query; const client = new faunadb.Client({ secret: process.env.FAUNADB_SERVER_SECRET });

Slide 49

Slide 49

functions/showLolly.js const faunadb = require(‘faunadb’); const pageTemplate = require(‘./lollyTemplate.js’); // setup and auth the Fauna DB client const q = faunadb.query; const client = new faunadb.Client({ secret: process.env.FAUNADB_SERVER_SECRET }); // Create a handler function to export const handler = async(event) =>” { // get the lolly ID from the request let lollyId = event.path.split(“lolly/”)[1]; // find the lolly data in the DB client.query( q.Get(q.Match(q.Index(“lolly_by_path”), lollyId)) ).then((response) =>” { // if found, return a view return { statusCode: 200, headers: { “Content-Type”: “text/html”, }, body: pageTemplate(response.data) } }).catch((error) =>” { // not found or an error, send to the generic error page }); } // export the handler function exports.handler = handler; @philhawksworth // Create a handler function to export const handler = async(event) =>” { // get the lolly ID from the request let lollyId = event.path.split(“lolly/”)[1];

Slide 50

Slide 50

functions/showLolly.js const faunadb = require(‘faunadb’); const pageTemplate = require(‘./lollyTemplate.js’); // setup and auth the Fauna DB client const q = faunadb.query; const client = new faunadb.Client({ secret: process.env.FAUNADB_SERVER_SECRET }); // Create a handler function to export const handler = async(event) =>” { // get the lolly ID from the request let lollyId = event.path.split(“lolly/”)[1]; // find the lolly data in the DB client.query( q.Get(q.Match(q.Index(“lolly_by_path”), lollyId)) ).then((response) =>” { // if found, return a view return { statusCode: 200, headers: { “Content-Type”: “text/html”, }, body: pageTemplate(response.data) } }).catch((error) =>” { // not found or an error, send to the generic error page }); } // export the handler function exports.handler = handler; @philhawksworth // find the lolly data in the DB client.query( q.Get(q.Match(q.Index(“lolly_by_path”), lollyId)) ).then((response) =>” {

Slide 51

Slide 51

functions/showLolly.js const faunadb = require(‘faunadb’); const pageTemplate = require(‘./lollyTemplate.js’); // setup and auth the Fauna DB client const q = faunadb.query; const client = new faunadb.Client({ secret: process.env.FAUNADB_SERVER_SECRET }); // Create a handler function to export const handler = async(event) =>” { // get the lolly ID from the request let lollyId = event.path.split(“lolly/”)[1]; // find the lolly data in the DB client.query( q.Get(q.Match(q.Index(“lolly_by_path”), lollyId)) ).then((response) =>” { // if found, return a view return { statusCode: 200, headers: { “Content-Type”: “text/html”, }, body: pageTemplate(response.data) } }).catch((error) =>” { // not found or an error, send to the generic error page }); } // export the handler function exports.handler = handler; @philhawksworth // if found, return a view return { statusCode: 200, headers: { “Content-Type”: “text/html”, }, body: pageTemplate(response.data) }

Slide 52

Slide 52

It’s JavaScript Add this to any framework or site @philhawksworth

Slide 53

Slide 53

netlify.toml # lolly page requests will be rendered and persisted on demand [[redirects]] from = “/lolly/*$” to = “/.netlify/functions/showLolly” status = 200

@philhawksworth

Slide 54

Slide 54

vlolly.net/lolly/abc7A7VxP @philhawksworth

Slide 55

Slide 55

New content Rebuild and fill the gaps 404 Render page on demand Lots of pages Cache the builds @philhawksworth

Slide 56

Slide 56

vlolly.net/popsicle/abc7A7VxP @philhawksworth

Slide 57

Slide 57

( ) Making-fun-of-myenglish-accent-driven development @philhawksworth

Slide 58

Slide 58

It’s JavaScript Add this to any framework or site @philhawksworth

Slide 59

Slide 59

But… More pages means longer builds @philhawksworth We could have better cache characteristics Complexity starting to muddy the mental model I’m inventing things rather than using a formal pattern

Slide 60

Slide 60

But behold, there are now Numerous rendering approaches @philhawksworth

Slide 61

Slide 61

ISR @philhawksworth DSG DPR

Slide 62

Slide 62

3 Removing the ceiling @philhawksworth

Slide 63

Slide 63

ISR @philhawksworth DSG DPR

Slide 64

Slide 64

Without undoing the benefits of Jamstack @philhawksworth

Slide 65

Slide 65

@philhawksworth

Slide 66

Slide 66

@philhawksworth

Slide 67

Slide 67

Without migrating to another framework @philhawksworth

Slide 68

Slide 68

DPR Distributed Persistent Rendering @philhawksworth

Slide 69

Slide 69

DPR Distributed Persistent Rendering @philhawksworth

Slide 70

Slide 70

@philhawksworth

Slide 71

Slide 71

vlolly.net/lolly/abc7A7VxP @philhawksworth

Slide 72

Slide 72

functions/showLolly.js const faunadb = require(‘faunadb’); const pageTemplate = require(‘./lollyTemplate.js’); const { builder } = require(‘@netlify/functions’); // setup and auth the Fauna DB client const q = faunadb.query; const client = new faunadb.Client({ secret: process.env.FAUNADB_SERVER_SECRET }); // Create a handler function to export const handler = async(event) =>” { // get the lolly ID from the request let lollyId = event.path.split(“lolly/”)[1]; // find the lolly data in the DB client.query( q.Get(q.Match(q.Index(“lolly_by_path”), lollyId)) ).then((response) =>” { // if found, return a view return { statusCode: 200, headers: { “Content-Type”: “text/html”, }, body: pageTemplate(response.data) } }).catch((error) =>” { // not found or an error, send to the generic error page }); } // export the handler function exports.handler = builder(handler); @philhawksworth const { builder } = require(‘@netlify/functions’);

Slide 73

Slide 73

functions/showLolly.js const faunadb = require(‘faunadb’); const pageTemplate = require(‘./lollyTemplate.js’); const { builder } = require(‘@netlify/functions’); // setup and auth the Fauna DB client const q = faunadb.query; const client = new faunadb.Client({ secret: process.env.FAUNADB_SERVER_SECRET }); // Create a handler function to export const handler = async(event) =>” { // get the lolly ID from the request let lollyId = event.path.split(“lolly/”)[1]; // find the lolly data in the DB client.query( q.Get(q.Match(q.Index(“lolly_by_path”), lollyId)) ).then((response) =>” { // if found, return a view return { statusCode: 200, headers: { “Content-Type”: “text/html”, }, body: pageTemplate(response.data) } }).catch((error) =>” { // not found or an error, send to the generic error page }); } // export the handler function exports.handler = builder(handler); @philhawksworth // export the handler function exports.handler = builder(handler);

Slide 74

Slide 74

Here endeth the refactoring @philhawksworth

Slide 75

Slide 75

Generated whenever we deploy @philhawksworth Added to the deploy when first requested

Slide 76

Slide 76

New content Render into cache on demand 404 Nope. Page delivered from deploy Lots of pages Fewer pages built in deploys @philhawksworth

Slide 77

Slide 77

tl;dr: I deleted some stuff and then added 2 lines @philhawksworth

Slide 78

Slide 78

This additive approach uses on-demand builders @philhawksworth

Slide 79

Slide 79

It can be augment a site built with any framework @philhawksworth

Slide 80

Slide 80

It doesn’t compromise the core benefits of Jamstack @philhawksworth

Slide 81

Slide 81

Next? @philhawksworth

Slide 82

Slide 82

Links Run Gatsby 4 with DSG and SSR on Netlify https://www.netlify.com/blog/2021/09/16/run-gatsby-4-with-dsg-and-ssr-on-netlify-today/ Keeping things fresh with stale-while-revalidate https://web.dev/stale-while-revalidate/ Use Next.js 12 on Netlify https://www.netlify.com/blog/2021/10/27/use-next.js-12-on-netlify/ Eleventy Serverless https://www.11ty.dev/docs/plugins/serverless/ Distributed Persistent Rendering: A new Jamstack approach for faster builds https://findthat.at/dpr Distributed Persistent Rendering RFC https://findthat.at/dpr/rfc

Slide 83

Slide 83

Thank you @ philhawksworth