There are several questions about leveraging browser caching but I didn't find anything useful for how to do this in an ASP.NET application. Google's Pagespeed tells this is performance biggest problem. So far I did this in my web.config:
<system.webServer> <staticContent> <!--<clientCache cacheControlMode="UseExpires" httpExpires="Fri, 24 Jan 2014 03:14:07 GMT" /> --> <clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="7.24:00:00" /> </staticContent> </system.webServer>
Commented code works. I can set expire header to be some particular time in future but I was not able to set cacheControlMaxAge
to set how many days from now static content would be cached. It does not work. My questions is:
How can I do that? I know it is possible to set caching only for specific folder which would be good solution, but it isn't working also. Application is hosted on Windows Server 2012,on IIS8, application pool is set to classic.
After I set this code in web config I got pagespeed of 72 (before was 71). 50 files were not cached. (Now 49) I was wondering why and I just realized that one file was actually cached (svg file). Unfortunately png and jpg file were not. This is my web.config
<?xml version="1.0" encoding="utf-8"?> <configuration> <configSections> <section name="exceptionManagement" type="Microsoft.ApplicationBlocks.ExceptionManagement.ExceptionManagerSectionHandler,Microsoft.ApplicationBlocks.ExceptionManagement" /> <section name="jsonSerialization" type="System.Web.Configuration.ScriptingJsonSerializationSection, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E34" requirePermission="false" allowDefinition="Everywhere" /> <sectionGroup name="elmah"> <section name="security" requirePermission="false" type="Elmah.SecuritySectionHandler, Elmah" /> <section name="errorLog" requirePermission="false" type="Elmah.ErrorLogSectionHandler, Elmah" /> <section name="errorMail" requirePermission="false" type="Elmah.ErrorMailSectionHandler, Elmah" /> <section name="errorFilter" requirePermission="false" type="Elmah.ErrorFilterSectionHandler, Elmah" /> </sectionGroup> </configSections> <exceptionManagement mode="off"> <publisher mode="off" assembly="Exception" type="blabla.ExceptionHandler.ExceptionDBPublisher" connString="server=188......;database=blabla;uid=blabla;pwd=blabla; " /> </exceptionManagement> <location path="." inheritInChildApplications="false"> <system.web> <httpHandlers> <add verb="GET,HEAD" path="ScriptResource.axd" type="System.Web.Handlers.ScriptResourceHandler,System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e34" validate="false" /> <add verb="GET" path="Image.ashx" type="blabla.WebComponents.ImageHandler, blabla/>" <add verb="*" path="*.aspx" type="System.Web.UI.PageHandlerFactory" /> <add verb="*" path="*.jpg" type="System.Web.StaticFileHandler" /> <add verb="GET" path="*.js" type="System.Web.StaticFileHandler" /> <add verb="*" path="*.gif" type="System.Web.StaticFileHandler" /> <add verb="GET" path="*.css" type="System.Web.StaticFileHandler" /> </httpHandlers> <compilation defaultLanguage="c#" targetFramework="4.5.1" /> <trace enabled="false" requestLimit="100" pageOutput="true" traceMode="SortByTime" localOnly="true"/> <authentication mode="Forms"> <forms loginUrl="~/user/login.aspx"> <credentials passwordFormat="Clear"> <user name="blabla" password="blabla" /> </credentials> </forms> </authentication> <authorization> <allow users="*" /> </authorization> <sessionState mode="InProc" stateConnectionString="tcpip=127.0.0.1:42424" sqlConnectionString="data source=127.0.0.1;Trusted_Connection=yes" cookieless="false" timeout="20" /> <globalization requestEncoding="utf-8" responseEncoding="utf-8" culture="en-GB" uiCulture="en-GB" /> <xhtmlConformance mode="Transitional" /> <pages controlRenderingCompatibilityVersion="4.5" clientIDMode="AutoID"> <namespaces> </namespaces> <controls> <add assembly="Microsoft.AspNet.Web.Optimization.WebForms" namespace="Microsoft.AspNet.Web.Optimization.WebForms" tagPrefix="webopt" /> </controls> </pages> <webServices> <protocols> <add name="HttpGet" /> <add name="HttpPost" /> </protocols> </webServices> </system.web> </location> <appSettings> </appSettings> <connectionStrings> </connectionStrings> <system.web.extensions> <scripting> <webServices> <jsonSerialization maxJsonLength="200000" /> </webServices> </scripting> </system.web.extensions> <startup> <supportedRuntime version="v2.0.50727" /> <supportedRuntime version="v1.1.4122" /> <supportedRuntime version="v1.0.3705" /> </startup> <system.webServer> <rewrite> <providers> <provider name="ReplacingProvider" type="ReplacingProvider, ReplacingProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=5ab632b1f332b247"> <settings> <add key="OldChar" value="_" /> <add key="NewChar" value="-" /> </settings> </provider> <provider name="FileMap" type="DbProvider, Microsoft.Web.Iis.Rewrite.Providers, Version=7.1.761.0, Culture=neutral, PublicKeyToken=0525b0627da60a5e"> <settings> <add key="ConnectionString" value="server=;database=blabla;uid=blabla;pwd=blabla;App=blabla"/> <add key="StoredProcedure" value="Search.GetRewriteUrl"/> <add key="CacheMinutesInterval" value="0"/> </settings> </provider> </providers> <rewriteMaps configSource="rewritemaps.config" /> <rules configSource="rewriterules.config" /> </rewrite> <modules> <remove name="ScriptModule" /> <add name="ScriptModule" preCondition="managedHandler" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3456AD264E35" /> <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah" preCondition="managedHandler" /> <add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah" preCondition="managedHandler" /> <add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah" preCondition="managedHandler" /> </modules> <handlers> <add name="Web-JPG" path="*.jpg" verb="GET,HEAD,POST" modules="IsapiModule" scriptProcessor="C:\Windows\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" resourceType="Unspecified" preCondition="classicMode,runtimeVersionv4.0,bitness64" /> <add name="Web-CSS" path="*.css" verb="GET,HEAD,POST" modules="IsapiModule" scriptProcessor="C:\Windows\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" resourceType="Unspecified" preCondition="classicMode,runtimeVersionv4.0,bitness64" /> <add name="Web-GIF" path="*.gif" verb="GET,HEAD,POST" modules="IsapiModule" scriptProcessor="C:\Windows\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" resourceType="Unspecified" preCondition="classicMode,runtimeVersionv4.0,bitness64" /> <add name="Web-JS" path="*.js" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="C:\Windows\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" resourceType="Unspecified" preCondition="classicMode,runtimeVersionv4.0,bitness64" /> </handlers> <validation validateIntegratedModeConfiguration="false" /> <httpErrors errorMode="DetailedLocalOnly" existingResponse="Auto"> <remove statusCode="404" subStatusCode="-1"/> <remove statusCode="500" subStatusCode="-1"/> <error statusCode="404" path="error404.htm" responseMode="File"/> <error statusCode="500" path="error.htm" responseMode="File"/> </httpErrors> </system.webServer> <system.serviceModel> <bindings> <basicHttpBinding> <binding name="soapBinding_AdriagateService" closeTimeout="00:01:00" openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00" allowCookies="false" bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard" maxBufferPoolSize="2147483647" maxBufferSize="2147483647" maxReceivedMessageSize="2147483647" textEncoding="utf-8" transferMode="Buffered" useDefaultWebProxy="true" messageEncoding="Text"> <readerQuotas maxDepth="2147483647" maxStringContentLength="2147483647" maxArrayLength="2147483647" maxBytesPerRead="2147483647" maxNameTableCharCount="2147483647" /> <security mode="None" /> </binding> </basicHttpBinding> <netTcpBinding> <binding name="NetTcpBinding_ITravellerService" closeTimeout="00:10:00" openTimeout="00:10:00" sendTimeout="00:10:00" maxReceivedMessageSize="2147483647" maxBufferPoolSize="2147483647"> <readerQuotas maxDepth="2147483647" maxStringContentLength="2147483647" maxArrayLength="2147483647" maxBytesPerRead="2147483647" maxNameTableCharCount="2147483647" /> <security mode="None" /> </binding> </netTcpBinding> </bindings> <client> <endpoint address="blabla" bindingConfiguration="soapBinding_blabla" contract="" Address="blabla" name="blabla" /> <endpoint address="blabla" binding="basicHttpBinding" bindingConfiguration="soapBinding_IImagesService" contract="ImagesService.IImagesService" name="soapBinding_IImagesService"/> <identity> <servicePrincipalName value="blabla"/> </identity> </endpoint> </client> </system.serviceModel> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="WebGrease" publicKeyToken="31bf3856ad364e35" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-1.5.2.14234" newVersion="1.5.2.14234" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-4.5.0.0" newVersion="4.5.0.0" /> </dependentAssembly> </assemblyBinding> </runtime> <system.web> <httpModules> <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah" /> <add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah" /> <add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah" /> </httpModules> </system.web> <elmah> <security allowRemoteAccess="false" /> </elmah> <location path="elmah.axd" inheritInChildApplications="false"> <system.web> <httpHandlers> <add verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" /> </httpHandlers> </system.web> <system.webServer> <handlers> <add name="ELMAH" verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" preCondition="integratedMode" /> </handlers> </system.webServer> </location> </configuration>
EDIT: If I set exact expiry date, caching is working, but not for jpg,gif....only for png
EDIT2: If I set cacheControlCustom="public"
like here:
<clientCache cacheControlCustom="public" cacheControlMode="UseMaxAge" cacheControlMaxAge="7.00:00:00" />
caching is working but still not for jpegs and gifs; it only works for svgs and pngs.
To fix the leverage browser caching warning with WP Rocket, all you have to do is install and activate the plugin. That's it. For more details, see our guide on how to properly install and setup WP Rocket in WordPress. WP Rocket will automatically enable browser caching and modify your .
First thing first, you can leverage browser caching by modifying the . htaccess file. To do so, you can use an FTP client like FileZilla or the file manager in your hosting control panel. This method might suit you well if you're an advanced user.
To leverage your browser's caching generally means that you can specify how long web browsers should keep images, CSS and JS stored locally. That way the user's browser will download less data while navigating through your pages, which will improve the loading speed of your website.
Most of the browser caching issues can be resolved by viewing the response headers (can be done in Google chrome developer tools).
Now the clientCache
section of your web.config
file should set your output caching to a maximum age as you see in the image below has set the max-age
to 86400
which is 1 day in seconds.
Here is the web.config snippet for this setup.
<clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="1.00:00:00" />
Now thats great, the response header has a max-age
property set on the Cache-Control
header. So the browser should be caching the content. Well this is mostly true but some browsers require another flag to be set. Specifically the public
flag set for the cache control header. This can be easily added by using the cacheControlCustom
attribute in the web.config
. Here is an example.
<clientCache cacheControlCustom="public" cacheControlMode="UseMaxAge" cacheControlMaxAge="1.00:00:00" />
Now when we retry the page and inspect the headers.
Now as you can see from the image above we now have the value public, max-age=86400
. So our browser has all it needs to cache the resources. Now examining the headers and the network tab of google chrome will help us.
Here is the first request to the file.. Note the file is not cached...
Now lets navigate back to this page (NOTE: do not refresh the page, we will talk about that in a second). You will see the response in now returning from cache (as circled).
Now what happens if I refresh the page using either F5 or using the browser refresh feature. Wait.. where did the (from cache)
go.
Well in Google Chrome (not sure about other browsers) using the refresh button will re-download the static resources regardless of the cache header (insert clarification here please). That means that the resources has been re-retrieved and the max age header sent over.
Now after all the explanation above, be sure to test how you are monitoring the cache headers.
Update
Based on your comments you stated you have a generic handler (IHttpHandler
) named Image.ashx
with the content type of image/jpg
. Now you may expect the default behaviour would be to cache this handler. However IIS sees the extension .ashx
(correctly) as a dynamic script and is not subject to caching without explicitly setting the cache headers in the code itself.
Now this is where you need to be careful, as typically IHttpHandlers
should infact not be cached as they usually deliver dynamic content. Now if that content is unlikely to change you could set your cache headers directly in the code. Here is an example of setting cache headers in IHttpHandlers
using the Response
context.
context.Response.ContentType = "image/jpg"; context.Response.Cache.SetMaxAge(TimeSpan.FromDays(1)); context.Response.Cache.SetCacheability(HttpCacheability.Public); context.Response.Cache.SetSlidingExpiration(true); context.Response.TransmitFile(context.Server.MapPath("~/out.jpg"));
Now looking at the code we are setting a few properties on the Cache
property. To get the desired response I have set the properties.
context.Response.Cache.SetMaxAge(TimeSpan.FromDays(1));
tells the out put cache to set the max-age=
part of the Cache-Control
header to be 1
day in the future (86400 seconds).context.Response.Cache.SetCacheability(HttpCacheability.Public);
tells the out put cache to set the Cache-Control
header to public
. This is quite important as it tells the browser to cache to object.context.Response.Cache.SetSlidingExpiration(true);
tells the output cache to ensure it is setting the max-age=
part of the Cache-Control
header properly. Without setting the sliding expiration, the IIS out put caching will ignore the max age header. Putting this together gives me this result.As I stated above you may not want to cache the .ashx
files as they typically deliver dynamic content. However if that dynamic content is not likely to change in a given period you can use the methods above to deliver your .ashx
file.
Now in conjunction with the process listed above you could also set the ETag (see wiki) component of the cache headers so the browser can verify the content being delivered by a custom string. The wiki states:
An ETag is an opaque identifier assigned by a web server to a specific version of a resource found at a URL. If the resource content at that URL ever changes, a new and different
ETag
is assigned.
So this is really some sort of unique identification for the browser to identify the content being delivered in the response. By providing this header the browser on the next reload will send over a If-None-Match
header with the ETag
from the last response. We can modify our handler to detect the If-None-Match
header and compare it to our own generated Etag
. Now there is no exact science to generating ETags
but a good rule of thumb is to deliver an identifier that will most likely define only one entity. In this case I like to use two strings concatenated together such as.
System.IO.FileInfo file = new System.IO.FileInfo(context.Server.MapPath("~/saveNew.png")); string eTag = file.Name.GetHashCode().ToString() + file.LastWriteTimeUtc.Ticks.GetHashCode().ToString();
In the snippet above we are loading a file from our file system (you could get this from anywhere). I am then using the GetHashCode()
method (on all objects) to get the integer hash code of the object. In the example I concat the hash of the file name, then the last write date. The reason for the last write date is in case the file is changed the hash code is changed as well, thus making the finger prints different.
This will generate a hash code similar to 306894467-210133036
.
So how do we use this in our handler. Below is the newly modified version of the handler.
System.IO.FileInfo file = new System.IO.FileInfo(context.Server.MapPath("~/out.png")); string eTag = file.Name.GetHashCode().ToString() + file.LastWriteTimeUtc.Ticks.GetHashCode().ToString(); var browserETag = context.Request.Headers["If-None-Match"]; context.Response.ClearHeaders(); if(browserETag == eTag) { context.Response.Status = "304 Not Modified"; context.Response.End(); return; } context.Response.ContentType = "image/jpg"; context.Response.Cache.SetMaxAge(TimeSpan.FromDays(1)); context.Response.Cache.SetCacheability(HttpCacheability.Public); context.Response.Cache.SetSlidingExpiration(true); context.Response.Cache.SetETag(eTag); context.Response.TransmitFile(file.FullName);
As you can see, I have changed quite alot of the handler however you will notice that we generate the Etag
hash, check for an incoming If-None-Match
header. If the etag hash and the header are equal then we tell the browser that the content hasnt changed by returning the status code 304 Not Modified
.
Next was the same handler except we add the ETag
header by calling:
context.Response.Cache.SetETag(eTag);
When we run this up in the browser we get.
You will see from the image (as i did change the file name) that we now have all the components of our cache system in place. The ETag
is being delivered as a header, and the browser is sending the request header If-None-Match
so our handler can respond accordingly to cache file changed.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With