Boost the UX of your React app with hash based content caching

Boost the UX of your React app with hash based content caching

So to get started, we are going to use good ole 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:

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 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 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 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/Dockerfilewith 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. 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:

#!/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 runtimemain..js do cat > /etc/nginx/conf.d/app/js.conf <<EOF location ~ ^/app/js/runtimemain.js([.]map)?$ { expires off; add_header Cache-Control "no-cache"; return 303 ${js}$1; } location ~* ^/app/js/(runtimemain[.][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 "[email protected]"

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 runtimemain..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 "[email protected]". 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. 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:

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 and checkout the network tab to confirm that the asset files are from the latest build.

🤘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:

Thanks for reading. If you liked this post, share it with all of your programming buddies!

Further reading

☞ React - The Complete Guide (incl Hooks, React Router, Redux)

☞ Modern React with Redux [2019 Update]

☞ Best 50 React Interview Questions for Frontend Developers in 2019

☞ JavaScript Basics Before You Learn React

☞ Microfrontends — Connecting JavaScript frameworks together (React, Angular, Vue etc)

☞ Reactjs vs. Angularjs — Which Is Best For Web Development

☞ React + TypeScript : Why and How

☞ How To Write Better Code in React

☞ React Router: Add the Power of Navigation

☞ Getting started with React Router

☞ Using React Router for optimizing React apps


This post was originally published here

reactjs react-native javascript docker mobile-apps

Bootstrap 5 Complete Course with Examples

Bootstrap 5 Tutorial - Bootstrap 5 Crash Course for Beginners

Nest.JS Tutorial for Beginners

Hello Vue 3: A First Look at Vue 3 and the Composition API

Building a simple Applications with Vue 3

Deno Crash Course: Explore Deno and Create a full REST API with Deno

How to Build a Real-time Chat App with Deno and WebSockets

Convert HTML to Markdown Online

HTML entity encoder decoder Online

How native is React Native? | React Native vs Native App Development

Article covers: How native is react native?, React Native vs (Ionic, Cordova), Similarities and difference between React Native and Native App Development.

Top React Native Mobile App Development Companies in USA

Looking for top React Native mobile app development company in USA for Startups & Enterprise? Find out the top list of React Native mobile app development company in USA.

Which is the best React Native app development company in New York?

Hire top react native app development company in New York to build and develop custom react native mobile apps for Android & iOS with the latest features.

Create React Native eCommerce Mobile App

Find out how to build eCommerce mobile application with React Native. Tips and tricks to save time, efforts, and money.

Top React Native Mobile App Development Companies in New York

Looking for top React Native mobile app development company in USA for Startups & Enterprise? Find out the top list of React Native mobile app development company in USA.