Adding ETag Support to Remix

Learn what an ETag and If-None-Match are, why they're beneficial to your users, and how to implement them in your Remix application quickly and easily with remix-etag.
An iMessage conversation. Person A: What's the weather? Person B: Partly cloudy, 73º, winds from the NNW at 2 mph. Be sure to write that down. Person A (an hour later): What's the weather, or is it the same as last time? Person B: It's the same.

Before we begin, let's look at a text message exchange between myself and Barack where he asked me what the weather was like. He asked again a short time later and this time I only needed to respond with "Same as last time" because the information hadn't changed between messages. That's basically the idea behind ETag.

Obviously there's a little more to it than that, but the concept is the same: don't repeat information that the asking party already has. Let's dive a little deeper into the details.

What's an ETag and why do you need it?

Let me take a few minutes to explain he mechanics behind ETag and why you might want it support for it on your website. I understand that many Remix developers come from a strict front-end background and full-stack techniques may be new to you. If you are full-stack, then let this act as a refresher (or you can skip ahead to the good stuff below).

A sample session without ETags

When a user visits a website, this is a typical exchange between the web browser and the server.

Step 1

The browser sends a request to the server.

GET / HTTP/1.1
Host: example.com

The first line specifies that we are using the GET method and requesting the root of the website (i.e. /). It also indicates the version of the HTTP protocol we are using.

In all of our examples, we are using the GET method against the path / on example.com.

Step 2

The server renders the page and sends the content back to the browser.

HTTP/1.1 200 OK
Content-Type: text/html

<html><body><h1>Hello World</h1></body></html>

The first line of the response contains the HTTP version number and status code. A 200 (OK) indicates that the request was successful.

Any headers that the server wants to send back to the browser are sent after the status code. If there is a body, there is a blank line, followed by the body.

This same exchange is repeated for every request.

A sample session with ETags

Not let's take a look at the same HTTP exchange, but with an added ETag header.

Step 1

The browser sends a request to the server. This is identical to the previous example.

GET / HTTP/1.1
Host: example.com

Step 2

The server sends the content back to the browser, but this time, an ETag header is added to the response. The value represents a hash of the response body (or some other identifier). The browser saves the hash value in its cache.

HTTP/1.1 200 OK
Content-Type: text/html
ETag: "c8d9f8c7f8d9f8c7"

<html><body><h1>Hello World</h1></body></html>

HTTP headers keys are case-insensitive. So even though it's commonly written as ETag, it could be written as etag or even EtAg.

Step 3

The next time the browser visits the same URL it includes an If-None-Match header with the cached hash value from the previous visit.

GET / HTTP/1.1
Host: example.com
If-None-Match: "c8d9f8c7f8d9f8c7"

Step 4

The server checks if the hash value matches the hash value in the If-None-Match header. If the body hasn't changed, then the hashes should match. If this is the case the server sends a 304 (Not Modified) response but does not send a body. This instructs the browser to use the cached content.

HTTP/1.1 304 NOT MODIFIED
Content-Type: text/html
ETag: "c8d9f8c7f8d9f8c7"

As you can see, sending an empty body is much more efficient that sending the entire body — maybe not for our "Hello World" example, but clearly if the body was sized like a typical web page. This means much faster TFP (Time to First Paint) and much less network traffic. The benefits for mobile devices, where bandwidth is often limited, are very apparent.

ETag/If-None-Match does not eliminate the need for the server to re-render subsequent requests, but it does allow the server to skip sending the duplicate rendered content back to the browser, saving bandwidth and time.

Let's get to the good stuff

Now that you see how beneficial this is for your users, let's see how to implement ETags in your Remix application.

As with many aspects of modern JavaScript web development, someone has already done much of the heavy lifting for us. There's a package called remix-etag that we can drop in with just a few lines of code.

Psst... That someone was me. 😉

First, install the package.

npm i remix-etag

Once installed, edit the file app/entry.server.tsx and add lines 4 and 22, and change line 18. Here's what your code looks like now.

entry.server.tsx
import { renderToString } from "react-dom/server";
import { RemixServer } from "remix";
import type { EntryContext } from "remix";
import { etag } from 'remix-etag';

export default function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext
) {
  let markup = renderToString(
    <RemixServer context={remixContext} url={request.url} />
  );

  responseHeaders.set("Content-Type", "text/html");

  const response new Response("<!DOCTYPE html>" + markup, {
    status: responseStatusCode,
    headers: responseHeaders,
  });
  return etag({ request, response });
}

That is the entirety of the changes needed to use ETags in your application.

Try it out

First, open a new tab in your browser. Next, open Developer Tools and go to the Network tab. Finally, navigate your app at http://localhost:3000.

As this is the first request, there will not be an If-None-Match request header, so the server responds with a status of 200 OK. Inspect the response headers and you'll see that there is an ETag header. Note that the value will be different from what is shown here.

Chrome Dev Tools screenshot showing a 200 OK response

Now refresh the page. In the request, you should see an If-None-Match request header with the value of the previous ETag response header. The response status should be a 304 (Not Modified).

Chrome Dev Tools screenshot showing a 304 Not Modified response

Congratulations! You've implemented an ETag.

Tip of the iceberg

This article just skims the surface of a healthy caching performance strategy remix-etag is a great starting point. It includes a default Cache-Control header that should work for most applications. Your mileage may vary, so please consult the docs on the GitHub repo.

You can also add an ETag to your handleDataRequest function to cache the response of a data request. More on that in a later article.

ETags can reduce the amount of network traffic that is required to render a page. They can be implemented on your site with the remix-etag package in just a few lines of code.

So what are you waiting for? Add support for ETag to your site now then go ahead and ship it — yes, even on a Partly cloudy, 73º Friday.