CINXE.COM
Cache-Control for Civilians – Harry Roberts – Web Performance Consultant
<!DOCTYPE html> <html lang=en-GB class="page page--post"> <script>performance.mark('headStart');</script> <meta charset=UTF-8> <meta name=viewport content="width=device-width, minimum-scale=1.0"> <title>Cache-Control for Civilians – Harry Roberts – Web Performance Consultant</title> <link rel=preconnect href=https://res.cloudinary.com> <link rel=preconnect href=https://cdn.carbonads.com> <link rel=preconnect href=https://srv.carbonads.net> <link rel=preconnect href=https://cdn.syndication.twimg.com> <link rel=preconnect href=https://syndication.twitter.com> <link rel=preconnect href=https://csswizardry.com crossorigin> <script> LUX=(function(){var a=("undefined"!==typeof(LUX)&&"undefined"!==typeof(LUX.gaMarks)?LUX.gaMarks:[]);var d=("undefined"!==typeof(LUX)&&"undefined"!==typeof(LUX.gaMeasures)?LUX.gaMeasures:[]);var j="LUX_start";var k=window.performance;var l=("undefined"!==typeof(LUX)&&LUX.ns?LUX.ns:(Date.now?Date.now():+(new Date())));if(k&&k.timing&&k.timing.navigationStart){l=k.timing.navigationStart}function f(){if(k&&k.now){return k.now()}var o=Date.now?Date.now():+(new Date());return o-l}function b(n){if(k){if(k.mark){return k.mark(n)}else{if(k.webkitMark){return k.webkitMark(n)}}}a.push({name:n,entryType:"mark",startTime:f(),duration:0});return}function m(p,t,n){if("undefined"===typeof(t)&&h(j)){t=j}if(k){if(k.measure){if(t){if(n){return k.measure(p,t,n)}else{return k.measure(p,t)}}else{return k.measure(p)}}else{if(k.webkitMeasure){return k.webkitMeasure(p,t,n)}}}var r=0,o=f();if(t){var s=h(t);if(s){r=s.startTime}else{if(k&&k.timing&&k.timing[t]){r=k.timing[t]-k.timing.navigationStart}else{return}}}if(n){var q=h(n);if(q){o=q.startTime}else{if(k&&k.timing&&k.timing[n]){o=k.timing[n]-k.timing.navigationStart}else{return}}}d.push({name:p,entryType:"measure",startTime:r,duration:(o-r)});return}function h(n){return c(n,g())}function c(p,o){for(i=o.length-1;i>=0;i--){var n=o[i];if(p===n.name){return n}}return undefined}function g(){if(k){if(k.getEntriesByType){return k.getEntriesByType("mark")}else{if(k.webkitGetEntriesByType){return k.webkitGetEntriesByType("mark")}}}return a}return{mark:b,measure:m,gaMarks:a,gaMeasures:d}})();LUX.ns=(Date.now?Date.now():+(new Date()));LUX.ac=[];LUX.cmd=function(a){LUX.ac.push(a)};LUX.init=function(){LUX.cmd(["init"])};LUX.send=function(){LUX.cmd(["send"])};LUX.addData=function(a,b){LUX.cmd(["addData",a,b])};LUX_ae=[];window.addEventListener("error",function(a){LUX_ae.push(a)});LUX_al=[];if("function"===typeof(PerformanceObserver)&&"function"===typeof(PerformanceLongTaskTiming)){var LongTaskObserver=new PerformanceObserver(function(c){var b=c.getEntries();for(var a=0;a<b.length;a++){var d=b[a];LUX_al.push(d)}});try{LongTaskObserver.observe({type:["longtask"]})}catch(e){}}; LUX = window.LUX || {}; LUX.samplerate = 100; LUX.label = 'Post'; </script> <script> (function(t,e,n,r){function a(){return e&&e.now?e.now():null}if(!n.version){n._events=[];n._errors=[];n._metadata={};n._urlGroup=null;window.RM=n;n.install=function(e){n._options=e;var a=t.createElement("script");a.async=true;a.crossOrigin="anonymous";a.src=r;var o=t.getElementsByTagName("script")[0];o.parentNode.insertBefore(a,o)};n.identify=function(t,e){n._userId=t;n._identifyOptions=e};n.sendEvent=function(t,e){n._events.push({eventName:t,metadata:e,time:a()})};n.setUrlGroup=function(t){n._urlGroup=t};n.track=function(t,e){n._errors.push({error:t,metadata:e,time:a()})};n.addMetadata=function(t){n._metadata=Object.assign(n._metadata,t)}}})(document,window.performance,window.RM||{},"https://cdn.requestmetrics.com/agent/current/rm.js"); RM.install({ token: "j5ss3vj:q7xf9tv" }); </script> <script>performance.mark('cssStart');</script> <style> /*! * inuitcss, by @csswizardry * * github.com/inuitcss | inuitcss.com *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}body,h1,h2,h3,h4,h5,h6,p,blockquote,pre,dl,dd,ol,ul,form,fieldset,legend,figure,table,th,td,caption,hr{margin:0;padding:0}abbr[title],dfn[title]{cursor:help}u,ins{text-decoration:none}ins{border-bottom:1px solid}html{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*,*:before,*:after{-webkit-box-sizing:inherit;-moz-box-sizing:inherit;box-sizing:inherit}h1,h2,h3,h4,h5,h6,ul,ol,dl,blockquote,p,address,hr,table,fieldset,figure,pre,details,[open]>summary,.page-head,.promo__block{margin-bottom:24px;margin-bottom:1.5rem}ul,ol,dd{margin-left:48px;margin-left:3rem}html{font-size:1em;line-height:1.5;background-color:#f9f9f9;color:#333;overflow-y:scroll;min-height:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased}@view-transition{navigation:auto}html{font-family:system-ui,sans-serif;font-weight:400;scroll-behavior:smooth}h1,.alpha{font-size:36px;font-size:2.25rem;line-height:1.333333333}h2,.beta{font-size:30px;font-size:1.875rem;line-height:1.6}h3,.gamma{font-size:24px;font-size:1.5rem;line-height:1}h4,.delta{font-size:20px;font-size:1.25rem;line-height:1.2}h5,.epsilon{font-size:16px;font-size:1rem;line-height:1.5}h6,.zeta{font-size:14px;font-size:.875rem;line-height:1.714285714}h1,h2,h3,h4,h5,h6{font-weight:300;color:#f43059;text-wrap:balance}@media screen and (min-width: 45em){h1{font-size:72px;font-size:4.5rem;line-height:1;font-weight:900;border-left:24px solid #f43059;border-radius:3px;margin-left:-36px;padding-left:12px}}h2:has(>img){line-height:1}.heading{display:block;font-size:22px;font-size:1.375rem;line-height:1.090909091;color:#f43059}.heading-sub{display:block;font-size:16px;font-size:1rem;line-height:1.5;font-weight:600;margin-bottom:0;color:#333}a,strong,b,dt{font-weight:600}p{text-wrap:pretty}code,kbd,samp,output{color:#859900;font-family:"Operator Mono", Inconsolata, Monaco, Consolas, "Andale Mono", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace;font-style:normal;word-break:break-word}a:hover code,a:active code,a:focus code,a:hover kbd,a:active kbd,a:focus kbd,a:hover samp,a:active samp,a:focus samp,a:hover output,a:active output,a:focus output{text-decoration:underline}pre{white-space:pre;word-wrap:normal;overflow:auto;padding:12px;background-color:#333;border-radius:3px}@media screen and (min-width: 45em){pre{margin-right:-12px;margin-left:-12px}}pre,pre code,pre kbd,pre samp{color:#fff;word-break:keep-all}li pre{margin-top:1.5rem}var{font-family:Hoefler Text, Georgia, serif;line-height:1;font-style:italic}figure{counter-increment:figure}@media screen and (min-width: 1380px){figure{margin-left:-128px}}figure>img{border-radius:3px}figcaption{font-size:12px;font-size:.75rem;line-height:2}@media screen and (min-width: 1380px){figcaption{margin-left:128px}}figcaption::before{content:"Fig. " counter(figure) ": ";font-weight:bold}hr{border:none;border-bottom:1px solid #ddd;margin-bottom:23px}pre mark{background:none;border-bottom:1px solid;color:inherit}.text-banner{text-align:center;text-wrap:balance}s,del{opacity:0.75}ins{border-bottom:none;background-color:#ffc}ol ol{list-style-type:lower-alpha}ol ol ol{list-style-type:lower-roman}ol ul{list-style-type:disc}a{text-decoration:none;color:#f43059;text-decoration-skip:ink}a:hover,a:active,a:focus{text-decoration:underline}.link-secret{color:inherit;font-weight:400}.link-secret:hover,.link-secret:active,.link-secret:focus{color:#f43059;text-decoration:none}@supports (position: sticky){@media screen and (min-height: 75em){:target{scroll-margin-top:120px}}}li>ul,li>ol{margin-bottom:0}img{max-width:100%;font-style:italic;vertical-align:middle;height:auto;background-repeat:no-repeat;background-size:cover;shape-margin:0.75rem}video{max-width:100%;vertical-align:middle}q{font-style:italic;quotes:"‘" "’" "“" "”"}q:before{content:open-quote}q:after{content:close-quote}blockquote{margin-right:48px;margin-left:48px;font-style:italic;quotes:"“" "”"}blockquote p{text-indent:-0.45em}blockquote p:before{content:open-quote}blockquote p:after{content:no-close-quote}blockquote p:last-of-type{margin-bottom:0}blockquote p:last-of-type:after{content:close-quote}blockquote q{quotes:"‘" "’"}blockquote q:before{content:open-quote}blockquote q:after{content:close-quote}label{font-size:12px;font-size:.75rem;line-height:2;font-weight:600;cursor:pointer}table{width:100%;font-size:12px;font-size:.75rem;line-height:2;font-variant-numeric:tabular-nums}@media screen and (min-width: 45em){table{font-size:16px;font-size:1rem;line-height:1.5}}th{font-weight:600;text-align:left}th,td{padding:6px}details+details{padding-top:24px;border-top:1px solid #ddd}summary{color:#f43059;font-weight:600;cursor:pointer;text-decoration:none}summary:hover,summary:focus,[open]>summary{text-decoration:underline}.wrapper{max-width:1100px;margin:0 auto;padding-right:12px;padding-left:12px}@media screen and (min-width: 45em){.wrapper{padding-right:24px;padding-left:24px}}.wrapper--wide{max-width:1280px}.wrapper--unconstrained{max-width:none}.btn{display:inline-block;vertical-align:middle;font:inherit;text-align:center;margin:0;cursor:pointer;overflow:visible;padding:10px 22px;background-color:#f43059;border:2px solid #f43059;border-radius:3px}.btn,.btn:hover,.btn:active,.btn:focus{text-decoration:none;color:#fff}.btn::-moz-focus-inner{border:0;padding:0}.btn--small{padding:4px 10px}.btn--full{width:100%}.layout{list-style:none;margin:0;padding:0;margin-left:-24px}.layout__item{display:inline-block;padding-left:24px;vertical-align:top;width:100%}.layout--large{margin-left:-48px}.layout--large>.layout__item{padding-left:48px}.layout--middle>.layout__item{vertical-align:middle}.layout--bottom>.layout__item{vertical-align:bottom}.box{display:block;padding:24px}.box>:last-child{margin-bottom:0}.media{display:block}.media__img{float:left;margin-right:24px}.media__img>img{display:block}.media__body{overflow:hidden;display:block}.media__body,.media__body>:last-child{margin-bottom:0}.flag{display:table;width:100%}.flag__img,.flag__body{display:table-cell;vertical-align:middle}.flag__img{padding-right:24px}.flag__img>img{display:block;max-width:none}.flag__body{width:100%}.flag__body,.flag__body>:last-child{margin-bottom:0}.flag--rev{direction:rtl}.flag--rev>.flag__img,.flag--rev>.flag__body{direction:ltr}.flag--rev>.flag__img{padding-right:0;padding-left:24px}@media screen and (max-width: 479px){.flag--responsive{direction:ltr}.flag--responsive,.flag--responsive>.flag__img,.flag--responsive>.flag__body{display:block}.flag--responsive>.flag__img{padding-right:0;padding-left:0;margin-bottom:24px}}.flag__img{white-space:nowrap}.list-ui,.list-ui__item,.list-ui>li{border:0 solid #ddd}.list-ui{margin:0;padding:0;list-style:none;border-top-width:1px}.list-ui__item,.list-ui>li{padding:24px;border-bottom-width:1px}.list-ui--small>.list-ui__item,.list-ui.list-ui--small>li{padding:12px}.list-ui__title{display:block;font-size:12px;font-size:.75rem;line-height:2;font-weight:600;color:#999;margin-bottom:0}.o-list-inline{margin-left:0}.o-list-inline>li{display:inline-block}.o-list-inline--spaced>li{padding:0.75rem}.page-head{border-style:solid;border-width:6px 0 1px;border-top-color:#f43059;border-bottom-color:#f2f2f2;padding-top:12px;padding-bottom:12px;background-color:#fff}@media screen and (min-width: 45em){.page-head{padding-top:24px;padding-bottom:24px}}@supports (position: sticky){@media screen and (min-height: 75em){.page-head{position:sticky;top:0;background-color:rgba(255,255,255,0.95);z-index:2}}}.band{padding-top:24px;padding-bottom:24px;content-visibility:auto;contain-intrinsic-size:1px 1000px}@media screen and (min-width: 64em){.band{padding-top:48px;padding-bottom:48px}}.band--large{padding:48px}.band--highlight{background-color:#fff}.band--tint{background-color:#f2f2f2}.band--dark{background-color:#333}.band--dark,.band--dark a,.band--dark code{color:#fff}.band--attention{background-color:#f43059;color:#fff}.band__title{text-align:center;font-size:16px;font-size:1rem;line-height:1.5;text-transform:uppercase;letter-spacing:0.1em;color:rgba(255,255,255,0.5);-webkit-font-smoothing:auto}.promo{padding-bottom:0;text-align:center}@media screen and (min-width: 64em){.promo{padding-bottom:24px}}.promo__links{text-align:center}@media screen and (min-width: 45em){.promo__links-spacer{padding-right:1.5em;padding-left:1.5em}}.page-head--masthead .site-nav{position:absolute;top:0;right:12px;left:12px}@media screen and (min-width: 45em){.page-head--masthead .site-nav{top:0;right:24px;left:24px}}.site-nav__home-link{display:block;float:left;position:relative;z-index:3;will-change:transform}@media screen and (min-width: 45em){.site-nav__home-link{position:static}}.site-nav__logo{display:block}.site-nav__logo-fill{fill:#f43059;transition:fill 0.2s}.site-nav.is-open .site-nav__logo-fill{fill:#fff}.page-head--masthead .site-nav__logo-fill{fill:#fff}.site-nav__opener,.site-nav__closer{font-size:12px;font-size:.75rem;line-height:2;line-height:64px;text-transform:uppercase;font-weight:bold;padding:0;background:none;border:none}.site-nav__opener:hover,.site-nav__closer:hover,.site-nav__opener:focus,.site-nav__closer:focus{text-decoration:none}.site-nav__opener{color:#f43059;float:right}.page-head--masthead .site-nav__opener{color:#fff}@media screen and (min-width: 45em){.site-nav__opener{display:none}}.site-nav__list{margin:0;padding:0;list-style:none;position:fixed;top:54px;right:0;bottom:0;left:0;z-index:2;overflow:scroll;transform:translateX(100%);transition:0.2s;will-change:transform;background-color:#333;contain:layout;content-visibility:hidden}@supports (backdrop-filter: blur(5px)){.site-nav__list{background-color:rgba(51,51,51,0.8);backdrop-filter:blur(5px)}}.is-open .site-nav__list{content-visibility:visible;transform:none}@media screen and (min-width: 45em){.site-nav__list{content-visibility:visible;position:relative;top:auto;right:auto;bottom:auto;left:auto;float:right;background-color:transparent;overflow:visible;backdrop-filter:none;transform:none}}.site-nav__item{border-bottom:1px solid rgba(255,255,255,0.1)}.site-nav__item:last-child{border-bottom:none}@media screen and (min-width: 45em){.site-nav__item{position:relative;border-bottom:none;float:left}.site-nav__item.has-sub-menu>a{transition:0.2s}.site-nav__item.has-sub-menu:hover>a{transform:translateY(-2px)}.site-nav__sub-menu .site-nav__item{float:none}.site-nav__sub-menu .site-nav__item+.site-nav__item{border-top:1px solid #ddd}}.site-nav__item--closer{text-align:right}@media screen and (min-width: 45em){.site-nav__item--closer{display:none}}.site-nav__closer{width:100%;text-align:right}.site-nav__closer.site-nav__closer.site-nav__closer:hover,.site-nav__closer.site-nav__closer.site-nav__closer:active,.site-nav__closer.site-nav__closer.site-nav__closer:focus{text-decoration:none}.site-nav__sub-menu{list-style:none;margin-left:0;padding-left:24px}@media screen and (min-width: 45em){.site-nav__sub-menu{position:absolute;top:100%;left:50%;transform:translateX(-50%) translateY(-10px);z-index:1;padding-left:0;text-align:center;white-space:nowrap;background-color:#fff;border-radius:3px;border:1px solid #ddd;box-shadow:3px 3px 0 rgba(0,0,0,0.1);visibility:hidden;opacity:0;transition:0.333s}.site-nav__item:hover .site-nav__sub-menu{opacity:1;visibility:visible;will-change:opacity;transform:translateX(-50%) translateY(0)}.site-nav__sub-menu:before,.site-nav__sub-menu:after{content:"";position:absolute;bottom:100%;left:50%}.site-nav__sub-menu:before{border:12px solid transparent;border-bottom-color:#ddd;margin-left:-12px}.site-nav__sub-menu:after{border:11px solid transparent;border-bottom-color:#fff;margin-left:-11px}}.site-nav__link{display:block;padding:12px;color:#fff}.site-nav__link:hover,.site-nav__link:active,.site-nav__link:focus{text-decoration:underline;color:#fff}@media screen and (min-width: 45em){.site-nav__link{line-height:64px;color:#333;padding-top:0;padding-bottom:0}.site-nav__link:hover,.site-nav__link:active,.site-nav__link:focus{color:#f43059;text-decoration:none}}.page-head--masthead .site-nav__link,.page-head--masthead .site-nav__link:hover,.page-head--masthead .site-nav__link:active,.page-head--masthead .site-nav__link:focus{color:#fff}.page-head--masthead .site-nav__link:hover,.page-head--masthead .site-nav__link:active,.page-head--masthead .site-nav__link:focus{text-decoration:underline}.page--about .site-nav__about,.page--case-studies .site-nav__case-studies,.page--speaking .site-nav__speaking,.page--services .site-nav__services,.page--contact .site-nav__contact{color:#fff;text-decoration:underline}@media screen and (min-width: 45em){.page--about .site-nav__about,.page--case-studies .site-nav__case-studies,.page--speaking .site-nav__speaking,.page--services .site-nav__services,.page--contact .site-nav__contact{color:#f43059;position:relative;text-decoration:none}.page--about .site-nav__about:before,.page--case-studies .site-nav__case-studies:before,.page--speaking .site-nav__speaking:before,.page--services .site-nav__services:before,.page--contact .site-nav__contact:before,.page--about .site-nav__about:after,.page--case-studies .site-nav__case-studies:after,.page--speaking .site-nav__speaking:after,.page--services .site-nav__services:after,.page--contact .site-nav__contact:after{content:"";position:absolute;left:50%;pointer-events:none;border-style:solid;border-color:transparent}.page--about .site-nav__about:before,.page--case-studies .site-nav__case-studies:before,.page--speaking .site-nav__speaking:before,.page--services .site-nav__services:before,.page--contact .site-nav__contact:before{border-width:12px;border-bottom-color:#f2f2f2;margin-left:-12px;bottom:-24px}.page--about .site-nav__about:after,.page--case-studies .site-nav__case-studies:after,.page--speaking .site-nav__speaking:after,.page--services .site-nav__services:after,.page--contact .site-nav__contact:after{border-width:11px;border-bottom-color:#f9f9f9;margin-left:-11px;bottom:-25px}}.page--workshops .site-nav__workshops,.page--code-reviews .site-nav__code-reviews,.page--consultancy .site-nav__consultancy,.page--downloads .site-nav__downloads,.page--sentinel .site-nav__sentinel,.page--masterclasses .site-nav__masterclasses{text-decoration:underline}@media screen and (min-width: 45em){.page--workshops .site-nav__workshops,.page--code-reviews .site-nav__code-reviews,.page--consultancy .site-nav__consultancy,.page--downloads .site-nav__downloads,.page--sentinel .site-nav__sentinel,.page--masterclasses .site-nav__masterclasses{text-decoration:none;color:#f43059}}.page--home .site-nav__home{text-decoration:underline}.site-nav__sub-link{display:block;padding:12px;color:#fff}.site-nav__sub-link:hover,.site-nav__sub-link:active,.site-nav__sub-link:focus{text-decoration:underline}@media screen and (min-width: 45em){.site-nav__sub-link{font-weight:400;color:#333}.site-nav__sub-link:hover,.site-nav__sub-link:active,.site-nav__sub-link:focus{text-decoration:none;color:#f43059}}.c-nav-secondary{margin-left:0;list-style:none;display:table;table-layout:fixed;width:100%;text-align:center;font-size:12px;font-size:.75rem;line-height:1.833333333;border:1px solid #999;border-radius:3px}.c-nav-secondary__item{display:table-cell}.c-nav-secondary__item+.c-nav-secondary__item{border-left:1px solid #999}.c-nav-secondary__link{display:block;padding-top:12px;padding-bottom:12px;color:#333}.c-nav-secondary__link.is-current{color:#fff;background-color:#999;text-decoration:none;cursor:default;pointer-events:none}.btn{font-weight:600;transition:0.2s;position:relative;will-change:transform;z-index:1}.btn:hover,.btn:active,.btn:focus{background-color:#e50c39;border-color:#e50c39;transform:translateY(-1px)}.btn::before{content:"";position:absolute;top:0;right:0;bottom:0;left:0;box-shadow:0 4px 0 #000;opacity:0;will-change:opacity}.btn:hover::before,.btn:active::before,.btn:focus::before{opacity:0.1}.btn.btn.btn::after{display:none}.btn--secondary{background:none;color:#f43059}.btn--secondary:hover,.btn--secondary:active,.btn--secondary:focus{background:none;color:#e50c39}.btn--positive{font-weight:400}.btn--positive,.btn--positive:hover,.btn--positive:active,.btn--positive:focus{background-color:#3f990f;border-color:#3f990f}.btn--negative{font-weight:400}.btn--negative,.btn--negative:hover,.btn--negative:active,.btn--negative:focus{background-color:#9f102e;border-color:#9f102e}.box{border-radius:3px}.box--highlight{background-color:#fff;border-bottom:1px solid #ddd}.box--tint{background-color:#f2f2f2}.post__date{display:block;color:#999;margin-bottom:0}.post__title{margin-bottom:0}.post__title>a{font-weight:400}.pull-quote{font-size:18px;font-size:1.125rem;line-height:1.333333333;font-style:normal;color:#666;padding-left:12px;border-left:12px solid #f43059;border-radius:3px;margin-right:0;margin-left:0}@media screen and (min-width: 45em){.pull-quote{margin-left:-24px}}.pull-quote__source{font-style:normal}.pull-quote--banner{max-width:27em;padding:0 12px;text-indent:0;border-left:none;margin:0 auto;color:#333;font-style:normal}@media screen and (min-width: 45em){.pull-quote--banner{font-size:32px;font-size:2rem;line-height:1.4}}.pull-quote--context{font-size:20px;font-size:1.25rem;line-height:1.2;font-style:italic}@media screen and (min-width: 45em){.pull-quote--context{width:50%;float:left;margin-right:24px;padding-right:12px;padding-left:0;border-right:12px solid #f43059;border-left:none;text-align:right}}.pull-quote--context-alt{font-size:20px;font-size:1.25rem;line-height:1.2;font-style:italic}@media screen and (min-width: 45em){.pull-quote--context-alt{width:50%;float:right;margin-left:24px}}.annotate{position:relative}.annotate__image{display:block;border-radius:3px}.annotate__list{list-style:none;margin:0;position:absolute;top:0;right:0;bottom:0;left:0}.annotate__item{position:absolute;width:5%;height:5%;overflow:hidden;white-space:nowrap;text-indent:100%;cursor:help;border-width:2px;border-style:solid;border-color:transparent;border-radius:3px;transition:0.2s}.annotate:hover .annotate__item{border-color:#ccc;border-color:rgba(255,255,255,0.5)}.annotate:hover .annotate__item:hover{z-index:1;border-color:#fff}.annotate__caption{position:relative;z-index:1}.adpacks{min-height:216px}.carbon-wrap{display:block;text-align:center}.carbon-img{display:block;margin-bottom:6px}.carbon-img>img{border-radius:3px}.carbon-img:hover+.carbon-text{text-decoration:underline}.carbon-text{overflow:hidden;display:block;color:#666;text-wrap:balance}.carbon-poweredby{display:block;text-align:center;font-size:12px;font-size:.75rem;line-height:2;text-transform:uppercase;color:#999;letter-spacing:0.01em}.c-label{display:block}.c-input-text{border:2px solid #ddd;min-width:25ch;outline:none;transition:border-color 0.2s;padding:11px 6px;vertical-align:middle;border-radius:3px}.c-input-text:active,.c-input-text:focus{border-color:#f43059}.c-newsletter{text-align:center}.c-newsletter__title{font-size:16px;font-size:1rem;line-height:1.5;font-weight:600;color:inherit;margin-bottom:0}.c-newsletter__text{font-size:12px;font-size:.75rem;line-height:2}.c-newsletter__email{width:100%;margin-bottom:24px;text-align:center}@media screen and (min-width: 45em){.c-newsletter__email{width:auto;margin-bottom:0;min-width:40ch}}.c-newsletter__submit{width:100%}@media screen and (min-width: 45em){.c-newsletter__submit{width:auto}}.c-video{position:relative;padding-bottom:56.25%}@media screen and (min-width: 1380px){.c-video{padding-bottom:75%}}.c-video__media{position:absolute;top:0;left:0;width:100%;height:100%}.footnotes{font-size:12px;font-size:.75rem;line-height:2}.footnotes p{margin-bottom:0}.twitter-tweet-rendered{margin-top:0 !important;margin-right:auto;margin-bottom:24px !important;margin-left:auto}.c-youtube-embed{max-width:100%}.c-youtube-embed,.c-spotify-embed{display:block;margin-right:auto;margin-bottom:24px;margin-left:auto}.c-highlight{padding:24px;background-color:#ffc}.c-highlight,.c-highlight a{color:#36393a}.c-highlight::selection,.c-highlight ::selection{color:#ffc;background-color:#36393a}.c-highlight a{text-decoration:underline}.c-highlight>:last-child{margin-bottom:0}.c-highlight--small{padding:12px}.c-highlight--ribbon{text-align:center}.s-post p,.s-post li{text-wrap:pretty}.s-post thead{border-bottom:2px solid #ddd}.s-post tbody tr:nth-of-type(even){background-color:rgba(0,0,0,0.05)}.s-post iframe{max-width:100%;width:100%;border:none;margin-bottom:24px}.s-post :target{background-color:#ffc}.s-post>h2:nth-of-type(2) ~ p{content-visibility:auto;contain-intrinsic-size:1px 250px}.s-post a[href^="https://csswizardry.com"]::after,.s-post a[href^="/"]::after{content:"";display:inline-block;width:1ch;height:1ch;background-image:url(/icon.png);background-size:cover;margin-left:0.1ch;vertical-align:super}.s-post a:visited:not(.btn){color:#6a0dad}.clearfix:after,.box:after,.media:after,.site-nav:after{content:"";display:table;clear:both}.one-whole{width:100% !important}.one-half,.two-quarters,.three-sixths,.four-eighths,.five-tenths,.six-twelfths{width:50% !important}.one-third,.two-sixths,.three-ninths,.four-twelfths{width:33.3333333% !important}.two-thirds,.four-sixths,.six-ninths,.eight-twelfths{width:66.6666666% !important}.one-quarter,.two-eighths,.three-twelfths{width:25% !important}.three-quarters,.six-eighths,.nine-twelfths{width:75% !important}.one-fifth,.two-tenths{width:20% !important}.two-fifths,.four-tenths{width:40% !important}.three-fifths,.six-tenths{width:60% !important}.four-fifths,.eight-tenths{width:80% !important}.one-sixth,.two-twelfths{width:16.6666666% !important}.five-sixths,.ten-twelfths{width:83.3333333% !important}.one-eighth{width:12.5% !important}.three-eighths{width:37.5% !important}.five-eighths{width:62.5% !important}.seven-eighths{width:87.5% !important}.one-ninth{width:11.1111111% !important}.two-ninths{width:22.2222222% !important}.four-ninths{width:44.4444444% !important}.five-ninths{width:55.5555555% !important}.seven-ninths{width:77.7777777% !important}.eight-ninths{width:88.8888888% !important}.one-tenth{width:10% !important}.three-tenths{width:30% !important}.seven-tenths{width:70% !important}.nine-tenths{width:90% !important}.one-twelfth{width:8.3333333% !important}.five-twelfths{width:41.6666666% !important}.seven-twelfths{width:58.3333333% !important}.eleven-twelfths{width:91.6666666% !important}@media screen and (min-width: 45em){.lap-and-up-one-half{width:50%}.lap-and-up-one-quarter{width:25%}}@media screen and (min-width: 64em){.desk-one-third{width:33.333333333%}.desk-two-thirds{width:66.666666666%}.desk-three-fifths{width:60%}.desk-three-tenths{width:30%}.desk-push-one-tenth{position:relative;left:10%}}.m{margin:24px !important}.mt{margin-top:24px !important}.mr{margin-right:24px !important}.mb{margin-bottom:24px !important}.ml{margin-left:24px !important}.mh{margin-right:24px !important;margin-left:24px !important}.mv{margin-top:24px !important;margin-bottom:24px !important}.m\+\+{margin:96px !important}.mt\+\+{margin-top:96px !important}.mr\+\+{margin-right:96px !important}.mb\+\+{margin-bottom:96px !important}.ml\+\+{margin-left:96px !important}.mh\+\+{margin-right:96px !important;margin-left:96px !important}.mv\+\+{margin-top:96px !important;margin-bottom:96px !important}.m0{margin:0 !important}.mt0{margin-top:0 !important}.mr0{margin-right:0 !important}.mb0{margin-bottom:0 !important}.ml0{margin-left:0 !important}.mh0{margin-right:0 !important;margin-left:0 !important}.mv0{margin-top:0 !important;margin-bottom:0 !important}.hide{position:absolute !important;height:1px;width:1px;overflow:hidden;clip:rect(1px 1px 1px 1px);clip:rect(1px, 1px, 1px, 1px)}@media screen and (max-width: 44.9375em){.hide-palm{position:absolute !important;height:1px;width:1px;overflow:hidden;clip:rect(1px 1px 1px 1px);clip:rect(1px, 1px, 1px, 1px)}}@media screen and (min-width: 45em) and (max-width: 63.9375em){.hide-lap{position:absolute !important;height:1px;width:1px;overflow:hidden;clip:rect(1px 1px 1px 1px);clip:rect(1px, 1px, 1px, 1px)}}@media screen and (min-width: 45em){.hide-lap-and-up{position:absolute !important;height:1px;width:1px;overflow:hidden;clip:rect(1px 1px 1px 1px);clip:rect(1px, 1px, 1px, 1px)}}@media screen and (max-width: 63.9375em){.hide-portable{position:absolute !important;height:1px;width:1px;overflow:hidden;clip:rect(1px 1px 1px 1px);clip:rect(1px, 1px, 1px, 1px)}}@media screen and (min-width: 64em){.hide-desk{position:absolute !important;height:1px;width:1px;overflow:hidden;clip:rect(1px 1px 1px 1px);clip:rect(1px, 1px, 1px, 1px)}}::selection{color:#fff;background-color:#f43059}.u-text-prominent{font-size:22px;font-size:1.375rem;line-height:1.090909091;font-weight:600;text-align:center}.u-color-brand{color:#f43059 !important}.u-color-positive{color:#3f990f !important}.u-color-negative{color:#9f102e !important} /*! * inuitcss, by @csswizardry * * github.com/inuitcss | inuitcss.com */.highlight{background-color:#002b36;color:#93a1a1;border-radius:3px}@media screen and (min-width: 45em){.highlight{margin-right:-12px;margin-left:-12px}}.highlight pre{background-color:transparent;margin-right:auto;margin-left:auto}.highlight .c{color:#586e75}.highlight .err{color:#93a1a1}.highlight .g{color:#93a1a1}.highlight .k{color:#859900}.highlight .l{color:#93a1a1}.highlight .n{color:#93a1a1}.highlight .o{color:#859900}.highlight .x{color:#cb4b16}.highlight .p{color:#93a1a1}.highlight .cm{color:#586e75}.highlight .cp{color:#859900}.highlight .c1{color:#586e75}.highlight .cs{color:#859900}.highlight .gd{color:#2aa198}.highlight .ge{color:#93a1a1;font-style:italic}.highlight .gr{color:#dc322f}.highlight .gh{color:#cb4b16}.highlight .gi{color:#859900}.highlight .go{color:#93a1a1}.highlight .gp{color:#93a1a1}.highlight .gs{color:#93a1a1;font-weight:bold}.highlight .gu{color:#cb4b16}.highlight .gt{color:#93a1a1}.highlight .kc{color:#cb4b16}.highlight .kd{color:#268bd2}.highlight .kn{color:#859900}.highlight .kp{color:#859900}.highlight .kr{color:#268bd2}.highlight .kt{color:#dc322f}.highlight .ld{color:#93a1a1}.highlight .m{color:#2aa198}.highlight .s{color:#2aa198}.highlight .na{color:#93a1a1}.highlight .nb{color:#B58900}.highlight .nc{color:#268bd2}.highlight .no{color:#cb4b16}.highlight .nd{color:#268bd2}.highlight .ni{color:#cb4b16}.highlight .ne{color:#cb4b16}.highlight .nf{color:#268bd2}.highlight .nl{color:#93a1a1}.highlight .nn{color:#93a1a1}.highlight .nx{color:#93a1a1}.highlight .py{color:#93a1a1}.highlight .nt{color:#268bd2}.highlight .nv{color:#268bd2}.highlight .ow{color:#859900}.highlight .w{color:#93a1a1}.highlight .mf{color:#2aa198}.highlight .mh{color:#2aa198}.highlight .mi{color:#2aa198}.highlight .mo{color:#2aa198}.highlight .sb{color:#586e75}.highlight .sc{color:#2aa198}.highlight .sd{color:#93a1a1}.highlight .s2{color:#2aa198}.highlight .se{color:#cb4b16}.highlight .sh{color:#93a1a1}.highlight .si{color:#2aa198}.highlight .sx{color:#2aa198}.highlight .sr{color:#dc322f}.highlight .s1{color:#2aa198}.highlight .ss{color:#2aa198}.highlight .bp{color:#268bd2}.highlight .vc{color:#268bd2}.highlight .vg{color:#268bd2}.highlight .vi{color:#268bd2}.highlight .il{color:#2aa198}.highlight .m{margin:0 !important}.highlight .p{padding:0 !important} /*# sourceMappingURL=components.syntax.css.map */ </style> <script> performance.mark('cssEnd'); performance.measure('cssTime', 'cssStart', 'cssEnd'); </script> <script src="https://www.googletagmanager.com/gtag/js?id=G-DNCL1RY4GT" async></script> <script> window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'G-DNCL1RY4GT'); </script> <script src=https://cdn.speedcurve.com/js/lux.js?id=47684395 defer></script> <script src=https://platform.twitter.com/widgets.js defer></script> <meta name=twitter:card content=summary_large_image> <meta name=twitter:site content=@csswizardry> <meta name=twitter:domain content=csswizardry.com> <meta name=twitter:creator content=@csswizardry> <meta name=twitter:title property=og:title content="Cache-Control for Civilians – CSS Wizardry"> <meta name=twitter:description property=og:description content="What does Cache-Control really do? In basic terms? Let’s find out!"> <meta name=twitter:image property=og:image content=https://csswizardry.com/img/css/masthead-small.jpg> <link rel=alternate type=application/atom+xml href=https://feeds.feedburner.com/csswizardry> <link rel=icon href=/icon.png> <link rel=apple-touch-icon href=/icon.png> <meta name=apple-mobile-web-app-title content=csswizardry> <meta name=description content="What does Cache-Control really do? In basic terms? Let’s find out!"> <link rel=canonical href="https://csswizardry.com/2019/03/cache-control-for-civilians/"> <meta property=og:url content="https://csswizardry.com/2019/03/cache-control-for-civilians/"> <meta name=theme-color content=#f43059> <link rel=manifest href=/manifest.json> <meta name=format-detection content="telephone=no"> <script> performance.mark('headEnd'); performance.measure('headTime', 'headStart', 'headEnd'); </script> <nav class=hide> <a href=" #section:main ">Skip to main content</a> </nav> <section class="c-highlight c-highlight--small c-highlight--ribbon"> <a href="/masterclasses/">Arrange a Masterclass</a> </section> <header class=page-head> <div class=wrapper> <nav class=site-nav id=jsSiteNav data-sctrack=site-nav> <a href=/ class=site-nav__home-link data-sctrack=logo id=logo elementtiming=siteLogo> <svg class=site-nav__logo width=64 height=64 xmlns="http://www.w3.org/2000/svg"><title>CSS Wizardry</title><path d="M.234 44.003v-41.002c0-1.664 1.344-3.001 3.001-3.001h57.998c1.664 0 3.001 1.344 3.001 3.001v39.536c-1.803-1.102-3.911-1.653-6.324-1.653-3.356 0-5.9.788-7.632 2.363-1.732 1.576-2.598 3.554-2.598 5.936 0 2.61.896 4.528 2.688 5.755 1.058.734 2.971 1.413 5.737 2.039l2.814.631c1.648.361 2.857.776 3.626 1.245.77.481 1.155 1.161 1.155 2.039 0 1.503-.776 2.532-2.327 3.085l-.063.022-.078.001h-5.853c-.773-.274-1.385-.684-1.836-1.232-.493-.601-.824-1.509-.992-2.724h-5.304c0 1.491.288 2.809.864 3.956h-4.483c.524-1.058.786-2.232.786-3.523 0-2.369-.794-4.18-2.381-5.43-1.022-.806-2.538-1.449-4.546-1.93l-4.583-1.101c-1.768-.421-2.923-.788-3.464-1.101-.842-.469-1.263-1.179-1.263-2.129 0-1.034.427-1.84 1.281-2.418.854-.577 1.997-.866 3.428-.866 1.287 0 2.363.223 3.229.668 1.299.674 1.997 1.81 2.093 3.41h5.34c-.096-2.827-1.134-4.982-3.112-6.468-1.979-1.485-4.369-2.228-7.172-2.228-3.356 0-5.9.788-7.632 2.363-1.732 1.576-2.598 3.554-2.598 5.936 0 2.61.896 4.528 2.688 5.755 1.058.734 2.971 1.413 5.737 2.039l2.814.631c1.648.361 2.857.776 3.626 1.245.77.481 1.155 1.161 1.155 2.039 0 1.503-.776 2.532-2.327 3.085l-.066.023h-5.928c-.773-.274-1.385-.684-1.836-1.232-.493-.601-.824-1.509-.992-2.724h-5.304c0 1.491.288 2.809.864 3.956h-5.604c.887-1.39 1.481-2.991 1.781-4.804h-5.503c-.373 1.371-.842 2.4-1.407 3.085-.697.869-1.609 1.442-2.736 1.719h-3.314c-1.201-.309-2.236-.987-3.106-2.034-1.233-1.485-1.849-3.72-1.849-6.702 0-2.983.583-5.289 1.75-6.919s2.76-2.445 4.781-2.445c1.985 0 3.482.577 4.492 1.732.565.649 1.028 1.612 1.389 2.887h5.557c-.084-1.66-.698-3.314-1.84-4.961-2.069-2.935-5.34-4.402-9.815-4.402-3.114 0-5.717.949-7.809 2.848zm64 9.742v-4.165h-1.381c-.096-1.6-.794-2.736-2.093-3.41-.866-.445-1.942-.668-3.229-.668-1.431 0-2.574.289-3.428.866-.854.577-1.281 1.383-1.281 2.418 0 .95.421 1.66 1.263 2.129.541.313 1.696.68 3.464 1.101l4.583 1.101c.774.185 1.475.395 2.103.629z" fill="#f43059" class="site-nav__logo-fill"/></svg> </a> <button id=jsSiteNavOpener class=site-nav__opener data-sctrack=hamburger>Menu</button> <ul class=site-nav__list id=jsSiteNavList> <li class="site-nav__item site-nav__item--closer"> <button id=jsSiteNavCloser class="site-nav__link site-nav__closer">Close</button> </li> <li class=site-nav__item><a href=/ class="site-nav__link site-nav__home">Home</a></li> <li class=site-nav__item><a href=/about/ class="site-nav__link site-nav__about">About Me</a></li> <li class="site-nav__item has-sub-menu"> <a href=/services/ class="site-nav__link site-nav__services">My Services</a> <ul class="site-nav__sub-menu"> <li class=site-nav__item><a href=/masterclasses/ class="site-nav__sub-link site-nav__masterclasses">Masterclasses <sup style="text-transform: uppercase; font-size: 0.625rem; line-height: 1.2; color: #f43059;"><strong>New</strong></sup></a></li> <li class=site-nav__item><a href=/sentinel/ class="site-nav__sub-link site-nav__sentinel">Sentinel</a></li> <li class=site-nav__item><a href=/workshops/ class="site-nav__sub-link site-nav__workshops">Workshops and Training</a></li> <li class=site-nav__item><a href=/consultancy/ class="site-nav__sub-link site-nav__consultancy">Consultancy</a></li> <li class=site-nav__item><a href=/code-reviews/ class="site-nav__sub-link site-nav__code-reviews">Performance Audits</a></li> <li class=site-nav__item><a href=/downloads/ class="site-nav__sub-link site-nav__downloads">Downloads</a></li> </ul> </li> <li class=site-nav__item><a href=/speaking/ class="site-nav__link site-nav__speaking">Speaking</a></li> <li class="site-nav__item has-sub-menu"> <a href=/case-studies/ class="site-nav__link site-nav__case-studies">Case Studies</a> <ul class=site-nav__sub-menu> <li class=site-nav__item><a href=/testimonials/ class="site-nav__sub-link site-nav__testimonials">Testimonials</a></li> </ul> </li> <li class=site-nav__item><a href=/contact/ class="site-nav__link site-nav__contact">Contact Me</a></li> </ul> </nav> <script> (function() { const site = document.documentElement; const siteNav = document.getElementById('jsSiteNav'); const siteNavList = document.getElementById('jsSiteNavList'); const siteNavOpener = document.getElementById('jsSiteNavOpener'); const siteNavCloser = document.getElementById('jsSiteNavCloser'); siteNavOpener.addEventListener('click', (event) => { site.style.overflow='hidden'; siteNav.classList.add('is-open'); }); function siteNavClose() { site.style.overflow=null; siteNav.classList.remove('is-open'); }; siteNavCloser.addEventListener('click', (event) => { siteNavClose(); }); window.addEventListener('pagehide', (event) => { siteNavClose(); }); }()); </script> </div> </header> <div class=wrapper> <div class=layout> <section class="layout__item desk-three-fifths s-post" data-ui-component="Main content" itemscope itemtype=http://schema.org/BlogPosting id=section:main> <script>performance.mark('contentStart')</script> <time class=post__date datetime=2019-03-04 itemprop=datePublished>4 March, 2019</time> <h1 itemprop="name headline" elementtiming=page-title style="view-transition-name: x-2019-03-04">Cache-Control for Civilians</h1> <p class=hide>Written by <b itemprop=author>Harry Roberts</b> on <b itemprop=publisher>CSS Wizardry</b>.</p> <details class=c-toc> <summary>Table of Contents</summary> <ol> <li><a href="#cache-control"><code class="language-plaintext highlighter-rouge">Cache-Control</code></a></li> <li><a href="#public-and-private"><code class="language-plaintext highlighter-rouge">public</code> and <code class="language-plaintext highlighter-rouge">private</code></a></li> <li><a href="#max-age"><code class="language-plaintext highlighter-rouge">max-age</code></a> <ol> <li><a href="#s-maxage"><code class="language-plaintext highlighter-rouge">s-maxage</code></a></li> </ol> </li> <li><a href="#no-store"><code class="language-plaintext highlighter-rouge">no-store</code></a></li> <li><a href="#no-cache"><code class="language-plaintext highlighter-rouge">no-cache</code></a></li> <li><a href="#must-revalidate"><code class="language-plaintext highlighter-rouge">must-revalidate</code></a> <ol> <li><a href="#proxy-revalidate"><code class="language-plaintext highlighter-rouge">proxy-revalidate</code></a></li> </ol> </li> <li><a href="#immutable"><code class="language-plaintext highlighter-rouge">immutable</code></a></li> <li><a href="#stale-while-revalidate"><code class="language-plaintext highlighter-rouge">stale-while-revalidate</code></a></li> <li><a href="#stale-if-error"><code class="language-plaintext highlighter-rouge">stale-if-error</code></a></li> <li><a href="#no-transform"><code class="language-plaintext highlighter-rouge">no-transform</code></a></li> <li><a href="#cache-busting">Cache Busting</a> <ol> <li><a href="#no-cache-busting--stylecss">No Cache Busting – <code class="language-plaintext highlighter-rouge">style.css</code></a></li> <li><a href="#query-string--stylecssv1214">Query String – <code class="language-plaintext highlighter-rouge">style.css?v=1.2.14</code></a></li> <li><a href="#fingerprint--styleae3f66css">Fingerprint – <code class="language-plaintext highlighter-rouge">style.ae3f66.css</code></a> <ol> <li><a href="#implementation-detail">Implementation Detail</a></li> </ol> </li> <li><a href="#clear-site-data"><code class="language-plaintext highlighter-rouge">Clear-Site-Data</code></a></li> </ol> </li> <li><a href="#examples-and-recipes">Examples and Recipes</a> <ol> <li><a href="#online-banking-page">Online Banking Page</a></li> <li><a href="#live-train-timetable-page">Live Train Timetable Page</a></li> <li><a href="#faqs-page">FAQs Page</a></li> <li><a href="#static-js-or-css-app-bundle">Static JS (or CSS) App Bundle</a></li> <li><a href="#decorative-image">Decorative Image</a></li> </ol> </li> <li><a href="#key-things-to-remember">Key Things to Remember</a></li> <li><a href="#resources-and-related-reading">Resources and Related Reading</a> <ol> <li><a href="#do-as-i-say-not-as-i-do">Do as I Say, Not as I Do</a></li> </ol> </li> </ol> </details> <p class="c-highlight">Try out the <a href="/max-age/"><code>max-age</code> calculator</a>.</p> <p>The best request is the one that never happens: in the fight for fast websites, avoiding the network is far better than hitting the network at all. To this end, having a solid caching strategy can make all the difference for your visitors.</p> <p>That being said, more and more often in my work I see lots of opportunities being left on the table through unconsidered or even completely overlooked caching practices. Perhaps it’s down to the heavy focus on first-time visits, or perhaps it’s a simple lack of awareness and knowledge? Whatever it is, let’s have a bit of a refresher.</p> <p>Let’s look at <code class="language-plaintext highlighter-rouge">Cache-Control</code> in simple terms, and how we can we can best utilise it. Let’s look at <code class="language-plaintext highlighter-rouge">Cache-Control</code> for civilians.</p> <hr /> <h2 id="cache-control"><code class="language-plaintext highlighter-rouge">Cache-Control</code></h2> <p>One of the most common and effective ways to manage the caching of your assets is via the <code class="language-plaintext highlighter-rouge">Cache-Control</code> HTTP header. This header applies to individual assets, meaning everything on our pages can have a very bespoke and granular cache policy. The amount of control we’re granted makes for very intricate and powerful caching strategies.</p> <p>A <code class="language-plaintext highlighter-rouge">Cache-Control</code> header might look something like this:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Cache-Control: public, max-age=31536000 </code></pre></div></div> <p><code class="language-plaintext highlighter-rouge">Cache-Control</code> is the header, and each of <code class="language-plaintext highlighter-rouge">public</code> and <code class="language-plaintext highlighter-rouge">max-age=31536000</code> are <em>directives</em>. The <code class="language-plaintext highlighter-rouge">Cache-Control</code> header can accept one or more directives, and it is these directives, what they really mean, and their optimum use-cases that I want to cover in this post.</p> <hr /> <h2 id="public-and-private"><code class="language-plaintext highlighter-rouge">public</code> and <code class="language-plaintext highlighter-rouge">private</code></h2> <p><code class="language-plaintext highlighter-rouge">public</code> means that any caches may store a copy of the response. This includes CDNs, proxy servers, and the like. The <code class="language-plaintext highlighter-rouge">public</code> directive is often redundant, as the presence of other directives (such as <code class="language-plaintext highlighter-rouge">max-age</code>) are implicit instructions that caches may store a copy. Further, the presence of <code class="language-plaintext highlighter-rouge">public</code> on requests with an <code class="language-plaintext highlighter-rouge">Authorization</code> header <em>will</em> cause the response to be stored in public caches which you really, really do not want:</p> <blockquote> <p>Responses for requests with <code class="language-plaintext highlighter-rouge">Authorization</code> header fields must not be stored in a shared cache; however, the <code class="language-plaintext highlighter-rouge">public</code> directive will cause such responses to be stored in a shared cache.</p> </blockquote> <p><small><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#public"><code class="language-plaintext highlighter-rouge">Cache-Control</code> on MDN.</a></small></p> <p><code class="language-plaintext highlighter-rouge">private</code>, on the other hand, is an explicit instruction that only the end recipient of the response (the <em>client</em>, or <em>the browser</em>) may store a copy of the file. While <code class="language-plaintext highlighter-rouge">private</code> isn’t a security feature in and of itself, it is intended to prevent public caches (such as a CDN) storing a response that contains information unique to one user.</p> <hr /> <h2 id="max-age"><code class="language-plaintext highlighter-rouge">max-age</code></h2> <p class="c-highlight">Want to know the largest valid value for a <code>max-age</code> directive? <a href="/2023/10/what-is-the-maximum-max-age/">I wrote all about it in 2023!</a></p> <p><code class="language-plaintext highlighter-rouge">max-age</code> defines a unit of time in seconds (relative to the time of the request) for which the response is deemed ‘fresh’.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Cache-Control: max-age=60 </code></pre></div></div> <p>This <code class="language-plaintext highlighter-rouge">Cache-Control</code> header tells the browser that it can use this file from the cache for the next 60 seconds without having to worry about revalidating it. Once the 60 seconds is up, the browser will head back to the server to revalidate the file.</p> <p>If the server has a new file for the browser to download, it will respond with a <code class="language-plaintext highlighter-rouge">200</code> response, download the new file, the old file will be ejected from the HTTP cache, the new file will replace it, and will honour its caching headers.</p> <p>If the server doesn’t have a fresher copy that needs downloading, the server responds with a <code class="language-plaintext highlighter-rouge">304</code> response, doesn’t need to download any new file, and will update the cached copy with the new headers. This means that, if the <code class="language-plaintext highlighter-rouge">Cache-Control: max-age=60</code> header is still present, the cached file’s 60 seconds starts again. 120 seconds overall cache time for one file.</p> <p>Note that, in certain scenarios, caches are permitted to continue to serve stale responses after the <code class="language-plaintext highlighter-rouge">max-age</code> limit has been passed:</p> <blockquote> <p>HTTP allows caches to reuse <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#fresh_and_stale_based_on_age">stale responses</a> when they are disconnected from the origin server. <code class="language-plaintext highlighter-rouge">must-revalidate</code> is a way to prevent this from happening – either the stored response is revalidated with the origin server or a 504 (Gateway Timeout) response is generated.<br /> — <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control">Cache-Control</a></p> </blockquote> <p>In order to prevent this from happening, we can augment <code class="language-plaintext highlighter-rouge">max-age</code> with a number of the following directives.</p> <h3 id="s-maxage"><code class="language-plaintext highlighter-rouge">s-maxage</code></h3> <p>The <code class="language-plaintext highlighter-rouge">s-maxage</code> (note the absence of the <code class="language-plaintext highlighter-rouge">-</code> between <code class="language-plaintext highlighter-rouge">max</code> and <code class="language-plaintext highlighter-rouge">age</code>) will take precedence over the <code class="language-plaintext highlighter-rouge">max-age</code> directive but only in the context of shared caches. Using <code class="language-plaintext highlighter-rouge">max-age</code> and <code class="language-plaintext highlighter-rouge">s-maxage</code> in conjunction allows you to have different fresh durations for private and public caches (e.g. proxies, CDNs) respectively.</p> <hr /> <h2 id="no-store"><code class="language-plaintext highlighter-rouge">no-store</code></h2> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Cache-Control: no-store </code></pre></div></div> <p>What if we don’t want to cache a file? What if the file contains sensitive information? Perhaps it’s an HTML page that contains your bank details? Or maybe the information is time-critical? Perhaps a page that contains realtime stock prices? We don’t want to store or serve any responses like this from cache at all: we always want to discard sensitive information and fetch the freshest realtime information. Now we’d use <code class="language-plaintext highlighter-rouge">no-store</code>.</p> <p><code class="language-plaintext highlighter-rouge">no-store</code> is a very strong directive not to persist any information to any cache, private or otherwise. Any asset that carries the <code class="language-plaintext highlighter-rouge">no-store</code> directive will always hit the network, no matter what.</p> <hr /> <h2 id="no-cache"><code class="language-plaintext highlighter-rouge">no-cache</code></h2> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Cache-Control: no-cache </code></pre></div></div> <p>This is the one that trips most people up… <code class="language-plaintext highlighter-rouge">no-cache</code> doesn’t mean ‘no cache’. It means ‘do <code class="language-plaintext highlighter-rouge">no</code>t serve a copy from <code class="language-plaintext highlighter-rouge">cache</code> until you’ve revalidated it with the server and the server said you can use the cached copy’. Right. Sounds like this should be called <code class="language-plaintext highlighter-rouge">must-revalidate</code>! Except that’s not what it sounds like, either.</p> <p><code class="language-plaintext highlighter-rouge">no-cache</code> is actually a pretty smart way of always guaranteeing the freshest content, but also being able to use the much faster cached copy if possible. <code class="language-plaintext highlighter-rouge">no-cache</code> will always hit the network as it has to revalidate with the server before it can release the browser’s cached copy (unless the server responds with a fresher response), but if the server responds favourably, the network transfer is only a file’s headers: the body can be grabbed from cache rather than redownloaded.</p> <p>So, like I say, this is a smart way to combine freshness and the possibility of getting a file from cache, but it will hit the network for at least an HTTP header response.</p> <p>A good use-case for <code class="language-plaintext highlighter-rouge">no-cache</code> would be almost any dynamic HTML page. Think of a news site’s homepage: it’s not realtime, nor does it contain any sensitive information, but ideally we’d like the page to always show the freshest content. We can use <code class="language-plaintext highlighter-rouge">cache-control: no-cache</code> to instruct the browser to check back with the server first, and if the server has nothing newer to offer (<code class="language-plaintext highlighter-rouge">304</code>), let’s reuse the cached version. In the event that the server did have some fresher content, it would respond as such (<code class="language-plaintext highlighter-rouge">200</code>) and send the newer file.</p> <p><strong>Tip:</strong> There is no use sending a <code class="language-plaintext highlighter-rouge">max-age</code> directive alongside a <code class="language-plaintext highlighter-rouge">no-cache</code> directive as the time-limit for revalidation is zero seconds.</p> <hr /> <h2 id="must-revalidate"><code class="language-plaintext highlighter-rouge">must-revalidate</code></h2> <p>Even more confusingly, while the above sounds like it should be called <code class="language-plaintext highlighter-rouge">must-revalidate</code>, it turns out <code class="language-plaintext highlighter-rouge">must-revalidate</code> is something different still (but still similar).</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Cache-Control: must-revalidate, max-age=600 </code></pre></div></div> <p><code class="language-plaintext highlighter-rouge">must-revalidate</code> needs an associated <code class="language-plaintext highlighter-rouge">max-age</code> directive; above, we’ve set it to ten minutes.</p> <p>Where <code class="language-plaintext highlighter-rouge">no-cache</code> will immediately revalidate with the server, and only use a cached copy if the server says it may, <code class="language-plaintext highlighter-rouge">must-revalidate</code> is like <code class="language-plaintext highlighter-rouge">no-cache</code> with a grace period. What happens here is that, for the first ten minutes, the browser will <em>not</em> (I know, I know…) revalidate with the server, but the moment that ten minutes passes, it’s back to the server we go. If the server has nothing new for us, it responds with a <code class="language-plaintext highlighter-rouge">304</code> and the new <code class="language-plaintext highlighter-rouge">Cache-Control</code> headers are applied to the cached file—our ten minutes starts again. If, after ten minutes, there is a newer file on the server, we get a <code class="language-plaintext highlighter-rouge">200</code> response and its body, and the local cache gets updated.</p> <p>A great candidate for <code class="language-plaintext highlighter-rouge">must-revalidate</code> is a blog like mine: static pages that seldom change. Sure, the latest content is desirable, but given how infrequently my site changes, we don’t need anything as heavy handed as <code class="language-plaintext highlighter-rouge">no-cache</code>. Instead, let’s assume everything is going to be good enough for ten minutes, then revalidate after that.</p> <h3 id="proxy-revalidate"><code class="language-plaintext highlighter-rouge">proxy-revalidate</code></h3> <p>In a similar vein to <code class="language-plaintext highlighter-rouge">s-maxage</code>, <code class="language-plaintext highlighter-rouge">proxy-revalidate</code> is the public-cache specific version of <code class="language-plaintext highlighter-rouge">must-revalidate</code>. It is simply ignored by private caches.</p> <hr /> <h2 id="immutable"><code class="language-plaintext highlighter-rouge">immutable</code></h2> <p><code class="language-plaintext highlighter-rouge">immutable</code> is a pretty new and very neat directive that tells the browser a little more about the type of file we’ve sent it—is its content mutable or immutable? But, before we look at what <code class="language-plaintext highlighter-rouge">immutable</code> does, let’s look at the problem it’s solving:</p> <p>A user refresh causes the browser to revalidate a file regardless of its freshness because a user refresh usually means one of two things:</p> <ol> <li>The page looks broken, or;</li> <li>content looks out of date…</li> </ol> <p>…so let’s check if there’s anything more up to date on the server.</p> <p>If there is a newer file available on the server, we definitely want to download it. As such, we’ll get a <code class="language-plaintext highlighter-rouge">200</code> response, a fresh file, and—hopefully—the issue is fixed. If, however, there wasn’t a new file on the server, we’ll bring back a <code class="language-plaintext highlighter-rouge">304</code> header, no new file, but an entire roundtrip of latency. If we’re revalidating many files that result in many <code class="language-plaintext highlighter-rouge">304</code>s, that can add up to hundreds of milliseconds of unnecessary overhead.</p> <p><code class="language-plaintext highlighter-rouge">immutable</code> is a way of telling the browser that a file will never change—it’s <em>immutable</em>—and therefore never to bother revalidating it. We can completely cut out the overhead of a roundtrip of latency. What do we mean by a mutable or immutable file?</p> <ul> <li><code class="language-plaintext highlighter-rouge">style.css</code>: When we change the contents of this file, we don’t change its name at all. The file always exists, and its content always changes. This file is mutable.</li> <li><code class="language-plaintext highlighter-rouge">style.ae3f66.css</code>: This file is unique—it is named with a fingerprint based on its content, so the moment that content changes, we get a whole new file. This file is immutable.</li> </ul> <p><small>We’ll discuss this in more detail in the <a href="#cache-busting">Cache Busting</a> section.</small></p> <p>If we can somehow communicate to the browser that our file is immutable—that its content never changes—then we can also let the browser know that it needn’t bother checking for a fresher version: there would never be a fresher version as the file simply ceases to exist the moment its content changes.</p> <p>This is exactly what the <code class="language-plaintext highlighter-rouge">immutable</code> directive does:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Cache-Control: max-age=31536000, immutable </code></pre></div></div> <p>In browsers that support <code class="language-plaintext highlighter-rouge">immutable</code>, a user refresh will never cause a revalidation within the 31,536,000-second freshness lifespan. This means no unnecessary roundtrips spent retrieving <code class="language-plaintext highlighter-rouge">304</code> responses, which potentially saves us a lot of latency on the critical path (<a href="/2018/11/css-and-network-performance/">CSS blocks rendering</a>). On high latency connections, this saving could be tangible.</p> <p><strong>Beware:</strong> You should not apply <code class="language-plaintext highlighter-rouge">immutable</code> to any files that are not immutable. You should also have a very robust cache busting strategy in place so that you don’t inadvertently aggressively cache a file to which <code class="language-plaintext highlighter-rouge">immutable</code> has been applied.</p> <hr /> <h2 id="stale-while-revalidate"><code class="language-plaintext highlighter-rouge">stale-while-revalidate</code></h2> <p>I really, really wish there was better support for <code class="language-plaintext highlighter-rouge">stale-while-revalidate</code>.</p> <p>We’ve talked a lot so far about revalidation: the process of the browser making the trip back to the server to check whether a fresher file might be available. On high latency connections, the duration of revalidation alone can be noticeable, and that time is simply dead time—until we’ve heard from the server, we can neither release a cached copy (<code class="language-plaintext highlighter-rouge">304</code>) or download the new file (<code class="language-plaintext highlighter-rouge">200</code>).</p> <p>What <code class="language-plaintext highlighter-rouge">stale-while-revalidate</code> provides is a grace period (defined by us) in which the browser is permitted to use an out of date (stale) asset while we’re checking for a newer version.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Cache-Control: max-age=31536000, stale-while-revalidate=86400 </code></pre></div></div> <p>This is telling the browser, ‘this file is good to use for a year, but after that year is up, you have one extra day in which you may continue to serve this stale resource while you revalidate it in the background’.</p> <p><code class="language-plaintext highlighter-rouge">stale-while-revalidate</code> is a great directive for non-critical resources that, sure, we’d like the freshest version, but we know there’ll be no damage caused if we use the stale response once more while we’re checking for updates.</p> <hr /> <h2 id="stale-if-error"><code class="language-plaintext highlighter-rouge">stale-if-error</code></h2> <p>In a similar manner to <code class="language-plaintext highlighter-rouge">stale-while-revalidate</code>, <code class="language-plaintext highlighter-rouge">stale-if-error</code> allows the browser a grace period in which it can permissibly return a stale response if the revalidated resource returns a <code class="language-plaintext highlighter-rouge">5xx</code>-class error.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Cache-Control: max-age=2419200, stale-if-error=86400 </code></pre></div></div> <p>Here, we instruct the cache that the file is fresh for 28 days (2,419,200 seconds), and that if we were to encounter an error after that time, we allow an additional day (86,400 seconds) during which we will allow a stale asset to be served.</p> <hr /> <h2 id="no-transform"><code class="language-plaintext highlighter-rouge">no-transform</code></h2> <p><code class="language-plaintext highlighter-rouge">no-transform</code> doesn’t have anything do with storing, serving, or revalidating freshness, but it does instruct intermediaries that they cannot modify, or <em>transform</em>, any of the response.</p> <p>A common scenario in which an intermediary might modify a response is to make optimisations on behalf of developers <em>for</em> users: a telco provider might proxy image requests though their stack and make optimisations to them before passing them off to end users on mobile connections.</p> <p>The issue here is that developers begin to lose control of the presentation of their resources, and the image optimisations made by the telco might be deemed too aggressive and unacceptable, or we might have already optimised the images to the ideal degree ourselves and anything further is unnecessary.</p> <p>Here, we want to instruct this middleware not to transform any of our content.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Cache-Control: no-transform </code></pre></div></div> <p>The <code class="language-plaintext highlighter-rouge">no-transform</code> header can sit alongside any other directives, and needs no other directives for it to function itself.</p> <p><strong>N.B.</strong> Some transformations are a good idea: CDNs choosing between Gzip or Brotli encoding for users that need the former or could use the latter; image transformation services automatically converting to WebP; etc.</p> <p><strong>N.B.</strong> If you’re running over HTTPS—which you should be—then intermediaries and proxies can’t modify payloads anyway, so <code class="language-plaintext highlighter-rouge">no-transform</code> would be ineffective.</p> <article class="[ box box--highlight ] [ flag flag--responsive ] mb" data-ui-component="Cross-sell promo"> <div class="flag__img"><a href="/contact/" class="btn btn--full">Get in touch</a></div> <div class="flag__body"> <span class="heading mb0">Need Some Help?</span> <p>I help companies find and fix site-speed issues. <b>Performance audits</b>, <b>training</b>, <b>consultancy</b>, and more.</p> </div> </article> <hr /> <h2 id="cache-busting">Cache Busting</h2> <p>It would be irresponsible to talk about caching without talking about cache busting. I would always recommend solving your cache busting strategy before even thinking about your caching strategy. To do it the other way round is the fast-path to headaches.</p> <p>Cache busting solves the problem: <q>I just told the browser to use this file for the next year, but I just changed it and I don’t want users to wait a whole year before they get the fresh copy! How can I intervene?!</q></p> <h3 id="no-cache-busting--stylecss">No Cache Busting – <code class="language-plaintext highlighter-rouge">style.css</code></h3> <p>This is is the least-preferred thing to do: absolutely no cache busting whatsoever. This is a mutable file that we’d really struggle to cache bust.</p> <p>You should be very wary of caching any files like these, because we lose almost all control over them once they’re on the user’s device.</p> <p>Despite this example being a stylesheet, HTML pages fall squarely into this camp. We can’t change the file name of a webpage—imagine the havoc that would cause!—which is exactly why we tend not to cache them at all.</p> <h3 id="query-string--stylecssv1214">Query String – <code class="language-plaintext highlighter-rouge">style.css?v=1.2.14</code></h3> <p>Here, we still have a mutable file, but we add a query string to its file path. While better than the nothing option, it’s still not perfect. If anything were to strip that query string away, we fall back into the previous category of having no cache busting in place at all. A lot of proxy servers and CDNs will not cache anything with a query string either by configuration (e.g. from Cloudflare’s own documentation: <q>…a request for “<code class="language-plaintext highlighter-rouge">style.css?something</code>” will be normalised to just “<code class="language-plaintext highlighter-rouge">style.css</code>” when serving from the cache.</q>), or defensively (the query string might contain information specific to one particular response).</p> <h3 id="fingerprint--styleae3f66css">Fingerprint – <code class="language-plaintext highlighter-rouge">style.ae3f66.css</code></h3> <p>Fingerprinting is by far the preferred method for cache busting a file. By literally changing the file each time its content changes, we don’t technically cache bust anything: we end up with a whole new file! This is very robust, and permits the use of <code class="language-plaintext highlighter-rouge">immutable</code>. If you can implement this on your static assets, please do! Once you’ve managed to implement this very reliable cache busting strategy, you can use the most aggressive form of caching:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Cache-Control: max-age=31536000, immutable </code></pre></div></div> <h4 id="implementation-detail">Implementation Detail</h4> <p>The key to this method is the changing of the filename, but it doesn’t <em>have</em> to be a fingerprint. All of the following examples have the same effect:</p> <ol> <li><code class="language-plaintext highlighter-rouge">/assets/style.ae3f66.css</code>: busting with a hash of the file’s contents.</li> <li><code class="language-plaintext highlighter-rouge">/assets/style.1.2.14.css</code>: busting with a release version.</li> <li><code class="language-plaintext highlighter-rouge">/assets/1.2.14/style.css</code>: busting by changing a directory in the URL.</li> </ol> <p>However, the last example <em>implies</em> that we’re versioning each release rather than each individual file. This in turn implies that if we only needed to cache bust our stylesheet, we’d also have to cache bust all of the static files for that release. This is potentially wasteful, so prefer options (1) or (2).</p> <h3 id="clear-site-data"><code class="language-plaintext highlighter-rouge">Clear-Site-Data</code></h3> <p>Cache invalidation is hard—<a href="https://martinfowler.com/bliki/TwoHardThings.html">famously so</a>—so there’s <a href="https://www.w3.org/TR/clear-site-data/">a spec currently underway</a> that helps developers quite definitively clear the entire cache for their site’s origin in one fell swoop: <code class="language-plaintext highlighter-rouge">Clear-Site-Data</code>.</p> <p>I don’t want to go into too much detail in this post as <code class="language-plaintext highlighter-rouge">Clear-Site-Data</code> is not a <code class="language-plaintext highlighter-rouge">Cache-Control</code> directive, but is in fact a whole new HTTP header.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Clear-Site-Data: "cache" </code></pre></div></div> <p>Applying this header to any one of your origin’s assets will clear the cache for the entire origin, not just the file to which it is attached. That means that, if you needed to hard-purge your entire site from all visitors’ caches, you could apply the above header to just your HTML payload.</p> <p><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data#Browser_compatibility">Browser support</a>, at the time of writing, is limited to Chrome, Android Webview, Firefox, and Opera.</p> <p><strong>Tip:</strong> There are a number of directives that <code class="language-plaintext highlighter-rouge">Clear-Site-Data</code> will accept: <code class="language-plaintext highlighter-rouge">"cookies"</code>, <code class="language-plaintext highlighter-rouge">"storage"</code>, <code class="language-plaintext highlighter-rouge">"executionContexts"</code>, and <code class="language-plaintext highlighter-rouge">"*"</code> (which, naturally, means ‘all of the above’).</p> <hr /> <h2 id="examples-and-recipes">Examples and Recipes</h2> <p>Okay, let’s take a look at some scenarios and what kinds of <code class="language-plaintext highlighter-rouge">Cache-Control</code> headers we might employ.</p> <h3 id="online-banking-page">Online Banking Page</h3> <p>Something like an online banking app page that lists your recent transactions, your current balance, and perhaps sensitive bank account details needs to be up-to-date (imagine being served a page that listed your balance as it appeared a week ago!) and also kept very private (you don’t want your bank details to be stored in a shared cache (or any cache, really)).</p> <p>To this end, let’s go with:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Request URL: /account/ Cache-Control: no-store </code></pre></div></div> <p>As per the spec, this would be sufficient to prevent a browser persisting the response to disk at all, across private and shared caches:</p> <blockquote> <p>The <code class="language-plaintext highlighter-rouge">no-store</code> response directive indicates that a cache MUST NOT store any part of either the immediate request or response. This directive applies to both private and shared caches. ‘MUST NOT store’ in this context means that the cache MUST NOT intentionally store the information in non-volatile storage, and MUST make a best-effort attempt to remove the information from volatile storage as promptly as possible after forwarding it.</p> </blockquote> <p>But if you wanted to be very defensive, perhaps you might opt for:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Request URL: /account/ Cache-Control: private, no-cache, no-store </code></pre></div></div> <p>This would explicitly instruct not to store anything in public caches (e.g. a CDN), to always serve the freshest possible copy, and not to persist anything to storage.</p> <h3 id="live-train-timetable-page">Live Train Timetable Page</h3> <p>If we’re building a page that displays near-realtime information, we want to guarantee that the user always sees the best, most up-to-date information we can give them, if that information exists. Let’s use:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Request URL: /live-updates/ Cache-Control: no-cache </code></pre></div></div> <p>This simple directive will mean that the browser won’t show a response directly from cache without checking with the server that it is allowed to. This means that a user will never be shown out of date train information, but they could benefit from grabbing file from their cache if the server dictates that the cache mirrors the latest information.</p> <p>This is usually a sensible default for almost all webpages: give us the latest possible content, but let us use the speed of the cache if possible.</p> <h3 id="faqs-page">FAQs Page</h3> <p>A page like FAQs is likely to update very infrequently, and the content on it is unlikely to be time sensitive. It’s certainly not as critical as realtime sport scores or flight statuses. We can probably cache an HTML page like this for a little while and force the browser to check for fresh content periodically instead of every visit. Let’s go for this:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Request URL: /faqs/ Cache-Control: max-age=604800, must-revalidate </code></pre></div></div> <p>This tells the browser to cache the HTML page for one week (604,800 seconds), and once that week is up, we need to check with the server for updates.</p> <p><strong>Beware:</strong> Having differing caching strategies for different pages within the same website could lead to a problem where your <code class="language-plaintext highlighter-rouge">no-cache</code> homepage requests the newest <code class="language-plaintext highlighter-rouge">style.f4fa2b.css</code> that it references, but your three-day cached FAQs page is still pointing at <code class="language-plaintext highlighter-rouge">style.ae3f66.css</code>. The effects of this may be slight, but it’s a scenario you should be aware of.</p> <h3 id="static-js-or-css-app-bundle">Static JS (or CSS) App Bundle</h3> <p>Let’s say our <code class="language-plaintext highlighter-rouge">app.[fingerprint].js</code> updates pretty frequently—potentially with every release we do—but we’ve also put in the work to fingerprint the file every time it changes (good work!) then we can do something like this:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Request URL: /static/app.1be87a.js Cache-Control: max-age=31536000, immutable </code></pre></div></div> <p>It doesn’t matter that we update our JS quite frequently: because of our ability to reliably cache bust it, we can cache it for as long as we like. In this case, we’ve chosen to cache it for a year. I picked a year because firstly, a year is a long time, but secondly, it’s pretty highly unlikely that a browser will actually hold onto a file for that long anyway (browsers have a finite amount of storage they can use for HTTP cache, so they periodically empty parts of it themselves; users may clear their own cache). Going anything beyond a year is likely to be no more effective.</p> <p>Further, because this file’s content never changes, we can signal to the browser that this file is immutable. We don’t need to revalidate it for the whole year, even if a user refreshes the page. Not only do we get the speed benefits of using the cache, we avoid the latency penalty of revalidation.</p> <h3 id="decorative-image">Decorative Image</h3> <p>Imagine a purely decorative photograph accompanying an article. It’s not an infographic or a chart, it doesn’t contain any content critical to understanding the rest of the page, and a user wouldn’t even really notice if it was completely missing anyway.</p> <p>Images are usually a heavy asset to download, so we want to cache it; it’s not critical to the page, so we don’t need to fetch the latest version; and we could probably even get away with serving the image after it’s gone a little out of date. Let’s do this:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Request URL: /content/masthead.jpg Cache-Control: max-age=2419200, must-revalidate, stale-while-revalidate=86400 </code></pre></div></div> <p>Here we’re telling the browser to store the image for 28 days (2,419,200 seconds), that we want to check with the server for updates after that 28-day time limit, and if the image is less than one day (86,400 seconds) out of date, let’s use that one while we fetch the latest version in the background.</p> <hr /> <h2 id="key-things-to-remember">Key Things to Remember</h2> <ul> <li>Cache busting is vitally important. Work out your cache busting strategy before you begin work on your caching strategy.</li> <li>Generally speaking, caching HTML—content—is a bad idea. HTML URLs can’t be busted, and as your HTML page is generally the entry point into the rest of your page’s subresources, you’ll end up caching the references to your static assets, too. This is going to cause you (and your users) a world of frustration.</li> <li>If you are going to cache any HTML, having different cache policies on different types of HTML page on a site could lead to inconsistencies if one class of page is always fresh and others are sometimes fetched from cache.</li> <li>If you can reliably cache-bust (with a fingerprint) your static assets, then you might as well go all-in and cache for years at a time with an <code class="language-plaintext highlighter-rouge">immutable</code> directive for good measure.</li> <li>Non-critical content can be given a stale grace period with directives like <code class="language-plaintext highlighter-rouge">stale-while-revalidate</code>.</li> <li><code class="language-plaintext highlighter-rouge">immutable</code> and <code class="language-plaintext highlighter-rouge">stale-while-revalidate</code> not only give us the traditional benefits of a cache, but they also allow us to mitigate the cost of latency while revalidating.</li> </ul> <p>Avoiding the network wherever possible makes for much faster experiences for our users (and much lower throughput for our infrastructure). By having a good view of our assets, and an overview of what’s available to us, we can begin to design very granular, bespoke, and effective caching strategies specific to our own applications.</p> <p>Cache rules everything.</p> <hr /> <h2 id="resources-and-related-reading">Resources and Related Reading</h2> <ul> <li><a href="https://jakearchibald.com/2016/caching-best-practices/"><cite>Caching best practices & max-age gotchas</cite></a> – <a href="https://twitter.com/jaffathecake">Jake Archibald</a>, 2016</li> <li><a href="https://bitsup.blogspot.com/2016/05/cache-control-immutable.html"><cite>Cache-Control: immutable</cite></a> – <a href="https://twitter.com/mcmanusducksong">Patrick McManus</a>, 2016</li> <li><a href="https://www.fastly.com/blog/stale-while-revalidate-stale-if-error-available-today"><cite>Stale-While-Revalidate, Stale-If-Error Available Today</cite></a> – <a href="https://twitter.com/Souders">Steve Souders</a>, 2014</li> <li><a href="https://calendar.perfplanet.com/2016/a-tale-of-four-caches/"><cite>A Tale of Four Caches</cite></a> – <a href="https://twitter.com/yoavweiss">Yoav Weiss</a>, 2016</li> <li><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data">Clear-Site-Data</a> – MDN</li> <li><a href="https://tools.ietf.org/html/rfc7234">RFC 7234 – HTTP/1.1 Caching</a> – 2014</li> <li><a href="https://cache-control.sdgluck.now.sh/">Cache-Control Header Builder </a> – 2019</li> </ul> <h3 id="do-as-i-say-not-as-i-do">Do as I Say, Not as I Do</h3> <p>Before someone on Hacker News hauls me over the coals for my hypocrisy, it’s worth noting that my own caching strategy is so sub-par that I’m not even going to go into it.</p> <script type="application/ld+json"> { "@context": "https://schema.org", "@type": "BlogPosting", "url": "https://csswizardry.com/2019/03/cache-control-for-civilians/", "name": "Cache-Control for Civilians", "headline": "Cache-Control for Civilians", "keywords": "", "description": "What does Cache-Control really do? In basic terms? Let’s find out!", "datePublished": "2019-03-04 01:21:39 +0000", "dateModified": "2019-03-04 01:21:39 +0000", "author": { "@type": "Person", "url": "https://csswizardry.com", "name": "Harry Roberts" }, "publisher": { "@type": "Organization", "name": "CSS Wizardry", "url": "https://csswizardry.com", "logo": { "@type": "ImageObject", "width": 128, "height": 128, "url": "https://csswizardry.com/icon.png" } }, "mainEntityOfPage": { "@type": "WebPage", "@id": "https://csswizardry.com/2019/03/cache-control-for-civilians/" } } </script> <script type="application/ld+json"> { "@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": [{ "@type": "ListItem", "position": 1, "name": "Blog", "item": "https://csswizardry.com/archive/" }, { "@type": "ListItem", "position": 2, "name": "2019", "item": "https://csswizardry.com/archive/#year-2019" }, { "@type": "ListItem", "position": 3, "name": "Cache-Control for Civilians", "item": "https://csswizardry.com/2019/03/cache-control-for-civilians/" }] } </script> <hr /> <style> .c-mini-profile { display: flex; gap: 0.75rem; align-items: flex-start; margin-bottom: 1.5rem; content-visibility: auto; contain-intrinsic-size: 1px 120px; } .c-mini-profile__avatar { border-radius: 100%; } .c-mini-profile__content { } </style> <article class=c-mini-profile> <img src=/img/content/avatar.jpg alt width=72 height=72 loading=lazy class=c-mini-profile__avatar> <div class=c-mini-profile__content> <h5 class=mb0 style="font-weight: bold;">By Harry Roberts</h5> <p class=mb0>Harry Roberts is an independent consultant web performance engineer. He helps companies of all shapes and sizes find and fix site speed issues.</p> </div> </article> <hr /> <style> /*! * inuitcss, by @csswizardry * * github.com/inuitcss | inuitcss.com */.c-pagination{display:flex;margin-bottom:24px}.c-pagination__item{flex-grow:1;flex-basis:0;padding:12px;border-radius:3px;border:1px solid transparent}.c-pagination__item:hover{text-decoration:none;border-color:currentColor}.c-pagination__item.c-pagination__item.c-pagination__item::after{display:none}.c-pagination__item::before{display:block;font-size:12px;font-size:.75rem;line-height:2;line-height:1;text-transform:uppercase;font-weight:normal;color:#333}.c-pagination__prev::before{content:"Previously: "}.c-pagination__next{text-align:right}.c-pagination__next::before{content:"Up next: "} /*# sourceMappingURL=components.pagination.css.map */ </style> <script type=speculationrules> { "prerender": [ { "source": "list", "urls": [ "/2019/04/tips-for-technical-interviews/", "/2019/01/bandwidth-or-latency-when-to-optimise-which/" ] } ] } </script> <nav class=c-pagination> <a href="/2019/01/bandwidth-or-latency-when-to-optimise-which/" class="c-pagination__item c-pagination__prev" style="view-transition-name: x-2019-01-31" id=articleOlder>Bandwidth or Latency: When to Optimise for Which</a> <a href="/2019/04/tips-for-technical-interviews/" class="c-pagination__item c-pagination__next" style="view-transition-name: x-2019-04-25" id=articleNewer>Tips for Technical Interviews</a> </nav> <script> (() => { const articleOlder = document.getElementById('articleOlder'); const articleNewer = document.getElementById('articleNewer'); articleOlder.addEventListener('click', (event) => { setTimeout(() => { LUX.addData('articlePaginationUsed', true) LUX.addData('articleOlderUsed', true) }); }); articleNewer.addEventListener('click', (event) => { setTimeout(() => { LUX.addData('articlePaginationUsed', true) LUX.addData('articleNewerUsed', true) }); }); })(); </script> <hr /> <p> <a href=/code-reviews/#fix-it-fast class="btn btn--full">Did this help? <strong>We can do way more!</strong></a> </p> <script>performance.mark('contentEnd')</script> </section ><section class="layout__item desk-three-tenths desk-push-one-tenth" data-ui-component="Sub content" id="section:sub-content"> <hr class=hide-desk> <article class="[ box box--highlight ] mb" data-ui-component=ad> <div class=adpacks> <script src=https://cdn.carbonads.com/carbon.js?zoneid=1673&serve=C6AILKT&placement=csswizardrycom defer id=_carbonads_js></script> </div> </article> <p> <img src=/img/css/masthead-small.jpg alt width=720 height=480 style="background-image: url(/img/css/masthead-small-lqip.jpg), url(data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQECAgICAgICAgICAgMDAwMDAwMDAwMBAQEBAQEBAgEBAgICAQICAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA//AABEIAAsAEAMBEQACEQEDEQH/xACAAAEBAQAAAAAAAAAAAAAAAAAICQoQAAAEBAUEAQUAAAAAAAAAAAMEBQYBAhMUBxIVFhcACAkkERgjJTdDAQADAQAAAAAAAAAAAAAAAAAHCAkGEQAABAUCAwcEAwAAAAAAAAABAgMEBhESExQFBwAVIwgWFyEiJDMxMkJDNEFR/9oADAMBAAIRAxEAPwDP12CsRLfmKpRpN7FZ6ok7kxPHIoiWbLlCyQvgFTI6tKWeSihwEUEcgspheMBBwYyyZBBYhTS5YRg6O3m4ei7fQrF0dEjLWmLNukNo7Vgmcz1c4uBQTTIqIpkdCc5VyFUOBDAcxREtEzV42T1KEND2/ibX1ojiNJu11ZyKTZqk2kc1XoUXE5TmKLg4lIAmEqQmrAhpgPD872MFxcADY5d2dyb4TRXcArubaCHq72buHpsQ7EQ5hxOvmicTyhscsfKlJRjMJBjAEwU88YzRm6KPZ47TCMd7RpljeO3ejbg6UgRu+W1Fskcj1e2UwvG+EmQBQVERLScEzFUAwGD7RMUtrtf0OM4AePIiiGLmcSaaUCulk0UnKKhlCVFVTstxEqJhAxSgsKavp8wH68Sm8SV79RRyjy/bUGpqHF+gafa7sb37YvvyvE1fLqume5b/AD/Kp0kz3H8MH9/Bu5xrWfexrmMr8Nnp8wpnh5ntsi3+yniWGx9zO1WjK/qdmij8v5df6p/FR536fxnwpPK9pXJTiteXaO9X9a095fFfOk1dp6x715Wz67eetmt6f2qXWm7OuD4R6lLkWdlpzxMvvHO0pbyp+0xaZ4tHTtXMnq08HN7id2HV3m96+aXLsnMnSaU7fSs/5X6JVVefH//Z );" elementtiming=sidebar-image decoding=sync> </p> <p>Hi there, I’m <b>Harry Roberts</b>. I am an <b><a href=https://web.archive.org/web/20190630140300/https://thenetawards.com/previous-winners/>award-winning</a> Consultant Web Performance Engineer</b>, <b>designer</b>, <b>developer</b>, <b>writer</b>, and <b>speaker</b> from the UK. I <b><a href=/archive/>write</a></b>, <b><a href=https://twitter.com/csswizardry>Tweet</a></b>, <b><a href=/speaking/>speak</a></b>, and <b><a href="https://github.com/csswizardry">share code</a></b> about measuring and improving site-speed. You <a href=/services/>should hire me</a>.</p> <p><a href=https://twitter.com/csswizardry class=twitter-follow-button data-show-count=true data-lang=en>Follow @csswizardry</a></p> <p>You can now find me on <a rel="me" href="https://webperf.social/@csswizardry">Mastodon</a>.</p> <hr> <p class=text-banner> <a href=/code-reviews/#fix-it-fast class="btn btn--full btn--positive" id=cta data-sctrack=cta-sidebar elementtiming=cta-sidebar>Suffering? <b>Fix It Fast!</b> </a> </p> <script> (() => { const cta = document.getElementById('cta'); cta.addEventListener('click', (event) => { setTimeout(() => { LUX.addData('ctaClicked', true) }); }); })(); </script> <h4>Projects</h4> <ul class="[ list-ui list-ui--small ] mb"> <li><a href=https://github.com/inuitcss/inuitcss><img src=https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/img/content/logo-inuitcss.png alt=inuitcss width=500 height=238 loading=lazy></a></li> <li><a href=https://itcss.io/>ITCSS</a> – coming soon…</li> <li><a href=https://cssguidelin.es/><img src=https://res.cloudinary.com/csswizardry/image/fetch/f_auto,q_auto/https://csswizardry.com/img/content/logo-css-guidelines.png alt="CSS Guidelines" width=540 height=180 loading=lazy></a></li> </ul> <h4>Next Appearance</h4> <ul class="list-ui list-ui--small"> <li> <h4 class="list-ui__title">Talk</h4> <img src="/img/icons/gr.png" alt width="16" height="11" loading="lazy" /> <a href="https://devoxx.gr/">Devoxx Greece</a>: Athens (Greece), April 2025 </li> </ul> </section> </div> </div> <section class="band band--dark" id="section:learn"> <div class=wrapper> <h4 class=band__title>Learn:</h4> <ul class="o-list-inline o-list-inline--spaced text-banner mb0"> <li><a href="/2019/03/cache-control-for-civilians/">Understand <code>Cache-Control</code></a> <li><a href="/2023/07/core-web-vitals-for-search-engine-optimisation/">Core Web Vitals and SEO</a> <li><a href="/2018/11/css-and-network-performance/">CSS Performance</a> <li><a href="/2023/07/in-defence-of-domcontentloaded/">Does <code>DOMContentLoaded</code> Still Matter?</a> <li><a href="/2021/02/measuring-network-performance-in-mobile-safari/">Improve Web Performance on iOS</a> <li><a href="/max-age/"><code>max-age</code> calculator</a> <li><a href="/2024/09/optimising-for-high-latency-environments/">Optimise for High Latency</a> <li><a href="/2022/03/optimising-largest-contentful-paint/">Optimise Largest Contentful Paint</a> <li><a href="/2020/05/the-fastest-google-fonts/">Speed Up Google Fonts</a> <li><a href="/2023/10/the-three-c-concatenate-compress-cache/">Can We Drop Our Build Tools Yet?</a> <li><a href="/2023/09/the-ultimate-lqip-lcp-technique/">Get the Fastest Possible LCP</a> <li><a href="/2019/08/time-to-first-byte-what-it-is-and-why-it-matters/">Diagnose Time to First Byte</a> <li><a href="/2023/10/what-is-the-maximum-max-age/">What Is the Maximum <code>max-age</code> Value?</a> </ul> </div> </section> <section class="band band--highlight"> <div class=wrapper> <!-- Begin MailChimp Signup Form --> <div id=mc_embed_signup class=c-newsletter> <form action=https://csswizardry.us14.list-manage.com/subscribe/post?u=95f3f41085f5f957a07ba5efd&id=ba05b5418d method=post id=mc-embedded-subscribe-form name=mc-embedded-subscribe-form class=validate> <div id=mc_embed_signup_scroll> <h5 class=c-newsletter__title>Newsletter</h5> <p class=c-newsletter__text>Infrequent updates, special offers, and exclusive content. <a href=/newsletter/>Learn more…</a></p> <div class=mc-field-group> <label for=mce-EMAIL class=c-label>Email Address </label> <input type=email value name=EMAIL class="c-input-text c-newsletter__email required email" id=mce-EMAIL required placeholder=email@domain.com> <input type=submit value=Join name=subscribe id=mc-embedded-subscribe class="button btn c-newsletter__submit"> </div> <div id=mce-responses class=clear> <div class=response id=mce-error-response style=display:none></div> <div class=response id=mce-success-response style=display:none></div> </div> <!-- real people should not fill this in and expect good things - do not remove this or risk form bot signups--> <div style="position: absolute; left: -5000px;" aria-hidden=true><input type=text name=b_95f3f41085f5f957a07ba5efd_ba05b5418d tabindex=-1 value></div> </div> </form> </div> <!--End mc_embed_signup--> </div> </section> <style> /*! * inuitcss, by @csswizardry * * github.com/inuitcss | inuitcss.com */.page-foot{font-size:12px;font-size:.75rem;line-height:2;padding-bottom:0}@media screen and (min-width: 64em){.page-foot{padding-bottom:24px}} </style> <section class="[ band band--tint ] page-foot"> <div class=wrapper> <div class="layout layout--middle"> <div class="layout__item lap-and-up-one-quarter"> <p>I am available for hire to consult, advise, and develop with passionate product teams across the globe.</p> </div ><div class="layout__item lap-and-up-one-quarter"> <p>I specialise in large, product-based projects where performance, scalability, and maintainability are paramount.</p> </div ><div class="layout__item lap-and-up-one-half"> <p><a href=/code-reviews/#fix-it-fast class="btn btn--secondary btn--full" data-sctrack=cta-footer>Suffering? <b>Fix It Fast!</b> </a></p> </div> </div> </div> </section> <style> /*! * inuitcss, by @csswizardry * * github.com/inuitcss | inuitcss.com */.page-micro{background-color:#333;padding:12px;text-align:center;content-visibility:auto;contain-intrinsic-size:1px 100px}.page-micro,.page-micro a{color:#fff}.page-micro__copy{font-size:12px;font-size:.75rem;line-height:2;display:block} </style> <footer class=page-micro> <p class=wrapper> <small class=page-micro__copy><b>CSS Wizardry Ltd</b> is a company registered in England and Wales. <b>Company No.</b> 08698093, <b>VAT No.</b> 170659396</small> </p> </footer> <script> if ('serviceWorker' in navigator) { window.addEventListener('load', function() { navigator.serviceWorker.register('/sw.js?0156').then(function(registration) { // Successfully registered the Service Worker //console.log('Service Worker registration successful with scope: ', registration.scope); }).catch(function(err) { // Failed to register the Service Worker //console.log('Service Worker registration failed: ', err); }); }); } </script> <script> performance.measure('contentTime', 'contentStart', 'contentEnd'); </script> <script> /** * The site nav has been explicitly ‘turned off’ with `content-visibility: * hidden;` in its component (S)CSS file. This means we don’t need to bother * rendering it at all on the first pass: it’s off-screen anyway. Once we’re * approaching `domInteractive` (that’s now), we turn it ‘back on’. It’s * wrapped in a rAF to make it asynchronous, which is probably a bit of * a micro-optimisation. */ requestAnimationFrame(() => { const siteNavList = document.getElementById('jsSiteNavList'); siteNavList.style.contentVisibility = 'visible'; }); const navReady = performance.mark('navReady'); console.log('Nav ready at: ' + navReady.startTime + 'ms'); </script> <noscript> <!-- - In the highly unlikely event that someone has disabled JS, turn the nav - ‘back on’ synchronously. --> <style> .site-nav__list { content-visibility: visible; } </style> </noscript> <script type=speculationrules> { "prefetch": [ { "source": "document", "where": { "href_matches": "/*" }, "eagerness": "immediate" } ], "prerender": [ { "source": "document", "where": { "href_matches": "/*" }, "eagerness": "moderate" } ] } </script> <script> (() => { const rtt = navigator.connection.rtt; LUX.addData('rtt', rtt); })(); </script>