Here's a snippet of my .htaccess file:
# ------------------------------------------------------------------------------
# | ETag removal |
# ------------------------------------------------------------------------------
# Since we're sending far-future expires headers (see below), ETags can
# be removed: http://developer.yahoo.com/performance/rules.html#etags.
# `FileETag None` is not enough for every server.
<IfModule mod_headers.c>
Header unset ETag
<filesMatch "\.(ico|jpe?g|png|gif|swf)$">
Header set Cache-Control "max-age=2592000, public"
</filesMatch>
<filesMatch "\.(css)$">
Header set Cache-Control "max-age=604800, public"
</filesMatch>
<filesMatch "\.(js)$">
Header set Cache-Control "max-age=216000, public"
</filesMatch>
<filesMatch "\.(x?html?|php)$">
Header set Cache-Control "max-age=600, private, must-revalidate"
</filesMatch>
</IfModule>
FileETag None
# ------------------------------------------------------------------------------
# | Expires headers (for better cache control) |
# ------------------------------------------------------------------------------
# The following expires headers are set pretty far in the future. If you don't
# control versioning with filename-based cache busting, consider lowering the
# cache time for resources like CSS and JS to something like 1 week.
<IfModule mod_expires.c>
ExpiresActive on
ExpiresDefault "access plus 1 month"
# CSS
ExpiresByType text/css "access plus 1 year"
# Data interchange
ExpiresByType application/json "access plus 0 seconds"
ExpiresByType application/xml "access plus 0 seconds"
ExpiresByType text/xml "access plus 0 seconds"
# Favicon (cannot be renamed!)
ExpiresByType image/x-icon "access plus 1 week"
# HTML components (HTCs)
ExpiresByType text/x-component "access plus 1 month"
# HTML
ExpiresByType text/html "access plus 0 seconds"
# JavaScript
ExpiresByType text/javascript "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType application/x-javascript "access plus 1 year"
# Manifest files
ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds"
ExpiresByType text/cache-manifest "access plus 0 seconds"
# Media
ExpiresByType audio/ogg "access plus 1 month"
ExpiresByType image/gif "access plus 1 month"
ExpiresByType image/jpeg "access plus 1 month"
ExpiresByType image/jpg "access plus 1 month"
ExpiresByType image/png "access plus 1 month"
ExpiresByType video/mp4 "access plus 1 month"
ExpiresByType video/ogg "access plus 1 month"
ExpiresByType video/webm "access plus 1 month"
# Web feeds
ExpiresByType application/atom+xml "access plus 1 hour"
ExpiresByType application/rss+xml "access plus 1 hour"
# Web fonts
ExpiresByType application/font-woff "access plus 1 month"
ExpiresByType application/vnd.ms-fontobject "access plus 1 month"
ExpiresByType application/x-font-ttf "access plus 1 month"
ExpiresByType font/opentype "access plus 1 month"
ExpiresByType image/svg+xml "access plus 1 month"
</IfModule>
As can be seen, I have Cache-Control and Expires in there in the appropriate syntax. And yet Google PageSpeed tool says my JS isn't leveraging browser caching. Is there anything I'm missing to add?
I understand it's not good practice to include both Cache-Control and Expires Headers simultaneously. So I removed the Cache-Control section. The local file (am not concerned with third-party externals at the moment) is still showing as uncached when run past Google. The file that needs to be cached is http://www.peppyburro.com/sandboxassets/js/burroinline.js. The .htaccess in question is located at the document root, i.e. http://www.peppyburro.com/.htaccess. An .htaccess with the above caching snippet also resides in the /js folder that contains burroinline.js.
To add to the confusion, https://www.giftofspeed.com/cache-checker/ says my file is caching as expected whereas Google PageSpeed and GTMetrix say it isn't.
UPDATE: Looks like my CDN (CloudFlare) has something to do with caching issues because once I disabled CF, the caching kind of started working. I say kind of because Google PageSpeed randomly alternates between cached and un-cached despite nothing changing in my htaccess! Also, GTMetrix still shows the file as un-cached. Here's what the header looks like on my browser:
**General**
Request URL:http://peppyburro.com/sandboxassets/js/burroinline.js
Request Method:GET
Status Code:200 OK (from disk cache)
Remote Address:209.99.16.94:80
**Response Headers**
Accept-Ranges:bytes
Access-Control-Allow-Origin:*
Age:0
Cache-Control:max-age=216000, public
Content-Encoding:gzip
Content-Length:38611
Content-Type:application/javascript
Date:Wed, 01 Mar 2017 16:22:41 GMT
Expires:Thu, 01 Mar 2018 16:22:41 GMT
Last-Modified:Wed, 01 Mar 2017 02:18:53 GMT
Server:Apache Phusion_Passenger/4.0.10 mod_bwlimited/1.4 mod_fcgid/2.3.9
Vary:Accept-Encoding,User-Agent
Via:1.1 varnish-v4
X-Varnish:31524632
Does this mean the file is actually getting cached? If so, what could be done to have the same reflected consistently in Google PageSpeed and GTMetrix? And how could I make it work with CF enabled?
PS: Being on a shared host, I don't have access to httpd.conf.
EDIT: The answer ended up being a conflict between CloudFlare and the .htaccess file. The comments on this post discuss the troubleshooting and resolution of this issue.
I ran this resource through pingdom's tools to see what the request/response looked like.
https://tools.pingdom.com/#!/d8QPQx/http://www.peppyburro.com/sandboxassets/js/burroinline.js
It is in fact not being cached. The header is set to no-cache.
"no-cache" indicates that the returned response can't be used to satisfy a subsequent request to the same URL without first checking with the server if the response has changed. As a result, if a proper validation token (ETag) is present, no-cache incurs a roundtrip to validate the cached response, but can eliminate the download if the resource has not changed.
The response from the server is:
Cache-Control public, max-age=216000
Because it is javascript, it may be advisable to extend this to a week or more. Additionally, the response here is public, while your setting is
Header set Cache-Control "max-age=216000, private"
The age is correct, but the visibility is a disparity.
"public" vs. "private"
If the response is marked as "public", then it can be cached, even if it has HTTP authentication associated with it, and even when the response status code isn't normally cacheable. Most of the time, "public" isn't necessary, because explicit caching information (like "max-age") indicates that the response is cacheable anyway.
By contrast, the browser can cache "private" responses. However, these responses are typically intended for a single user, so an intermediate cache is not allowed to cache them. For example, a user's browser can cache an HTML page with private user information, but a CDN can't cache the page.
I am seeing some cloudflare (CDN) headers as well, marking that it is not cached. Typically, private responses are intended for sensitive content. I would first try to set this as public, but only if you are not concerned with sensitive information.
If you are concerned with sensitive information leave this as private.
While I think this may be the problem, there are several other factors (centered around a CDN) that could also be contributing to the problem.
Accept-Ranges:bytes
Access-Control-Allow-Origin:*
Age:0
Cache-Control:public, max-age=216000
CF-Cache-Status:MISS
CF-RAY:338d062cb1035a6e-BOS
Connection:Keep-Alive
Content-Type:application/javascript
Date:Wed, 01 Mar 2017 15:07:08 GMT
Expires:Sat, 04 Mar 2017 03:07:08 GMT
Last-Modified:Wed, 01 Mar 2017 02:18:53 GMT
Proxy-Connection:Keep-Alive
Server:cloudflare-nginx
Vary:Accept-Encoding
Via:1.1 varnish-v4
X-Varnish:18615326
These are the response headers from the server. They include a "MISS" in CF (cloudflare) caching. Additionally, here the cache control is also set to public.
Because of this, I think that the intermediate CDN may be causing caching issues.
If you have any additional information to provide (such as CDN/CloudFlare information), I would be happy to take another look.