How do I cache bust imported modules in es6?

WebBrowser CacheEs6 Modules

Web Problem Overview


ES6 modules allows us to create a single point of entry like so:

// main.js

import foo from 'foo';

foo()

<script src="scripts/main.js" type="module"></script>

foo.js will be stored in the browser cache. This is desirable until I push a new version of foo.js to production.

It is common practice to add a query string param with a unique id to force the browser to fetch a new version of a js file (foo.js?cb=1234)

How can this be achieved using the es6 module pattern?

Web Solutions


Solution 1 - Web

There is one solution for all of this that doesn't involve query string. let's say your module files are in /modules/. Use relative module resolution ./ or ../ when importing modules and then rewrite your paths in server side to include version number. Use something like /modules/x.x.x/ then rewrite path to /modules/. Now you can just have global version number for modules by including your first module with <script type="module" src="/modules/1.1.2/foo.mjs"></script>

Or if you can't rewrite paths, then just put files into folder /modules/version/ during development and rename version folder to version number and update path in script tag when you publish.

Solution 2 - Web

HTTP headers to the rescue. Serve your files with an ETag that is the checksum of the file. S3 does that by default at example. When you try to import the file again, the browser will request the file, this time attaching the ETag to a "if-none-match" header: the server will verify if the ETag matches the current file and send back either a 304 Not Modified, saving bandwith and time, or the new content of the file (with its new ETag).

This way if you change a single file in your project the user will not have to download the full content of every other module. It would be wise to add a short max-age header too, so that if the same module is requested twice in a short time there won't be additional requests.

If you add cache busting (e.g. appending ?x={randomNumber} through a bundler, or adding the checksum to every file name) you will force the user to download the full content of every necessary file at every new project version.

In both scenario you are going to do a request for each file anyway (the imported files on cascade will produce new requests, which at least may end in small 304 if you use etags). To avoid that you can use dynamic imports e.g if (userClickedOnSomethingAndINeedToLoadSomeMoreStuff) { import('./someModule').then('...') }

Solution 3 - Web

From my point of view dynamic imports could be a solution here.

Step 1) Create a manifest file with gulp or webpack. There you have an mapping like this:

export default {
    "/vendor/lib-a.mjs": "/vendor/lib-a-1234.mjs",
    "/vendor/lib-b.mjs": "/vendor/lib-b-1234.mjs"
};

Step 2) Create a file function to resolve your paths

import manifest from './manifest.js';

const busted (file) => {
 return manifest[file];
};

export default busted;

Step 3) Use dynamic import

import busted from '../busted.js';

import(busted('/vendor/lib-b.mjs'))
  .then((module) => {
    module.default();
});

I give it a short try in Chrome and it works. Handling relative paths is tricky part here.

Solution 4 - Web

what i did was handle the cache busting in webserver (nginx in my instance)

instead of serving

<script src="scripts/main.js" type="module"></script>

serve it like this where 123456 is your cache busting key

<script src="scripts/123456/main.js" type="module"></script>

and include a location in nginx like

location ~ (.+)\/(?:\d+)\/(.+)\.(js|css)$ {
  try_files $1/$2.min.$3 $uri;
}

requesting scripts/123456/main.js will serve scripts/main.min.js and an update to the key will result in a new file being served, this solution works well for cdns too.

Solution 5 - Web

Just a thought at the moment but you should be able to get Webpack to put a content hash in all the split bundles and write that hash into your import statements for you. I believe it does the second by default.

Solution 6 - Web

I've created a Babel plugin which adds a content hash to each module name (static and dynamic imports).

import foo from './js/foo.js';

import('./bar.js').then(bar => bar());

becomes

import foo from './js/foo.abcd1234.js';

import('./bar.1234abcd.js').then(bar => bar());

You can then use Cache-control: immutable to let UAs (browsers, proxies, etc) cache these versioned URLs indefinitely. Some max-age is probably more reasonable, depending on your setup.

You can use the raw source files during development (and testing), and then transform and minify the files for production.

Solution 7 - Web

You can use ETags, as pointed out by a previous answer, or alternatively use Last-Modified in relation with If-Modified-Since.

Here is a possible scenario:

  1. The browser first loads the resource. The server responds with Last-Modified: Sat, 28 Mar 2020 18:12:45 GMT and Cache-Control: max-age=60.
  2. If the second time the request is initiated earlier than 60 seconds after the first one, the browser serves the file from cache and doesn't make an actual request to the server.
  3. If a request is initiated after 60 seconds, the browser will consider cached file stale and send the request with If-Modified-Since: Sat, 28 Mar 2020 18:12:45 GMT header. The server will check this value and:
  • If the file was modified after said date, it will issue a 200 response with the new file in the body.
  • If the file was not modified after the date, the server will issue a304 "not modified" status with empty body.

I ended up with this set up for Apache server:

<IfModule headers_module>
  <FilesMatch "\.(js|mjs)$">
    Header set Cache-Control "public, must-revalidate, max-age=3600"
    Header unset ETag
  </FilesMatch>
</IfModule>

You can set max-age to your liking.

We have to unset ETag. Otherwise Apache keeps responding with 200 OK every time (it's a bug). Besides, you won't need it if you use caching based on modification date.

Solution 8 - Web

If you are using Visual Studio 2022 and TypeScript to write your code, you can follow a convention of adding a version number to your script file names, e.g. MyScript.v1.ts. When you make changes and rename the file to MyScript.v2.ts Visual Studio shows the following dialog similar to the following:

Rename file dialog

If you click Yes it will go ahead and update all the files that were importing this module to refer to MyScript.v2.ts instead of MyScript.v1.ts. The browser will notice the name change too and download the new modules as expected.

It's not a perfect solution (e.g. if you rename a heavily used module, a lot of files can end up being updated) but it is a simple one!

Solution 9 - Web

this work for me

let url = '/module/foo.js'
url = URL.createObjectURL(await (await fetch(url)).blob())
let foo = await import(url)

Solution 10 - Web

A solution that crossed my mind but I wont use because I don't like it LOL is

window.version = `1.0.0`;

let { default: fu } = await import( `./bar.js?v=${ window.version }` );

Using the import "method" allows you to pass in a template literal string. I also added it to window so that it can be easily accessible no matter how deep I'm importing js files. The reason I don't like it though is I have to use "await" which means it has to be wrapped in an async method.

Solution 11 - Web

Use of relative path works for me:

import foo from './foo';

or

import foo from './../modules/foo';

instead of

import foo from '/js/modules/foo';

EDIT

Since this answer is down voted, I update it. The module is not always reloaded. The first time, you have to reload the module manually and then the browser (at least Chrome) will "understand" the file is modified and then reload the file every time it is updated.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionspinnersView Question on Stackoverflow
Solution 1 - WebWantonView Answer on Stackoverflow
Solution 2 - WebRiccardo GalliView Answer on Stackoverflow
Solution 3 - Webd-bro82View Answer on Stackoverflow
Solution 4 - WebNiall DawsonView Answer on Stackoverflow
Solution 5 - Webuser11639687View Answer on Stackoverflow
Solution 6 - WebJeroen VersteegView Answer on Stackoverflow
Solution 7 - WebTigranView Answer on Stackoverflow
Solution 8 - WebEric MuttaView Answer on Stackoverflow
Solution 9 - WebTime KillerView Answer on Stackoverflow
Solution 10 - WebBanningView Answer on Stackoverflow
Solution 11 - WebFifiView Answer on Stackoverflow