Treat your users right with Segmented Rendering

Bookmark
Slides

If you think that static rendering is limited to generic and public content that is the same for every user of your website, this talk is for you. Segmented Rendering is a new pattern for the Jamstack that lets you personalize content, statically, without any sort of client-side rendering or per-request Server-Side Rendering. Get the best possible performances for use cases such as theming, internationalization, A/B testing, multi-tenancy, and start treating your users right!

by



Transcription


Hello everybody and welcome to my talk, treat your users right with segmented rendering. First, let me introduce myself. My name is Eric Burel, I'm the founder of LBKE, a small company in Montpellier, France. I'm a web developer, but I'm also a consultant in public funding for research to business. I'm the maintainer of VulkanJS, a framework you might know if you come from the Meteor JS ecosystem, and that is now running on Next.js and GraphQL. I'm a member of the DevoGraphics Collective created by Sacha Gref, who runs the state of JS, CSS, and GraphQL surveys. I'm in particular in charge of the survey form where you actually fill the survey. And I'm teaching Next.js at Human Coders, a teaching company in France. You can meet me on Twitter or on Medium where I publish a few articles on various subjects, namely Next.js and GraphQL. I'm going to talk about personalization to explain what is the segment in segmented rendering. What is web personalization? Let's take an example. Picking a theme is the most basic example and the most visible example of web personalization. Let's say that in my application I have three possible themes, fire, water, and grass. And each theme will change the display of the application. To implement theming, most often I will use a cookie. This is very common for web personalization to store some specific information about the user in a cookie. Why? Because they are automatically sent alongside every request, and if they are not HTTP-only, they are also available to the JavaScript code. They are very useful to keep information to tell who is the user, what's their segment, in which category they belong, for instance. So here I could have a cookie named starter with three possible values, fire, water, and grass. And depending on the value, of course, I pick a different theme in the application. But it's not the sole use case. There are many use cases of web personalization. We have this theming use case, but also internationalization. It's a case of personalization that we sometimes forget, but when we change the language of an application to adapt the language of the user, we are doing what we call personalization. We are optimizing the website and the user experience, depending on their characteristics, the language we think they talk or the language they selected in a menu. This is personalization. Having paid versus free content is also personalization, especially, for instance, if you have only the beginning of an article before hitting a paywall. This is personalization in the sense that unpaid users have a different experience from paid users. Paid users have their content, and unpaid users are invited to get a subscription. They have a different experience, a personalized experience, and they belong to different segments, guest users, paid users. A-B testing is also a very important use case, maybe more advanced. But if you make a lot of money out of a website, you probably have set up already A-B testing because it helps you test new versions of your website incrementally without affecting your whole user database. So most often, we have two segments that are actually named buckets in A-B testing environment, A and B, and you have two different of the website, A and B, depending on the segment. Let's come back at our use case, rendering the right thing. Let's talk about Next.js because Next.js is a very interesting framework when it comes to rendering because it embeds three ways of rendering application. The first one is client-side rendering. This is the most common way to render content in modern JavaScript application in software as a service because it's very dynamic. It happens in the user's browser, so you can do a lot of computation there. It's just using client-side JavaScript. And to get data to fill the content of the page, it will be used to process built-in like fetch or libraries that are themselves built on top of this built-in like SWR in the Vercel ecosystem. Server-side rendering is the polar opposite. You are doing everything on the server for each request. This is closer, for instance, to how PHP works. When you do a request, it will hit a page that is a template. The server will fetch some data from the database at this point and will fill the template with the right values, and you end up with your under a page that is then sent as is to the browser as pure HTML. And you have static seed generation, which sits in between, which is basically just server-side rendering, but not per request, but computed at build time. It's similar in a way to just writing HTML. You could just write the page. But of course, it would be quite dumb to write the content directly. So static seed generation lets you do this at scale. You have the same page, and static seed generator is able to generate multiple versions depending on some parameters you input, like the language, the theme, and so on. They divide into two categories. The first one would be the dynamic ways to render a content. Client-side rendering and per-request server-side rendering are dynamic because server-side rendering happens for each request, so the data can be updated every time the user refresh the browser, at least. Client-side rendering is even more dynamic because it can be interactive. Since it operates on the user's browsers, it can do computation anytime. It doesn't even have to talk with the server, so most often you have this pattern, this jamstack pattern of calling APIs to get new data and rendering everything client-side. This is a very jamstack-ish way of doing things. Static seed generation is well static. Because things are computed at build time, they cannot evolve for each request because you don't have the request yet. No one requested your server. You are just building it and deploying your server at this point. But it's faster because things are already computed and stored in a cache, so there is no new computation. The server just has to serve the computed HTML. And what will you use for personalization? You might be tempted to say dynamic patterns because they can adapt to the request, because they can happen in the user's browser, so literally on their machine. It's easier to adapt and to personalize for a precise user, of course. However, they are slower. They need more computations. So you also have the choice to use static seed generation, but how? It's faster, so it's tempting, but how? Again, since it doesn't move, how do you do that? Again, using systemically the dynamic and slower solution leads to an issue that I call the rich guest, poor customer issue. You will use the faster static renders only for public content, for content that is available to anyone surfing the web, so unpaid visitors, guest users, as well as connected users. And you will use the slower dynamic renders for personalized content. You will use CPU time from the users if you do client-side rendering. You will have slower computation on the backend if you do pair request server-side rendering. So basically your customers that are authenticated, that receive personalized content based on their own personal data, have the poorest pattern, the slowest pattern. They are poor customers, while the guests that provide most often less value on the website because they are not connected, you don't know them, you can't target them, they are not customers. They have the best experience. They are rich guests. This is the rich guest, poor customer issue. And segmented rendering is a way to reduce this issue by enabling static renders for personalized content to have rich customers. So let's talk about personalized staticness using segmented rendering patterns. Let's do a first try. Let's try to use the URL to personalize the content statically. It's very easy because most static seed generators rely on the URL, be it Next.js or Gatsby or Astro and so on, you give them URLs to render and depending on the parameter, they will fetch the correct data and render the page accordingly. That's how they work. So the easiest implementation is to have one URL per segment, one URL per theme choice for users. So users that picked the fire theme will have one version, water theme one version, and grass theme one version. But it leads to some issues. First, the user can change the URL. It's okay for a theme. It's okay for a language. It's even a feature in a way for a language. You can change the language of a page by just changing the URL. But it's a problem if you start doing personalization with paid content. You don't want users to change the URL to suddenly become paid users. You'd rather want them to pay for something. Then the user can see the parameter. Again, not a problem for a language. It's even a good thing for a language because it allows sharing the URL to other people speaking the same language with the language in the URL so they get the right content. Yes, but for an A-B test, that's a huge issue. Users shouldn't be able to tell in which bucket they are. Otherwise, you are drastically biasing the A-B test. It leads to a lot of issues and the data are just not valid anymore. So they shouldn't see the parameter in this case. And finally, the URL can become long and ugly. If you have a lot of parameters, you will end up with the language, the dark or light theme, the tenant, the company, the organization, the users belongs to. If you are doing business to business, it's very common to have multi-tenancy, to have multiple companies using your application with a lot of users per company. You will have the A-B test bucket, which is not good, paid content and so on. You have very long and ugly URLs. So to solve this issue, this limitation, let's introduce a new piece in our architecture. Let's introduce a simple, lightweight proxy server. The role of this proxy server will be to take the HTTP request that hits the server and to rewrite the URL depending on the segment. I insist on the word rewrite the URL and not redirect the URL. A URL rewrite is not visible to the end user. And that's the key difference because it solves all our issues with the URL at once. The user cannot see the parameter anymore. The user cannot change the parameter anymore because it's protected by the server, it's computed server side. So the user cannot hack it by just changing browser values. And finally, the fact that the URL is long and ugly is not a problem anymore because the user do not see it anymore. You can reach the limitation of 255 characters for URL, but in practice, this will never actually happen. And even then, we have more, even more advanced pattern to fix that. It's linked in the resource, it's called the mega-param patterns. Basically, you can encode the parameters to compress them. So there is solution even for this slight limitation. So it solves our issue. We have an HTTP request with the cookie fire, and we turn that into a URL parameter fire, which is compatible with how existing frameworks works because Gatsby, Nexus, will be able to statically render content as long as the URL for each variation is unique. This is what we call segmented rendering. This is a recipe with two ingredients, having one URL per segment. This is just how static rendering works. If you generate multiple variations of your website statically, you already do this. But the trick is to use a tiny URL rewriting server to redirect to the right URL depending on other parameters of the request, like the cookies or the headers. If I take another example with internationalization, you could use the accept language header and rewrite the URL to the right language. This would remove the need from having an explicit language parameter. You still need a way for users to change the language, but you could do that using a cookie that is invisible instead of using a URL parameter which is visible and in some scenarios might be ugly. This might sound a bit abstract. Now let's take a look at an implementation with Next.js, which is very interesting because it allows Next.js to embed the concept of edge middlewares that are exactly that, the proxy server that we need to implement segmented rendering. They provide that out of the box. Here is our application using segmented rendering. It's a Next.js application that lets me pick a theme, fire, water, or grass. So let's pick the fire theme. You will see that it changes the color as expected. The interesting part is that it doesn't alter the URL first. So if I go to application and check the cookies for this application, I will see that I'm relaying on the theme cookie named fire. Instead of using a root parameter, I'm using a cookie, which is the right way to do personalization in many scenarios. Second interesting fact is that if I reload the page, I will get the segmented version already. Of course, this is all statically rendered. So for instance, if I go to the network and I check the response of the server for the HTML request, I will see my content with the right theme. So here I've not configured the CSS for server-side rendering, but I can see the fire theme in the HTML already. So I'm indeed using static rendering. This is computed in advanced server-side. There is no client-side computation happening. Regarding the implementation, the first piece is just a page with static seed generation, as you would find in any Next.js application using getStaticPath, getStaticProps, as usual. I generate different versions of the page depending on the theme parameter. Notice that I'm using incremental static regeneration, which is just a pattern in Next to handle the case where I have a lot of themes. For instance, here I'm not pre-rendering the green starter because maybe this is less requested by people, and I don't want to have a very long build time, so it will be computed only on the first request. So we have a lot of flexibility with this approach. And then of course, I render my page based on this parameter. This is very, very usual if you use Next.js or any other static rendering framework, you will be used to this approach. But the most important part is the middleware. This is my proxy server, and when I mean tiny and lightweight, I'm not joking. It's 25 lines with a lot of comments. What it does? It reads the cookie. If it's a valid theme, it will write the URL. As simple as that. This is how I get segmented rendering, so static rendering with personalization without using a root parameter. This is invisible to the user, and this is secure because it's happening server-side. I can't let the user pick a theme that does not exist, for instance. They can only select fire, water, grass, or none. Otherwise, I throw an error. This is secure. It's great for paid content, A-B test, for instance. Finally, just a detail, but that's optional. I've added some logic to switch the theme based on a cookie using an API root. So I'm using the set-cookie HTTP header, but you could do the same client-side, of course. Let's get back to my slides. So now you have an example of the implementation, and in Vercel, you might have a different wording. They won't call that segmented rendering. They will call that edge personalization. It's actually the Vercel translation of this pattern. This is the same thing, except that Vercel is allowing you to have segmented rendering at the edge using its edge network, but it's basically the same idea. Short conclusion, what's the future of server-side rendering? I think the future of server-side rendering is understanding that it's all about caching. Static is server-side rendering cached at build time. Per-request server-side rendering is just a cache miss in the same pattern. The cache key is not just the URL, but the full request. This is the idea behind segmented rendering, is to use the request and not just the URL. And the value is, of course, the rendered HTML or the data. It depends on the pattern. Gatsby deferred static rendering, for instance, is untying both, and Next.js is always considering them as a whole. They are very tightly related in Next. But either way, that's the cache value. So my take is that in the future, we will have a unified API for server rendering that encompasses static per-request and everything in between. And until that, we have segmented rendering. Thank you for your attention, and see you later.
21 min
24 Oct, 2022

Check out more articles and videos

We constantly think of articles and videos that might spark Git people interest / skill us up or help building a stellar career

Workshops on related topic