Использовать кеширование браузера в IIS (проблема с страницами в версии Google)

Есть несколько вопросов об использовании кеширования браузера, но я не нашел ничего полезного в том, как это сделать в приложении ASP.NET. Google Pagespeed говорит, что это самая большая проблема производительности. Пока что я сделал это в своем 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>

Прокомментированный код работает. Я могу установить заголовок expire для определенного времени в будущем, но я не смог установить cacheControlMaxAge, чтобы установить, сколько дней с этого времени будет кэшироваться статический контент. Это не работает. Мои вопросы:

Как я могу это сделать? Я знаю, что можно установить кеширование только для конкретной папки, которая будет хорошим решением, но она не работает. Приложение размещено на Windows Server 2012, в IIS8, пул приложений настроен на классический.

После того, как я установил этот код в веб-конфигурации, я получил strpeed из 72 (раньше было 71). 50 файлов не были кэшированы. (Теперь 49) Мне было интересно, почему, и я просто понял, что один файл был фактически кэширован (файл svg). К сожалению, файл png и jpg не был. Это мой 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: Если я установил точную дату истечения срока действия, кеширование работает, но не для jpg, gif.... только для png

EDIT2: Если я установил cacheControlCustom="public", как здесь:

<clientCache cacheControlCustom="public" 
cacheControlMode="UseMaxAge" cacheControlMaxAge="7.00:00:00" /> 

работает кеширование, но еще не для jpegs и gif; он работает только для svgs и pngs.

Ответ 1

Большинство проблем кэширования браузера можно разрешить, просмотрев заголовки ответов (это можно сделать в инструментах разработчика Google Chrome).

enter image description here

Теперь в разделе clientCache вашего файла web.config следует установить максимальное кэширование вывода, как показано на рисунке ниже, для параметра max-age - 86400, которое составляет 1 день в секундах.

Вот фрагмент web.config для этой настройки.

<clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="1.00:00:00" />

Теперь отлично, заголовок ответа имеет свойство max-age, заданное в заголовке Cache-Control. Поэтому браузер должен кэшировать содержимое. Ну, это в основном верно, но для некоторых браузеров требуется установить еще один флаг. В частности, флаг public установлен для заголовка управления кэшем. Это можно легко добавить с помощью атрибута cacheControlCustom в web.config. Вот пример.

<clientCache cacheControlCustom="public" cacheControlMode="UseMaxAge" cacheControlMaxAge="1.00:00:00" />

Теперь, когда мы повторяем страницу и проверяем заголовки.

enter image description here

Теперь, как вы можете видеть из изображения выше, мы теперь имеем значение public, max-age=86400. Поэтому у нашего браузера есть все необходимое для кэширования ресурсов. Теперь изучение заголовков и вкладки сети google chrome поможет нам.

Вот первый запрос к файлу. Обратите внимание, что файл не кэшируется... enter image description here

Теперь вернемся к этой странице ( ПРИМЕЧАНИЕ: не обновляйте страницу, мы поговорим об этом через секунду). Теперь вы увидите ответ, возвращающийся из кеша (по кругу).

enter image description here

Теперь, что произойдет, если я обновляю страницу с помощью F5 или используя функцию обновления браузера. Подождите, куда пошел (from cache). enter image description here

Хорошо в Google Chrome (не уверен в других браузерах) с помощью кнопки обновления будет перезагружать статические ресурсы независимо от заголовка кеша (вставьте пояснение здесь, пожалуйста). Это означает, что ресурсы были повторно восстановлены, а максимальный заголовок заголовка отправлен.

Теперь, после объяснения выше, обязательно проверьте , как отслеживать заголовки кеша.

Обновление

На основании ваших комментариев, которые вы указали, у вас есть общий обработчик (IHttpHandler) с именем Image.ashx с типом контента image/jpg. Теперь вы можете ожидать, что поведение по умолчанию будет кэшировать этот обработчик. Однако IIS видит расширение .ashx (правильно) как динамическое script и не подлежит кешированию без явной установки заголовков кеша в самом коде.

Теперь вам нужно быть осторожным, так как обычно IHttpHandlers должен быть неакшен, поскольку они обычно доставляют динамический контент. Теперь, если это содержимое вряд ли изменится, вы можете настроить заголовки кэша непосредственно в коде. Ниже приведен пример настройки заголовков кеша в IHttpHandlers с использованием контекста Response.

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"));

Теперь, просматривая код, мы устанавливаем несколько свойств в свойстве Cache. Чтобы получить желаемый ответ, я установил свойства.

  • context.Response.Cache.SetMaxAge(TimeSpan.FromDays(1)); сообщает кешу out put для того, чтобы max-age= часть заголовка Cache-Control была 1 день в будущем (86400 секунд).
  • context.Response.Cache.SetCacheability(HttpCacheability.Public); указывает кешу out put для заголовка Cache-Control на public. Это очень важно, поскольку он указывает браузеру кэшировать объект.
  • context.Response.Cache.SetSlidingExpiration(true); указывает кэшу вывода, чтобы убедиться, что он правильно настроил часть max-age= заголовка Cache-Control. Без установки скользящего истечения кеширование IIS вне кэша будет игнорировать максимальный возрастный заголовок. Объединение этого результата дает мне этот результат.

output cache from ashx file

Как я уже говорил выше, вы можете не кэшировать файлы .ashx, поскольку они обычно предоставляют динамический контент. Однако, если этот динамический контент вряд ли изменится за определенный период, вы можете использовать приведенные выше методы для доставки вашего файла .ashx.

Теперь в связи с вышеописанным процессом вы также можете установить компонент ETag (см. wiki) заголовков кеша, чтобы браузер может проверить содержимое, которое будет доставлено специальной строкой. Вики заявляет:

ETag - это непрозрачный идентификатор, назначенный веб-сервером для определенного версию ресурса, найденного по URL-адресу. Если содержимое ресурса при этом URL-адрес когда-либо изменен, назначается новый и другой ETag.

Таким образом, это действительно своего рода уникальная идентификация для браузера, чтобы идентифицировать контент, который доставляется в ответ. Предоставляя этот заголовок, браузер следующей перезагрузки отправит по заголовку If-None-Match с ETag из последнего ответа. Мы можем изменить наш обработчик, чтобы обнаружить заголовок If-None-Match и сравнить его с нашим собственным созданным ETag. Теперь нет точной науки для создания ETags, но хорошим правилом является предоставление идентификатора, который, скорее всего, определит только одну сущность. В этом случае мне нравится использовать две строки, объединенные вместе, например.

System.IO.FileInfo file = new System.IO.FileInfo(context.Server.MapPath("~/saveNew.png"));
string eTag = file.Name.GetHashCode().ToString() + file.LastWriteTimeUtc.Ticks.GetHashCode().ToString();

В приведенном выше фрагменте мы загружаем файл из нашей файловой системы (вы можете получить это из любого места). Затем я использую метод GetHashCode() (для всех объектов), чтобы получить целочисленный хэш-код объекта. В примере я конкатена хэш имени файла, а затем последняя дата записи. Причина последней даты записи в том случае, если файл изменен, хэш-код также изменен, что делает отпечатки пальцев разными.

Это создаст хэш-код, похожий на 306894467-210133036.

Итак, как мы используем это в нашем обработчике. Ниже приведена новая версия обработчика.

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);

Как вы можете видеть, я изменил довольно много обработчика, но вы заметите, что мы генерируем хеш ETag, проверяем входящий заголовок If-None-Match. Если хеш etag и заголовок равны, мы сообщим браузеру, что содержимое не изменилось, возвратив код состояния 304 Not Modified.

Далее был тот же обработчик, за исключением добавления заголовка ETag, вызвав:

context.Response.Cache.SetETag(eTag);

Когда мы запустим это в браузере, мы получим.

Cache-Control with ETag

Вы увидите на изображении (как я изменил имя файла), что теперь у нас есть все компоненты нашей системы кэширования. ETag доставляется как заголовок, а браузер отправляет заголовок запроса If-None-Match, чтобы наш обработчик мог реагировать соответственно на файл кэша.

Ответ 2

Используйте это. Это работает для меня.

<staticContent>
<clientCache cacheControlMode="UseExpires" httpExpires="Tue,19 Jan 2038 03:14:07 GMT"/>
</staticContent>