SEO check

JavaScript Caching: Test and Fix Cache Headers on JS Files

See how our free report tests cache headers on your JavaScript files, what pass and fail mean, and the exact settings that fix a failed check.

Free to use 14 min read Updated Oct 2026

JavaScript caching in one minute

Quick answer: In our report, JavaScript caching means every script file on your page is served with a Cache-Control or Expires header, so returning visitors reuse the file instead of downloading it again. The check looks at whether those headers are present on the first 8 script files it finds, and the section on how our check works below gives the exact rules.

  • Pass: every script the report tests comes back with a Cache-Control or Expires header, whatever its value.
  • Fail: none of the tested scripts has either header, or the page loads no external script at all. A mix of the two earns a warning.
  • Fix: send Cache-Control: public, max-age=31536000, immutable for script files with hashed names, and a shorter or revalidating policy for files whose names never change.

Check my website

The report is free to run, so you can see your own result first and then come back here for the fix.

What is JavaScript caching?

When a browser downloads a script for the first time, it can keep a copy on disk or in memory. On the next page view it reuses that copy while it is still fresh, so the file never crosses the network again. That is JavaScript caching in the sense our report cares about: the HTTP cache applied to your .js files.

Your server controls the behavior through response headers. Cache-Control and Expires say how long a copy stays fresh. ETag and Last-Modified work differently: they let the browser ask whether the file changed, and the server can answer with a tiny 304 Not Modified reply that has no body. Freshness saves the request entirely, while validation (a conditional request) saves only the download. If a response has neither header, browsers fall back on heuristic freshness, guessing a lifetime from Last-Modified, which is why leaving it unset is a gamble.

Other meanings: Search results for this phrase are crowded with code level caching, such as memoization inside a function, values kept in localStorage, the Cache API used with service workers, and module caches in Node. Our report does not test any of them, and this page covers only the browser cache of script files.

There is a third cache worth knowing about. Google's rendering service keeps its own cache of fetched scripts and may ignore your caching headers, as Google explains in its Google JavaScript SEO basics. The practical answer is the same one browsers like: content fingerprinting, meaning a hash in the file name, so a changed script always arrives under a new URL.

A stored copy is not always a fresh copy. If you were wondering about caching whole HTML pages on the server, that is a separate job, covered in our guide to server side page caching.

How our JavaScript caching check works

This section shows what the SEO Site Checker report does, so you can judge what a pass actually proves. In the report, the JavaScript Caching row shows a status icon and the line "This webpage has total N JavaScript links". Below it you may see up to two collapsible lists, JavaScript Cache Links and JavaScript Uncache Links, each with a count badge. A list appears only when it holds at least one file.

The exact rule: a script counts as cached when its response carries a Cache-Control header or an Expires header, with any value and in any letter case. The report passes when at least one tested script is cached and none is uncached. It gives a warning when both lists have files, and it fails when nothing qualifies.

What the check fetches

We render your page in headless Chromium and read the final DOM, so scripts injected by other scripts are visible to us. Every script element with a src attribute is collected in document order, and only the first 8 are tested. Inline scripts are ignored because they have no file to cache. A relative path is joined to your site root before the request goes out.

Each file is fetched with a plain GET request that identifies itself as Mozilla/5.0. Redirects are followed, the headers are read from the final response, and each request has a 3 second timeout. Up to 8 requests run in parallel inside a 10 second window, so a slow third party script can drop out of the result.

What counts as cached

The check tests header presence only and never reads the value. That means Cache-Control: no-store and max-age=0 both count as cached, even though neither lets a browser reuse the file without asking. A response with only ETag and Last-Modified counts as uncached. A file is also listed as uncached when the request fails, times out or hits an invalid URL.

What pass, warning and fail mean

Picture a report that says 6 total links, with 4 files in the cached list and 2 in the uncached list. That row is a warning, and giving those two files a header turns it into a pass. A fail means no tested file had either header, every fetch failed, or the page has no script with a src at all. Each check counts once toward your score and only a pass adds to it. The file lists show up to 10 names each and are visible to signed in users.

Three limits are worth knowing before you trust a green tick. Our request comes from our server, so a CDN that varies headers by country or user agent may show visitors something different. The check also ignores the status code, so a missing file that returns an error page with a Cache-Control header still counts as cached. Relative paths are joined to your site root, so a path such as js/app.js on a nested page may be tested at the wrong address. The sibling JavaScript minification check uses the same script files but measures size, not headers.

  • Pass: at least one tested script has Cache-Control or Expires and none lacks both.
  • Fail: no tested script has either header, every request failed, or the page has no script tag with a src.
  • Not tested: the header value, whether the script URL returns 200, inline scripts, the ninth script onward, and what visitors in other regions receive.

Test my scripts now

Why JavaScript caching matters for speed and SEO

A fresh cached script costs no request and no transferred bytes on the next view. Picture a returning visitor on a slow mobile connection: a few hundred kilobytes of bundle can take seconds to download, and the cached copy skips all of that on every page view after the first.

Cache headers are not a direct ranking factor. What you gain is faster repeat visits, less load on your server and an easier path to good Core Web Vitals. Our article on why page speed affects SEO and the Core Web Vitals guide cover that wider picture.

Lighthouse flags the same problem as "Serve static assets with an efficient cache policy". Newer Chrome DevTools insights name the finding "Use efficient cache lifetimes".

How to check JavaScript caching headers

You can confirm the report's verdict in four quick ways, and it's worth doing before and after any change.

  1. Run our report. Open the caching row and read the counts. Sign in to see which files sit in the cached and uncached lists.
  2. Use Chrome DevTools. Open the Chrome DevTools Network tab, filter by JS and click a script. The Response Headers panel shows what the server sent. On a repeat view the Size column reads disk cache or memory cache when the browser skipped the network. Tick Disable cache only when you want to imitate a first visit.
  3. Ask the server directly. Run curl against the script URL and filter the output for the four headers that matter.
  4. Read a Lighthouse or PageSpeed Insights audit. The cache policy audit lists each flagged resource with its Cache TTL and transfer size, so you can fix the heaviest files first.
Text
curl -sI https://example.com/assets/app.3f9a1c.js | grep -iE "cache-control|expires|etag|last-modified"

If the output looks odd, repeat with curl -s -D- -o /dev/null followed by the URL, which sends a GET like the report does and prints only the headers. Look for a cache-control or expires line in the output. A large max-age such as 31536000 tells the browser to keep the file for that many seconds. The values max-age=0 and no-cache mean the browser checks with the server every time, and no-store means it should never keep a copy. The official Lighthouse cache policy audit documentation explains the audit output.

How to cache JavaScript files with the right headers

The fix is a server setting for static asset caching, not a code change in your scripts.

  1. Decide the policy per file. Bundles with a hash in the name get public, max-age=31536000, immutable. Files with fixed names get a short max-age, anywhere from minutes to a day, or no-cache together with an ETag.
  2. Add the rule in Apache. Use mod_expires with ExpiresByType for the JavaScript media types, or mod_headers with a FilesMatch rule for .js files.
  3. Add the rule in Nginx. Put a location block around .js files and use one approach only: either add_header Cache-Control on its own, or the Nginx expires directive, which already emits both a max-age Cache-Control line and an Expires line.
  4. Check the CDN or host panel. Set the browser lifetime there or let it respect your origin headers, and make sure the CDN does not strip Cache-Control on the way out.
  5. Reload and re-test. Reload the server config, run curl on a script URL, then run the report again.

Directives you will actually use

Nine directive names cover almost every real setup. The report treats all of them as cached because it only asks whether a header exists, yet visitors experience them very differently. The full model, including validation and freshness rules, is in the MDN HTTP caching guide.

DirectiveWhat it tells the cacheTypical use for scripts
max-age=NTreat the copy as fresh for N seconds.31536000 for hashed bundles
s-maxage=NSame, but for shared caches such as a CDN.Longer edge lifetime than browser lifetime
publicAny cache may store the response.Static files served to everyone
privateOnly the visitor's own browser may store it.Rare for scripts
no-cacheStore it, but revalidate before every reuse.Fixed name files you edit often
no-storeNever keep a copy.Sensitive responses, not normal scripts
immutableThe file will not change while fresh.Hashed bundles
must-revalidateDo not serve a stale copy without checking.Short lifetimes on fixed names
stale-while-revalidate=NServe stale for N seconds while updating in the background.Optional polish for fixed names

Apache and Nginx examples

This Apache version uses mod_expires and suits files whose names change with their content. For fixed names, swap the year for a shorter period such as one day.

Text
<IfModule mod_expires.c>
  ExpiresActive On
  ExpiresByType application/javascript "access plus 1 year"
  ExpiresByType text/javascript "access plus 1 year"
</IfModule>

Put these .htaccess cache rules in the site's .htaccess or virtual host, and confirm mod_expires is enabled, since otherwise the block is skipped.

The mod_expires block gives you max-age plus an Expires line, but it cannot add public or immutable. To send the full value recommended in step 1, use mod_headers instead, and do not combine the two blocks on the same files.

Text
<IfModule mod_headers.c>
  <FilesMatch "\.js$">
    Header set Cache-Control "public, max-age=31536000, immutable"
  </FilesMatch>
</IfModule>

On Nginx the equivalent is a location block with a single header line and no expires directive beside it.

Text
location ~* \.js$ {
    add_header Cache-Control "public, max-age=31536000, immutable";
}

Run nginx -t, reload with nginx -s reload, and check again with curl. Use this block only for hashed file names. For fixed names, change the value to something like "public, max-age=14400, must-revalidate". One trap catches many people: add_header inside a block replaces the add_header lines inherited from the level above it. If your server block sets security headers, repeat them inside this location or they vanish for .js responses.

JavaScript cache busting without breaking updates

The reason most people avoid long lifetimes is fear of visitors stuck on old code. Cache busting removes that fear, because it gives every new version of a file a new address, and nobody has to ask visitors for a hard refresh.

  • Hash the file name. Content hash file names such as app.3f9a1c.js produce versioned URLs that change whenever the content does, which makes long term caching safe. Webpack does this when output.filename uses [contenthash], and the default Vite build adds a hash to its output files.
  • Keep HTML on a short policy. The HTML document names the current script files, so cache it briefly or revalidate it, never for a year.
  • Prefer file names to query strings. Query string versioning, a suffix such as ?v=2, works in browsers, but some proxies and CDN settings treat or strip query strings differently.
  • Never stamp every request. A timestamp or random number on each load turns caching off for every visitor and makes the check pointless.
  • Deploy in the right order. Upload the new hashed files first, then the HTML that references them, and keep the old files around for a while so open tabs do not break.

JavaScript caching on WordPress, Shopify, Cloudflare and Next.js

If you cannot edit server config, the platform usually offers a switch or does the work for you.

On WordPress, a caching plugin or your host's panel can add expiry headers for .js files. Plugin and theme scripts normally carry ?ver=, which changes on a version update, so a long lifetime is usually safe for them. Custom scripts with a fixed version number need a manual bump whenever you edit them.

Shopify, Wix and Squarespace set headers on their own files. A failing file in your report is often an app or tag script served from another host, so the realistic fix is to remove or replace that script.

With Cloudflare, Browser Cache TTL or one of the Cloudflare cache rules sets how long visitors keep a file, while the CDN edge cache is a separate layer with its own lifetime. Check whether your zone respects origin headers or overrides them, and read our CDN usage test guide for the wider setup. Next.js is the easy case, since its static assets under /_next/static are fingerprinted and sent with public, max-age=31536000, immutable, and its documentation says that header cannot be overridden for those assets. Custom headers for other responses belong in next.config.js under headers().

JavaScript caching mistakes that trip the check

Watch for: a pass does not prove good caching. The report passes a file with no-store or max-age=0 because it only tests that a header exists, so check the value yourself with curl before you celebrate.

The opposite error happens too. Some servers send only ETag and Last-Modified. Browsers may reuse those files for a guessed period (heuristic freshness) or revalidate them with a conditional request, and neither outcome is under your control. Our report counts such a file as uncached because it has no explicit lifetime, so add Cache-Control to make the behavior predictable.

  • Long lifetime on the wrong files. Putting a year on your HTML or on scripts with fixed names leaves visitors on old code after a release.
  • Third party scripts. Chat widgets, ad scripts and Google Tag Manager containers often have short or missing headers that you cannot change, so delay, replace or remove them.
  • Expecting full coverage. Only the first 8 scripts are tested, so a ninth uncached file never shows up in the lists.
  • Protocol relative URLs. Our check reads a src that starts with // as an address without a scheme and lists the file as uncached, so write the full https address in the script tag. That also gives you a trustworthy reading.

How to Fix

The short version, the same steps you see when you click How to Fix in your report.

Why this fails: at least one of the first 8 script files on your page came back without a Cache-Control or Expires header, so the browser has no clear instruction on how long to keep it. The check looks only for those two headers. ETag and Last-Modified alone do not count, and a script that timed out is listed as uncached too.

How to fix it:

  1. Find the files with no header. Open the uncached list in the report (sign in to see the file names) or check the response headers in the browser DevTools Network tab.
  2. Choose a lifetime. Use one year for files whose names contain a content hash, and a few hours for files with fixed names.
  3. Add the header in Apache or Nginx with one of the snippets below.
  4. Handle third party files separately. If you cannot change the header, ask the vendor for one, delay the script or replace it.

Apache with mod_expires. Use this for hashed file names only, and pick a shorter time for fixed names:

Text
<IfModule mod_expires.c>
  ExpiresActive On
  ExpiresByType application/javascript "access plus 1 year"
  ExpiresByType text/javascript "access plus 1 year"
</IfModule>

Nginx, with one header line and no separate expires directive. Use it for hashed file names only; for fixed names change the value to something like "public, max-age=14400, must-revalidate":

Text
location ~* \.js$ {
    add_header Cache-Control "public, max-age=31536000, immutable";
}

If you cannot edit server files, use the option your platform gives you:

  • WordPress: turn on expiry headers in a caching plugin or in your host panel.
  • Cloudflare: set Browser Cache TTL or add a cache rule for .js files.
  • Shopify or Wix: these platforms set headers on their own files, so replace or remove any uncached app or tag script from another host.

Re-test: run curl -sI on the script URL and confirm a cache-control line appears. Then run our free report again and check that the JavaScript Uncache Links list no longer appears and the row shows a green tick. The Lighthouse cache policy audit is a good second opinion.

Re-test my website

Frequently Asked Questions

How do I cache JavaScript files in the browser?
Send a Cache-Control header with every script response, for example public, max-age=31536000, immutable for files whose names change whenever their content changes. Set it in Apache, Nginx, your CDN or your hosting panel. Then confirm the header in Chrome DevTools or with our free report, which flags any script that still arrives without one.
How long does a browser cache JavaScript files?
For as long as max-age or Expires says, and for a guessed period if neither is present. With only Last-Modified the browser estimates a lifetime, and once the copy goes stale it sends a conditional request that the server can answer with a short 304 Not Modified. Common choices are one year for files with a content hash in the name, and anywhere from a few minutes to a day for files with fixed names.
How do I stop the browser from caching a JavaScript file?
Send Cache-Control: no-store to forbid keeping a copy, or no-cache to force a check with the server before every reuse. During deployments, changing the file name or version beats turning caching off, because visitors still get fast repeat views. While developing locally, tick Disable cache in the DevTools Network tab instead, which affects only your own browser.
How do I clear the JavaScript cache in Chrome?
As a visitor, press Ctrl+Shift+R, or Cmd+Shift+R on a Mac, for a hard reload. You can also open DevTools, right click the reload button and choose Empty Cache and Hard Reload, or delete cached images and files in Chrome's browsing data settings. As a site owner you cannot clear other visitors' caches, so change the file name or version instead.
Does Cloudflare cache JavaScript files?
Cloudflare caches common static file types such as .js at its edge by default, and it decides by file extension rather than content type. How long the visitor's browser keeps the file is a separate setting, controlled by Browser Cache TTL or a cache rule. Check the response headers your visitors actually receive instead of assuming a default.
Does JavaScript caching affect SEO?
No, not directly, because cache headers are not a ranking signal. Caching makes repeat views faster and lowers server load, which supports your Core Web Vitals field data for returning visitors, though first visits see no change. Treat the benefit as speed and stability for people who come back, rather than a ranking boost.
What is caching in JavaScript?
On the web, caching means the browser keeping a downloaded script file for reuse. In code it means storing results in memory, localStorage or the Cache API, as memoization does inside a function. This page and our report cover the first meaning, the HTTP cache of script files, and the report does not test the code level kind.
Why does the report say some of my JavaScript files are not cached?
The listed file came back with no Cache-Control and no Expires header, or our request for it failed or timed out after three seconds. Third party tags are the usual cause, because you do not control their host. Fix the header at the source if you can, otherwise delay, replace or remove the script, then run the report again to confirm the list is gone.

See how your site scores right now

Paste your address into the free report, then use the How to Fix button on anything that needs work.

Free to use. Results in under a minute for most sites.