Fast Response Times with ETags
- cache
- etag
- javascript
- performance
Caching basics
When creating a web application, a common performance optimization is to save many of the files that are required to run your application on users’ devices so that the next time they visit the page it will load faster. This is called caching, you can open your browser cache and see all of the different assets that have been saved to your computer.
For each Response
sent from your application, you can set headers
in the Response
that tell the browser to cache the request in various ways.
Linked assets
Many assets such as JS and CSS can be cached for a long time since they have a unique hashed filenames that are generated during the build. These assets are requested indirectly by the client—for example via <link>
or <script>
tags—after the initial HTML response is received.
A linked stylesheet might look like this.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>etag</title>
<link
rel="stylesheet"
crossorigin
href="/_immutable/assets/main.Cs4aW6Ww.css"
/>
</head>
<body>
...
</body>
</html>
Each time the CSS file is updated, a new hash will be generated for the file using its content. The client will always make the request to the correct, newest asset, since the link is provided in the HTML.
These assets are often all output during the build process into an immutable/
directory and served with immutable cache headers.
{ "Cache-Control": "public, immutable, max-age=31536000" }
By doing this, users only ever have to download these assets once, they are saved to their computer for up to a year and loaded from the cache in subsequent requests.
HTML
One response that generally cannot have a hashed URL is the HTML response. Users need to be able to navigate to the same URL and always receive the most up to date HTML page. You wouldn’t want to have to send a new link to your users every time you updated your website!
This response can still be sped up by using ETags (entity tags). Etags allow you to check if the file has been modified since the last time it was requested. If it hasn’t, you can just send a small response saying that it is the same as last time, instead of sending the entire response again.
ETag Tutorial
In this tutorial, I’ll show you how to implement ETags to speed up an HTML response. I’m going to be using domco, but this example can be used in any server-side JavaScript framework that supports setting custom headers on a response.
Run npm create domco@latest
to get a similar template as the one in this tutorial. The final example can be found in this repository as well.
Here’s an example of sending a HTML response without an ETag.
import { html } from "client:page";
import type { Handler } from "domco";
export const handler: Handler = (req) => {
return new Response(html, {
headers: {
"Content-Type": "text/html",
},
});
};
Currently, this response requires Response.body
—the html
—to be sent over the network with each request.
Creating a hash
The first step to utilizing ETags is to find a hashing algorithm to use to create the tag and to check if the content has changed.
Since ETags will often be generated on every request, it’s important to choose an efficient method to create a unique, consistent hash. The hash must produce the same output, given the same input, in order to understand if the content has changed since the last time the hash was created. So methods like using the current time will not work if the ETag is generated for each request, since the time will be different each time the function is run.
Framework examples
Here’s how a variety of popular frameworks generate ETags, I’ve linked to the code where the ETag is created. All of these methods have proved to work well in widespread production use.
- sirv uses a combination of the file size and the last modified time. This works great for static assets, but it won’t work if you are generating a string of HTML dynamically since there is no file to read.
- Next.js and Astro both use the same FNV-1a algorithm from this fnv-plus project.
- SvelteKit uses a JavaScript implementation of the DJB2 algorithm.
- Hono uses the SHA1 algorithm via the Web Crypto API. The etag package (used in Express) also uses SHA1 via the Node Crypto API.
DJB2
I’m going to use the DJB2 algorithm from SvelteKit for this tutorial, I modified it to use TypeScript.
Sending the ETag to the client
Next, we can create an ETag using the HTML string, and send it to the client in a response header along with the HTML.
import { html } from "client:page";
import type { Handler } from "domco";
export const handler: Handler = (req) => {
// surround the hash with double quotes
const eTag = `"${djb2(html)}"`;
return new Response(html, {
headers: {
// other headers should remain the same
"Content-Type": "text/html",
// send it as the "Etag" header
ETag: eTag,
},
});
};
Now, the client’s browser will automatically send this hash back in subsequent requests in the If-None-Match
header.
import { html } from "client:page";
import type { Handler } from "domco";
export const handler: Handler = (req) => {
// hash is sent back in this header
console.log(req.headers.get("If-None-Match")); // ex: "1wnhp22"
const eTag = `"${djb2(html)}"`;
return new Response(html, {
headers: {
"Content-Type": "text/html",
ETag: eTag,
},
});
};
Check for changes
You can check if the content is still the same as what the user has by comparing their hash, to the one you are currently generating.
If the content hasn’t been modified, we can send null
instead of sending the content again, with a 304
status code to tell the client that the response is Not Modified
.
import { html } from "client:page";
import type { Handler } from "domco";
export const handler: Handler = (req) => {
const eTag = `"${djb2(html)}"`;
// Check if the hash sent from the client matches the hash
// generated. If it does, the content hasn't been modified.
const notModified = eTag === req.headers.get("If-None-Match");
// send `null` instead of the html in the body
return new Response(notModified ? null : html, {
// change the status to 304 - not modified
status: notModified ? 304 : 200,
headers: {
"Content-Type": "text/html",
ETag: eTag,
},
});
};
Now when the user refreshes the page, a much smaller response will be sent instead of the entire HTML page being sent over the network again.
Conclusion
I hope this provides some insight into how you can speed up your web application with different caching techniques. For large HTML pages or for slow connections, ETags can really speed up the loading time of a website.
Thanks for reading!