Serve home page for first time users from the edge, and still avoid redirects for logged in users

recipe-exchange

#1

Typically, when a user visits the home page, the request has to reach the origin server to validate the cookie and decide whether to server the static home page or user’s personalized dashboard.

For repeat users, there are many optimizations like browser cache, service workers, etc. For the first time user, the request having to reach the origin means, it will be one of the slowest request.

I found a small trick to optimize for the first time users (and search crawlers).

Serve the home page for first time users from the cache and still avoid redirects for logged in users.

How to serve home page from CDN for first time users

  1. Check if the user has any cookie. (For Express users, it is connect.sid)

  2. If no cookie, serve the static html page (set appropriate Page Rules to cache the static html page).

  3. Else, forward the request to the origin server to validate the cookie and respond correctly.

     if (event.request.headers.has('Cookie')
         && event.request.headers.get('Cookie').indexOf('connect.sid')>=0) {
       // Existing user.  
       event.respondWith(fetch(event.request));
     } else {
       // You may need to set the Page Rule to cache .html files
       var newUrl = new URL('/static/homepage.html', event.request.url);
    
       event.respondWith(fetch(newUrl.toString())
           .then(function(response){
             // Fix some headers
    
             var init = {
                 status:     response.status,
                 statusText: response.statusText,
                 headers:    {
                   'Content-Type': 'text/html;charset=UTF-8'
    
                 }
             };
    
             for (var pair of response.headers.entries()) {
               if (pair[0] !== 'expires'
                   && pair[0] !== 'cache-control') {
                 init.headers[pair[0]] = pair[1];
               }
             }
    
             return response.text().then(function(body){
                 return new Response(body, init);
             });
         }
    
       ));
     }
    

Correct me if I missed something.


#2

This is a great use case @jayaprabhakar!

One comment: I think you can clean up the header re-writing code a bit, and improve performance slightly at the same time. Specifically, this code here:

         // Fix some headers

         var init = {
             status:     response.status,
             statusText: response.statusText,
             headers:    {
               'Content-Type': 'text/html;charset=UTF-8'

             }
         };

         for (var pair of response.headers.entries()) {
           if (pair[0] !== 'expires'
               && pair[0] !== 'cache-control') {
             init.headers[pair[0]] = pair[1];
           }
         }

         return response.text().then(function(body){
             return new Response(body, init);
         });

could be rewritten as:

         response = new Response(response.body, response)
         response.headers.delete('expires')
         response.headers.delete('cache-control')
         return response

The key is to copy the Response object up front with new Response(response.body, response). That has the following nice benefits:

  1. The response’s Headers object becomes mutable, so you can use response.headers.delete() instead of the filtering logic in the for loop.

  2. The original Content-Type header gets propagated, so you don’t have to worry about setting it explicitly.

Lastly, I mentioned that this provides a performance benefit. Since response.body is a ReadableStream, the new Response object is constructed and returned before the body is read. This lets the worker runtime send the response headers to the client as soon as possible, then stream the body afterward. Using response.text().then(...), as originally written, forces the runtime to wait for the entire body to be received from cache before sending the first response headers to the client.

This is admittedly a small optimization, given that the file is coming from cache, but for a very large HTML file the difference could be measurable.

Thank you for sharing the script!

Harris


#3

Cool write-up. I guess the only way to do this without Workers is with a javascript reading cookies, and different logged-in home page like /dashboard/.


#4

That’s right. Without workers, the changes would be big.

  1. The CDN will send a cached homepage.
  2. The home page will block the rendering, and run some JavaScript.
  3. Check if there is a cookie, if not, then show the homepage.
  4. If there is a cookie, send a request to the server to get the necessary json and do a client side rendering. (This is as good as JavaScript redirect)

#5

In my mind, the home page would always stay the same (cached html). The logged-in page would be /dashboard/ or similar, non cached html. Javascript isn’t needed at all.
Similar to https://www.cloudflare.com/ vs https://dash.cloudflare.com/foo/ . It’s not uncommon.


#6

@jules I’m not sure if it’s common because that’s the way users want it or that’s the way developers built it.
Facebook serves personalized feeds screen when a user logs in.


#7

Thanks Harris. That’s clean.


#8

Maybe I’m getting old. I don’t like the “you want fries with that?” syndrome. Workers are the fries of CloudFlare. They aren’t free.

Having a “My Dashboard” button on a common super-cached home page linking to a non-cached smart page (logged in = show tools, not logged in = show login form) works just fine and is free.
It’s what CloudFlare does. Maybe they don’t want to pay for Workers either :rofl:

CloudFlare Pro used to be $5/m. That’s now $15… and the fries are $5. That’s 4x the cost with fries!