Boost the UX of your React app with hash based content caching.
Tags: caching, performance, ngnix, react
Published: 2019-08-04 15:38:44

---
title: Boost the UX of your React app with hash based content caching.
published: true
description: This is a guide on an nginx hash based strategy for content caching, which is very useful for single page JavaScript apps.
tags: caching, performance, ngnix, react
---

## What problem are we trying to solve?
Say you have a JavaScript app that gets served up at `http://mysite.com/js/myapp.js`. A typical performance optimization is to tell the browser to cache `myapp.js` so that the user doesn't have to re-download the asset every time they use the app. If you practice continuous delivery, the problem you run into is delivering new app updates. If `myapp.js` is cached, the user won't get the new updates until either a) they clear their cache or b) the [max-age](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#max-age) expires.

*From the google dev docs:*
> Ideally, you should aim to cache as many responses as possible on the client for the longest possible period, and provide validation tokens for each response to enable efficient revalidation.


What we're going to do in this guide is we're going to come up with a way to cache our application assets for the longest possible time: FOREVER! Well sort of.. we are going to be using a hash based content caching strategy, which the [google dev docs](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#defining_optimal_cache-control_policy) mentions it gives you the best of both worlds: client-side caching and quick updates.


### Getting started with create-react-app
So to get started, we are going to use good ole [create react app](https://github.com/facebook/create-react-app) to quickly standup a new single page application.

Let's create a new app, `create-react-app content-cache`

So in a new directory, `~/code`, lets run this:

```
npx create-react-app content-cache
cd content-cache
```

So now you'll have a new app setup in `~/code/content-cache` and you should now be in the `content-cache` directory.

Now we can run `npm run build` which will output all the assets for your app in `./build`. With these assets now available, let's take a look at serving these with nginx.

### nginx + docker = yayyyyyy
Let's go ahead and create a new file, `touch ~/code/content-cache/Dockerfile` with the following contents:

```
FROM nginx:1.13-alpine

RUN apk add --no-cache bash curl

COPY nginx/ /

CMD ["/docker-entrypoint.sh", "nginx", "-g", "daemon off;"]

EXPOSE 8080

COPY build/static/ /usr/share/nginx/html/

COPY package.json /

```
You'll notice we are missing a few things:
* The `nginx/`folder being copied.
* The `docker-entrypoint.sh` script.

Let's go ahead and add those now.

Create a new directory, `mkdir -p ~/code/content-cache/nginx/etc/nginx` and then create a new file `touch ~/code/content-cache/nginx/etc/nginx/nginx.conf`.

Then open up the file and copy the following contents into it:
```
user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    sendfile        on;

    keepalive_timeout  65;

    gzip  on;
    gzip_types text/plain application/xml application/javascript text/css;

    include /etc/nginx/conf.d/*.conf;
}
```

Most of this is boilerplate nginx config, so I am not going to spend time explaining it, you can learn more from the [nginx docs](https://docs.nginx.com/nginx/admin-guide/web-server). Just note that we are including `/etc/nginx/conf.d/*.conf`, which includes the `default.conf` file, we'll be creating next.


Let's go ahead and create the file, `touch ~/code/content-cache/nginx/etc/nginx/conf.d/default.conf` and add the following contents to it:

```
server {
    listen       8080;

    # include the hash based content
    include /etc/nginx/conf.d/app/*.conf;

    location ~ ^/$ {
        # we are serving the app at `/a/`
        return 303 a/;
    }

    # serve other static assets
    location / {
        root   /usr/share/nginx/html;
        index  /index.html;
        try_files $uri /index.html;
        include /etc/nginx/conf.d/app/preload.headers;
    }
}
```

We are going to be serving the app at `/a/`, which is a strategy used to make life a bit easier when dealing with reverse proxying to backend APIs that live on the same domain.

So again, make note that we are including `/etc/nginx/conf.d/app/*.conf;`, which is our hash based content.

Now let's move on to creating a new file `touch ~/code/content-cache/nginx/docker-entrypoint.sh` where the magic happens.

Paste in the following contents:

```["shell"}
#!/usr/bin/env bash

mkdir -p /etc/nginx/conf.d/app
pushd /usr/share/nginx/html/js/ > /dev/null

APP_JS=/app/js/app.js
for js in main.*.*.js
do
    cat  > /etc/nginx/conf.d/app/js.conf <<EOF
location ~* ^/app/js/main.js([.]map)?\$ {
    expires off;
    add_header Cache-Control "no-cache";
    return 303 ${js}\$1;
}
location ~* ^/app/js/(main[.][a-z0-9][a-z0-9]*[.]js(?:[.]map)?)\$ {
    alias   /usr/share/nginx/html/js/\$1;
    expires max;
    add_header Cache-Control "public; immutable";
}
EOF
    APP_JS="/js/${js}"
    break;
done
RUNTIME_JS=/app/js/runtime.js
for js in runtime~main.*.js
do
    cat  > /etc/nginx/conf.d/app/js.conf <<EOF
location ~* ^/app/js/runtime~main.js([.]map)?\$ {
    expires off;
    add_header Cache-Control "no-cache";
    return 303 ${js}\$1;
}
location ~* ^/app/js/(runtime~main[.][a-z0-9][a-z0-9]*[.]js(?:[.]map)?)\$ {
    alias   /usr/share/nginx/html/js/\$1;
    expires max;
    add_header Cache-Control "public; immutable";
}
EOF
    RUNTIME_JS="/js/${js}"
    break;
done
VENDOR_JS=/app/js/vendor.js
for js in 2.*.*.js
do
    cat >> /etc/nginx/conf.d/app/js.conf <<EOF
location ~* ^/app/js/2[.]js([.]map)?\$ {
    expires off;
    add_header Cache-Control "no-cache";
    return 303 ${js}\$1;
}
location ~* ^/app/js/(2[.][a-z0-9][a-z0-9]*[.]js(?:[.]map)?)\$ {
    alias   /usr/share/nginx/html/js/\$1;
    expires max;
    add_header Cache-Control "public; immutable";
}
EOF
    VENDOR_JS="/js/${js}"
    break;
done

cd ../css
APP_CSS=/app/css/main.css
for css in main.*.*.css
do
    cat > /etc/nginx/conf.d/app/css.conf <<EOF
location ~* ^/app/css/main.css([.]map)?\$ {
    expires off;
    add_header Cache-Control "no-cache";
    return 303 ${css}\$1;
}
location ~* ^/app/css/(main[.][a-z0-9][a-z0-9]*[.]css(?:[.]map)?)\$ {
    alias   /usr/share/nginx/html/css/\$1;
    expires max;
    add_header Cache-Control "public; immutable";
}
EOF
    APP_CSS="/css/${css}"
done

cd ..

cat > /etc/nginx/conf.d/app/preload.headers <<EOF
add_header Cache-Control "public; must-revalidate";
add_header Link "<${APP_CSS}>; rel=preload; as=style; type=text/css; nopush";
add_header Link "<${VENDOR_JS}>; rel=preload; as=script; type=text/javascript; nopush";
add_header Link "<${APP_JS}>; rel=preload; as=script; type=text/javascript; nopush";
add_header X-Frame-Options "SAMEORIGIN" always;
EOF

cat > index.html <<EOF
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <meta http-equiv="X-UA-Compatible" content="ie=edge"/>
    <title>Create React app</title>
    <link href="${APP_CSS}" rel="stylesheet">
</head>
<body>
    <div id="root"></div>
    <script type="text/javascript" src="${VENDOR_JS}"></script>
    <script type="text/javascript" src="${APP_JS}"></script>
    <script type="text/javascript" src="${RUNTIME_JS}"></script>
</body>
</html>
EOF

popd > /dev/null

exec "$@"
```

Let's go ahead and break this down bit by bit.

```
mkdir -p /etc/nginx/conf.d/app
pushd /usr/share/nginx/html/js/ > /dev/null
```
This creates a new directory and uses `pushd` to cd into the `/usr/share/nginx/html/js` directory, while redirecting the output to `/dev/null` so the console doesn't get noisy.

```
APP_JS=/a/js/app.js
for js in main.*.*.js
do
    cat  > /etc/nginx/conf.d/app/js.conf <<EOF
```
This is a for loop, which iterates over the javascript files matching `main.*.*.js`, which is the pattern for our hashed content files. It then concatenates the location blocks into a file `/etc/nginx/conf.d/app/js.conf`.

```
location ~* ^/a/js/main.js([.]map)?\$ {
    expires off;
    add_header Cache-Control "no-cache";
    return 303 ${js}\$1;
}
```
We also are redirecting any requests to `/a/js/main.js` to the matching hash based filed. 

```
location ~* ^/a/js/(main[.][a-z0-9][a-z0-9]*[.]js(?:[.]map)?)\$ {
```
Also notice we are matching `.map` files so that we can load source map files as well.

```
    alias   /usr/share/nginx/html/js/\$1;
```
Then we are caching those hash based files to the MAX!
```
    expires max;
    add_header Cache-Control "public; immutable";
}
EOF
```
We then store the hashed asset file in `APP_JS` so we can use that later in the script.
```
    APP_JS="/js/${js}"
    break;
done
```

The next three for loops do the same as above, but for the different asset files. The runtime files `runtime~main.*.js`, the vendor files `2.*.*.js`, and the css files `main.*.*.css`.

Next we set our `preload.headers`.
```
cat > /etc/nginx/conf.d/app/preload.headers <<EOF
add_header Cache-Control "public; must-revalidate";
add_header Link "<${APP_CSS}>; rel=preload; as=style; type=text/css; nopush";
add_header Link "<${VENDOR_JS}>; rel=preload; as=script; type=text/javascript; nopush";
add_header Link "<${APP_JS}>; rel=preload; as=script; type=text/javascript; nopush";
add_header X-Frame-Options "SAMEORIGIN" always;
EOF
```
This tells the browser to preload these assets and store these files in the http cache. We specify `nopush` so that the server knows we only want to preload it for now.

We then dynamically create our `index.html` file:
```
cat > index.html <<EOF
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <meta http-equiv="X-UA-Compatible" content="ie=edge"/>
    <title>Create React app</title>
    <link href="${APP_CSS}" rel="stylesheet">
</head>
<body>
    <div id="root"></div>
    <script type="text/javascript" src="${VENDOR_JS}"></script>
```
We use the `APP_JS` variable to set the src for our js file. We also do the same for the other asset files.
```
    <script type="text/javascript" src="${APP_JS}"></script>
    <script type="text/javascript" src="${RUNTIME_JS}"></script>
</body>
</html>
EOF
```

Then we change back to the original directory with `popd > /dev/null` and then execute any args passed to this script `exec "$@"`. That's important otherwise the args after the `"/docker-entrypoint.sh"` will not work in our Dockerfile command: `CMD ["/docker-entrypoint.sh", "nginx", "-g", "daemon off;"]`.




### Let's see it all in action

We're going to build and run the Docker container.

In `~/code/content-cache`, run: 
* `chmod +x ./nginx/docker-entrypoint.sh` - make the script executable.
* `docker build -t nginx/test .` - this builds the image.
* `docker run --name="nginx-test-app" -p 8080:8080 nginx/test` - this runs the docker container.

Now that your app is running, head to [http://localhost:8080](http://localhost:8080). Open up the network tab in your dev tools and refresh the page. You should see the JavaScript and CSS assets should now be getting cached. It should look something like this:

![hashed content in network tab](https://thepracticaldev.s3.amazonaws.com/i/hywwhoqnzlaoeat5qeeb.png)

Looking good! Now let's do another build just to make sure it is working as intended. Kill the current docker container by pressing ctr + c and then running `docker rm nginx-test-app`.

Now run `npm run build && docker build -t nginx/test .` then  `docker run --name="nginx-test-app" -p 8080:8080 nginx/test`, open up [http://localhost:8080](http://localhost:8080) and checkout the network tab to confirm that the asset files are from the latest build.

![latest build assets in network tab](https://thepracticaldev.s3.amazonaws.com/i/hywwhoqnzlaoeat5qeeb.png)


🤘Now we're talking! At this point now, we have the best of both worlds setup: _*Max content caching and quick updates*_ when a new version of our app is deployed.

Feel free to use this technique and modify to fit your own needs. The link to the repo is below.


#### Resources:
* [github repo](https://github.com/matsilva/nginx-hash-cache)

#### Credits:
* [@connolly_s](https://twitter.com/connolly_s) - showed me the light with this strategy 🤣