Friday, April 19, 2024
HomeCSSTroubleshooting Caching

Troubleshooting Caching


Whereas launching the new model of this website just lately, I got here throughout a number of points with some browsers unexpectedly caching the previous model – regardless of this being a complete rebuild. It meant some customers had been nonetheless seeing the earlier model of the location except they manually cleared their cache. Clearly this isn’t an affordable request to make of each consumer!

There are completely different the explanation why information could be cached, and for essentially the most half, caching is an efficient factor. As soon as downloaded, property will be saved in a cache to keep away from the browser repeatedly making requests and re-downloading the information. If you wish to study the ins and outs of caching, MDN docs are an excellent place to begin. This text particularly tackles caching of the undesirable selection, as described above. I’ll share some steps I took to make sure browsers serve the most recent information after deploying adjustments.

Deploying distinctive information

If we deploy a file with a singular identify, the browser will recognise it as a brand new file and serve it up. A commonly-used cache-busting method is to append a singular identifier (or hash) to the names of our asset information (comparable to CSS and JS information). model.css turns into style27850398694772.css, for instance.

The earlier model of this website was constructed with Gatsby, which dealt with this out of the field. With Eleventy, the static website generator I’m at present utilizing, we have to do some work. Eleventy truly has a plugin for this, nevertheless, I’m utilizing Parcel to construct the asset information, so it’s not going to work for us right here. To be able to implement the file hashing, we are able to do the next:

  1. Construct the asset information.
  2. Create a singular hash variable and write this to a knowledge file. Guarantee that is pulled into our Eleventy information as a variable.
  3. Rename the asset information, appending the hash variable.

Let’s break that down:

Construct the asset information

I’m working Parcel’s construct command to compile our CSS from Sass and bundle our JS modules, outlined within the package deal.json:

"prod:parcel": "construct:*",
"construct:css": "parcel construct src/css/types.scss",
"construct:js": "parcel construct src/js/index.js",

By default these will construct to a dist listing, however you possibly can change the output file if you must with Parcel’s construct choices.

Operating Eleventy builds our template information to the identical listing. I’ve a script outlined in my package deal.json for that too:

"prod:eleventy": "npx @11ty/eleventy",

With out hashing the information, I can simply reference them by their path in my HTML:

<hyperlink rel="stylesheet" href="/types.css" />

Create a singular hash

To be able to create a file with a singular hash, we have to write some JS. I’ve created a file, onBuild.js, which I can run with the next script:

"hash": "node onBuild.js"

In that file, we are able to use a timestamp to provide a singular hash – since it will likely be completely different each time:

let hash = Date.now()
hash = hash.toString()

Date.now() outputs a quantity, however we wish to write this to a JSON file, so I’m changing it to a string.

Then we are able to use the Node File System module to jot down that hash to a JSON file in our Eleventy _data listing:

fs.writeFile('src/_data/model.json', hash, perform (err) {
if (err) return console.log(err)
console.log(`${hash} > src/_data/model.json`)
})

Now we’re free to make use of this variable in our Eleventy template information. I’m utilizing Nunjucks as my templating language, so we are able to embody it like this:

<hyperlink rel="stylesheet" href="types{ { model } }.css" />

(We are able to apply it to our JS file in the same manner.)

Rename the information

In the identical onBuild.js file, we’ll rename our compiled CSS and JS information, appending the hash:

fs.rename('dist/types.css', `dist/types${hash}.css`, perform (err) {
if (err) return console.log(err)
console.log(`dist/types.css > dist/types${hash}.css`)
})

fs.rename('dist/index.js', `dist/index${hash}.js`, perform (err) {
if (err) return console.log(err)
console.log(`dist/index.js > dist/index${hash}.js`)
})

We have to run this script on construct, but it surely’s depending on the CSS and JS information already current within the dist listing (our construct listing). I exploit the package deal npm-run-all to specify when to run scripts concurrently or sequentially. Again in our package deal.json, utilizing the run-s command we are able to make sure the hash script runs after the command that builds the CSS and JS information:

"construct": "run-s prod:parcel hash",

Lastly as soon as the CSS and JS information are constructed and the hash variable created and appended to the file names, we are able to compile our Eleventy template information, protected within the information that the hash variable will exist. So let’s append our Eleventy construct command to the construct script, in order that the instructions are run in sequence:

"construct": "run-s prod:parcel hash prod:eleventy",

Eradicating the hash in dev mode

Once I’m working Parcel in watch mode (i.e. whereas I’m growing my website), I don’t wish to pull within the hash variable, as Parcel will solely create the un-hashed information at that time. So moreover we are able to create a script file (which I’m calling onStart.js) to run once I run the npm begin command:

const fs = require('fs')

fs.writeFile('src/_data/model.json', '', perform (err) {
if (err) return console.log(err)
console.log(`${''} > src/_data/model.json`)
})

This removes the hash variable within the model.json knowledge file, making certain it is not going to be appended to the file names referenced in our Nunjucks information.

Limitations

I’m at present solely hashing the CSS and JS information, not different asset information comparable to pictures, which suggests they nonetheless run the danger of being cached. Coping with pictures on this manner can be extra advanced, and as they’re unlikely to vary fairly often I made a decision to not take these additional steps on this case.

Content material-based versioning

It’s price taking into account that if we’re utilizing a timestamp, the information will probably be re-hashed on each construct. Customers must obtain new information, even when the file contents themselves haven’t modified. For content-based hashing we may as an alternative use the MD5 package deal. The step to implement content-based hashing are a bit extra in depth than we’ll cowl right here, however my Eleventy starter venture, Eleventy-Parcel has content-based versioning in-built.

Set cache-control HTTP headers

Moreover, we are able to stop browsers caching our HTML pages by setting HTTP headers. My website is hosted with Netlify, which suggests I can specify the headers in a netlify.toml configuration file:

[[headers]]
for = "/*"

[headers.values]
cache-control = '''
max-age=0,
no-cache,
no-store,
must-revalidate'''

By setting max-age="0", we make sure that the browser considers the file stale, prompting it to revalidate with the server.

We are able to additionally set cache-control headers as HTML meta tags:

<meta
http-equiv="Cache-Management"
content material="no-store, no-store, must-revalidate"
/>

<meta http-equiv="Pragma" content material="no-cache" />
<meta http-equiv="Expires" content material="0" />

However that is thought-about inferior as a result of the meta tags are solely honoured by a some browser caches, not proxy caches. I needed to do a little bit of digging to search out out concerning the distinction between utilizing meta tags versus HTTP headers, however this text explains it properly.

Service employees

Even after taking the entire above steps, I nonetheless had some issues with my website caching. It turned out, the reply was a service employee. Service employees are Javascript employees that run off the principle thread, separate out of your internet software. They will deal with all kinds of duties, notably intercepting community requests and caching responses. Service employees are a fairly large subject themselves, so I’m not going to delve into the small print right here. However in the event you’re thinking about additional studying, I like to recommend testing Google’s official documentation.

To see any service employees registered in your website in Chrome, open the developer instruments panel and go to the ‘Software’ tab. Within the ‘Software’ menu, it is best to see ‘Service employees’.

The attention-grabbing factor about my new website was that it didn’t have a service employee – but in my dev instruments I used to be nonetheless seeing a service employee registered. Why? As a result of the previous model of the location had a service employee, and it hadn’t been unregistered. You’ll be able to unregister a service employee from the dev instruments panel, however in fact we are able to’t anticipate each consumer to do this! So we have to unregister the previous service employee.

Unregistering service employees

Service employees are registered in Javascript – and so they can be unregistered. To verify for current service employees and unregister any that exist, we are able to add the next to our primary JS file:

navigator.serviceWorker.getRegistrations().then(perform (registrations) {
for (let registration of registrations) {
registration.unregister().then((unregistered) => {
console.log(
unregistered == true ? 'unregistered' : 'did not unregister'
)
})
}
})

Updating the service employee

An alternative choice is to put in a brand new service employee to replace the previous one. Google’s documentation has this to say about updating service employees:

When the consumer navigates to your website, the browser tries to redownload the script file that outlined the service employee within the background. If there may be even a byte’s distinction within the service employee file in comparison with what it at present has, it considers it new.

So updating the service employee (or registering it anew) ought to successfully overwrite the previous one. In my venture, I created the file service-worker.js, and ensured this was constructed with Parcel by updating my construct.js command:

"construct:js": "parcel construct src/js/index.js src/js/service-worker.js"

First we have to register the brand new service employee in our primary JS file. We first verify that the browser helps service employees, then register the brand new service employee on load:

if ('serviceWorker' in navigator) {
window.addEventListener('load', perform () {
navigator.serviceWorker.register('/service-worker.js').then(
perform (registration) {
console.log(
'ServiceWorker registration profitable with scope: ',
registration.scope
)
},
perform (err) {
console.log('ServiceWorker registration failed: ', err)
}
)
})
}

When a consumer visits your website, the service employee will probably be put in. It then enters a “ready” state – if there may be an current service employee, the brand new service employee will wait till the web page is refreshed to be able to take management.

Within the service employee file itself (service-worker.js on this venture), we are able to specify callbacks to run when the service employee is put in and activated. On set up, we are able to name self.skipWaiting() to instruct it to take management immediately:

self.addEventListener('set up', perform () {
self.skipWaiting()
})

Now, if we wish to unregister any previous service employees (together with this one!), we are able to run a callback on the activate occasion:

self.addEventListener('activate', perform() {
.then(() => {
self.registration.unregister()
.then(() => console.log('unregister'))
})
.catch((err) => console.log(err))
})

We most likely wish to do a bit extra with our service employee than simply take away an current service employee, however that’s past the scope of this text.

Clearing the cache with a service employee

This did the job of unregistering the problematic service employee, and initially it gave the impression to be sufficient. But it surely turned out that some customers had been nonetheless seeing the previous model of the location till they carried out a tough reload. The wrongdoer turned out to be gatsby-plugin-offline, a plugin I had put in on my previous Gatsby website. Though I wasn’t utilizing Gatsby anymore, once I opened the cache within the browser dev instruments, I may nonetheless see a number of information associated to that plugin in there.

Ultimately, I used to be capable of resolve this by utilizing my new service employee to clear the cache:

self.addEventListener('set up', perform (e) {
self.skipWaiting()
})

self.caches
.keys()
.then((keys) => {
keys.forEach((key) => {
console.log(key)
self.caches.delete(key)
})
})
.then(() => {
self.registration.unregister()
console.log('unregister')
})
.catch((err) => console.log(err))

Lastly, we are able to take the non-compulsory step of retrieving a listing of any open tabs below the service employee’s management and forcing them to reload, quite than the consumer having to do it manually. self.shoppers.matchAll() retrieves a listing of open browser tabs. Our service employee file now seems to be like this:

self.addEventListener('set up', perform (e) {
self.skipWaiting()
})

self.caches
.keys()
.then((keys) => {
keys.forEach((key) => {
console.log(key)
self.caches.delete(key)
})
})
.then(() => {
self.registration.unregister()
console.log('unregister')
})
.then(() => {
self.shoppers.matchAll()
console.log(self.shoppers)
})
.then((shoppers) => {
shoppers.forEach((shopper) => shopper.navigate(shopper.url))
})
.catch((err) => console.log(err))

Assets

I learnt rather a lot about caching and repair employees throughout this course of! Listed here are a number of the sources that helped me:

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments