Cómo configurar Internet Information Server (IIS) para mejorar el rendimiento y el SEO de tu web

Juan Pedro Catalá

Escrito por Juan Pedro Catalá

Configurar de forma óptima el servidor web en el que tienes alojada tu página es uno de los factores clave en el rendimiento de tu web. Establecer correctamente los parámetros para comprimir y cachear los archivos puede mejorar de forma notable la velocidad de carga de tu página.

En este artículo voy a explicar cómo configurar Internet Information Server (IIS), uno de los servidores web más utilizados hoy en día junto con Apache y Nginx.

Para ello voy a ir creando el archivo web.config, que es el archivo en el que se especifica la configuración de una aplicación ASP.NET. Este archivo debe ir siempre en la raíz de nuestra aplicación ASP.NET.

En este caso, tomamos como partida que contamos con un IIS en la versión 7.5 y ASP.NET en la versión 4.5, aunque la mayoría de las cosas también funcionarían en ASP.NET 4.

A continuación especifico el archivo XML web.config con las opciones comentadas.

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <appSettings>
    <!-- Estos parámetros de appSettings solo funcionan en ASP.NET 4.5 o superior -->
    <add key="ClientValidationEnabled" value="true" />
    <!-- Con está opción especificamos que ASP.NET utilice la validación no intrusiva, evitando que inserte código de javascript en linea en todas las páginas de nuestra web -->
    <add key="UnobtrusiveJavaScriptEnabled" value="true" />
  </appSettings>
  <!-- Configuración de ASP.NET -->
  <system.web>
    <!-- 
      Especificamos el framework que utilizamos (4.5)
      El lenguaje de programación (C#)
      Que no compile la aplicación entera cada vez que hagamos un cambio en el código.
      Especificamos que utilice la compilación por lotes y el tamaño máximo de los ensamblados generados al compilar. Esto suele funcionar bien para aplicaciones pequeñas y medianas pero puede dar problemas en aplicaciones con un gran número de archivos, se debe probar y ver la forma en la que la aplicación funciona mejor.
      Establecemos el número máximo de veces que la aplicación puede recompilarse parcialmente antes de reiniciar completamente. Esto es muy útil si no se hacen cambios muy a menudo en la web ya que evitamos que la aplicación tenga que cargar de nuevo todo el código compilado en memoria siendo los cambios transparentes para los visitantes. Si se hacen varios cambios diariamente es mejor establecer está opción a un número menor como 5, para evitar consumir memoria de forma excesiva.
      Especificamos que la aplicación no se ejecute en modo depuración.
      -->
      <compilation targetFramework="4.5" defaultLanguage="C#" optimizeCompilations="true" batch="true" maxBatchGeneratedFileSize="2147483647" maxBatchSize="2147483647" numRecompilesBeforeAppRestart="2147483647" debug="false" />

      <!-- Especificamos un nombre de cookie de sesión corto y habilitamos la compresión, esta opción es muy útil sobre todo utilizamos el modo en el que la sesión se guarda en memoria (InProc) -->
      <sessionState timeout="1440" cookieName="s" compressionEnabled="true" />

      <!-- Eliminamos la cabecera de la versión de ASP.NET para mayor seguridad -->
      <httpRuntime targetFramework="4.5" requestValidationMode="2.0" requestPathInvalidCharacters="" enableVersionHeader="false" relaxedUrlToFileSystemMapping="true" />

<!-- Especificamos que las cookies enviadas no se puedan modificar desde javascript -->
<httpCookies httpOnlyCookies="true" requireSSL="false" />

        <!-- Especificamos el idioma y la codificación de los archivos por defecto. -->
        <globalization culture="es-ES" uiCulture="es-ES" enableBestFitResponseEncoding="false" enableClientBasedCulture="false" fileEncoding="UTF-8" requestEncoding="UTF-8" responseEncoding="UTF-8" responseHeaderEncoding="UTF-8" />

        <!-- 
        (clientIdMode) Especificamos que los IDs de los controles se generen con el algoritmo "Predictable" para que sean lo más cortos posibles.
        (autoEventWireUp) Evitamos que ASP.NET examine la página para asignar los eventos de los controles.
        (renderAllHiddenFieldsAtTopOfForm) Con está opción en false evitamos que en los webforms aparezcan todos los hidden en la parte superior de la página.
        -->
        <pages clientIDMode="Predictable" autoEventWireup="false" enableEventValidation="true" buffer="true" compilationMode="Always" enableSessionState="true" enableViewState="true" enableViewStateMac="true" validateRequest="true" renderAllHiddenFieldsAtTopOfForm="false" viewStateEncryptionMode="Always"></pages>

       <!--
       Especificamos que se utilice la reescritura de la respuesta cuando se produce un error en la aplicación en vez de hacer una redirección. De esta forma logramos que los códigos de estado devueltos sean correctos.
       -->
       <customErrors mode="RemoteOnly" redirectMode="ResponseRewrite" defaultRedirect="error.aspx?error=400">
         <error statusCode="404" redirect="error.aspx?error=404" />
         <error statusCode="500" redirect="error.aspx?error=500" />
       </customErrors>

       <!-- Deshabilitamos el seguimiento -->
       <trace enabled="false" />
  </system.web>

  <!-- Habilitamos la compresión y las cabeceras de cache en los manejadores AJAX de ASP.NET -->
  <system.web.extensions>
    <scripting>
      <scriptResourceHandler enableCaching="true" enableCompression="true" />
    </scripting>
  </system.web.extensions>

  <!-- Configuración de IIS -->
  <system.webServer>
<!-- Habilitamos la compresión GZIP y especificamos las cabeceras de cache a un año -->
        <urlCompression doStaticCompression="true" doDynamicCompression="true" dynamicCompressionBeforeCache="true" />
<httpCompression directory="%SystemDrive%\inetpub\temp\IIS Temporary Compressed Files" sendCacheHeaders="true" cacheControlHeader="max-age=31536000" dynamicCompressionDisableCpuUsage="90" staticCompressionDisableCpuUsage="90" dynamicCompressionEnableCpuUsage="0" staticCompressionEnableCpuUsage="0">
  <scheme name="gzip" dll="%Windir%\system32\inetsrv\gzip.dll" doStaticCompression="true" doDynamicCompression="true" dynamicCompressionLevel="10" staticCompressionLevel="10" />
  <staticTypes>
    <add mimeType="text/*" enabled="true" />
            <add mimeType="message/*" enabled="true" />
            <add mimeType="application/javascript" enabled="true" />
            <add mimeType="application/x-javascript" enabled="true" />
            <add mimeType="application/json" enabled="true" />
            <add mimeType="*/*" enabled="false" />
  </staticTypes>
          <dynamicTypes>
            <add mimeType="text/*" enabled="true" />
            <add mimeType="message/*" enabled="true" />
            <add mimeType="application/javascript" enabled="true" />
            <add mimeType="application/x-javascript" enabled="true" />
            <add mimeType="application/json" enabled="true" />
            <add mimeType="*/*" enabled="false" />
          </dynamicTypes>
</httpCompression>

        <!-- Especificamos que se utilice el reemplazo de la respuesta cuando se produce un error en el servidor en vez de hacer una redirección. De está forma logramos que los códigos de estado devueltos sean correctos. -->
<httpErrors existingResponse="Replace" errorMode="Custom">
  <remove statusCode="404" subStatusCode="-1" />
  <error statusCode="404" subStatusCode="-1" path="/error.aspx?error=404" responseMode="ExecuteURL" />
  <remove statusCode="500" subStatusCode="-1" />
  <error statusCode="500" subStatusCode="-1" path="/error.aspx?error=500" responseMode="ExecuteURL" />
</httpErrors>

        <!-- Deshabilitamos el listado de directorios -->
<directoryBrowse enabled="false" />

        <!-- Deshabilitamos la validación de la configuración en modo integrado -->
<validation validateIntegratedModeConfiguration="false" />

<!-- Deshabilitamos que todos los módulos se ejecuten en todas las peticiones -->
<modules runAllManagedModulesForAllRequests="false" />

        <!-- 
        Establecemos los tipos MIME de todos los archivos que servimos.
        IIS no sirve los archivos que desconoce su tipo MIME por seguridad.
        Establecemos la cache en un año del cliente.
        -->
<staticContent>
          <clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="365.00:00:00" cacheControlCustom="public" />
  <remove fileExtension=".css" />
  <mimeMap fileExtension=".css" mimeType="text/css" />
  <remove fileExtension=".js" />
  <mimeMap fileExtension=".js" mimeType="application/javascript" />
  <remove fileExtension=".json" />
  <mimeMap fileExtension=".json" mimeType="application/json" />
  <remove fileExtension=".rss" />
  <mimeMap fileExtension=".rss" mimeType="application/rss+xml; charset=UTF-8" />
  <remove fileExtension=".html" />
  <mimeMap fileExtension=".html" mimeType="text/html; charset=UTF-8" />
  <remove fileExtension=".xml" />
  <mimeMap fileExtension=".xml" mimeType="application/xml; charset=UTF-8" />
  <remove fileExtension=".mp3" />
  <mimeMap fileExtension=".mp3" mimeType="audio/mpeg" />
  <remove fileExtension=".mp4" />
  <mimeMap fileExtension=".mp4" mimeType="video/mp4" />
  <remove fileExtension=".ogg" />
  <mimeMap fileExtension=".ogg" mimeType="audio/ogg" />
  <remove fileExtension=".ogv" />
  <mimeMap fileExtension=".ogv" mimeType="video/ogg" />
  <remove fileExtension=".webm" />
  <mimeMap fileExtension=".webm" mimeType="video/webm" />
  <remove fileExtension=".svg" />
  <mimeMap fileExtension=".svg" mimeType="image/svg+xml" />
  <remove fileExtension=".svgz" />
  <mimeMap fileExtension=".svgz" mimeType="image/svg+xml" />
  <remove fileExtension=".eot" />
  <mimeMap fileExtension=".eot" mimeType="application/vnd.ms-fontobject" />
  <remove fileExtension=".ttf" />
  <mimeMap fileExtension=".ttf" mimeType="application/x-font-ttf" />
  <remove fileExtension=".ttc" />
  <mimeMap fileExtension=".ttc" mimeType="application/x-font-ttf" />
  <remove fileExtension=".otf" />
  <mimeMap fileExtension=".otf" mimeType="font/opentype" />
  <remove fileExtension=".woff" />
  <mimeMap fileExtension=".woff" mimeType="application/font-woff" />
  <remove fileExtension=".crx" />
  <mimeMap fileExtension=".crx" mimeType="application/x-chrome-extension" />
  <remove fileExtension=".xpi" />
  <mimeMap fileExtension=".xpi" mimeType="application/x-xpinstall" />
  <remove fileExtension=".safariextz" />
  <mimeMap fileExtension=".safariextz" mimeType="application/octet-stream" />
  <remove fileExtension=".flv" />
  <mimeMap fileExtension=".flv" mimeType="video/x-flv" />
  <remove fileExtension=".f4v" />
  <mimeMap fileExtension=".f4v" mimeType="video/mp4" />
  <remove fileExtension=".ico" />
  <mimeMap fileExtension=".ico" mimeType="image/x-icon" />
  <remove fileExtension=".webp" />
  <mimeMap fileExtension=".webp" mimeType="image/webp" />
  <remove fileExtension=".htc" />
  <mimeMap fileExtension=".htc" mimeType="text/x-component" />
  <remove fileExtension=".vcf" />
  <mimeMap fileExtension=".vcf" mimeType="text/x-vcard" />
  <remove fileExtension=".torrent" />
  <mimeMap fileExtension=".torrent" mimeType="application/x-bittorrent" />
  <remove fileExtension=".cur" />
  <mimeMap fileExtension=".cur" mimeType="image/x-icon" />
</staticContent>
<httpProtocol>
  <customHeaders>
<!-- Establecemos la cabecera que indica que nuestra página solo puede aparecer en frames o iframes de nuestro propio dominio. -->
                <add name="X-Frame-Options" value="SAMEORIGIN" />

<!-- Habilitamos la protección contra XSS en Internet Explorer. -->
                <add name="X-XSS-Protection" value="1; mode=block"/>

<!-- Habilitamos la protección contra cambios de tipo MIME en Internet Explorer -->
<add name="X-Content-Type-Options" value="nosniff" />

<!-- Eliminamos la cabecera X-Powered-By -->
<remove name="X-Powered-By" />

<!-- 
                Eliminamos la cabecera ETAG. Si un recurso cambia lo mejor es que cambie de URL para que se vuelva a descargar así nos ahorramos que el cliente haga una petición de consulta por cada archivo con la cabecera ETAG
                -->
                <add name="ETAG" value="" />

<!--
                Forzamos a que Internet Explorer use la última versión de su motor de renderizado.
                -->
<add name="X-UA-Compatible" value="IE=Edge" />
  </customHeaders>
</httpProtocol>

        <!-- Establecemos default.aspx como el archivo por defecto de un directorio -->
        <defaultDocument enabled="true">
          <files>
            <clear />
            <add value="default.aspx" />
          </files>
        </defaultDocument>
<!-- 
        Reglas de reescritura de URLs 
        Es necesario tener instalado el módulo "URL Rewrite Module 2.0"
        -->
        <rewrite>
            <rules>
                <!-- Forzamos que el dominio salga siempre con www, excepto cuando ejecutamos el proyecto en local o empieza por www -->
                <rule name="WWW" stopProcessing="true">
                    <match url="(.*)" />
                    <conditions>
                        <add input="{HTTP_HOST}" pattern="(.*)" />
                        <add input="{HTTP_HOST}" pattern="^www\." negate="true" />
                        <add input="{HTTP_HOST}" pattern="^localhost" negate="true" />
                    </conditions>
                    <action type="Redirect" url="http://www.{C:1}/{R:1}" redirectType="Permanent" />
                </rule>

                <!--  Eliminamos default.aspx de la URL -->
                <rule name="Default Document" stopProcessing="true">
                  <match url="^(.*)default\.aspx$" />
                  <action type="Redirect" url="{R:1}" redirectType="Permanent" />
                </rule>

                <!-- Convierte toda la url a minúscula, excepto cuando sea un archivo, un directorio, un manejador AJAX de ASP.NET o un bundle -->
                <rule name="Lower Case URLs" stopProcessing="true">
                  <match url="[A-Z]" ignoreCase="false" />
                    <conditions logicalGrouping="MatchAll" trackAllCaptures="false">
                      <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
                      <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
                      <add input="{URL}" pattern="^(.+)\.axd$" negate="true" />
                      <add input="{URL}" pattern="^bundles/" negate="true" />
                    </conditions>
                    <action type="Redirect" url="{ToLower:{URL}}" redirectType="Permanent" />
                  </rule>

                <!-- 
                Especificamos regla de reescritura para los bundles de ASP.NET 
                /bundles/css/asd12SDasdada -> /bundles/css?v=asd12SDasdada
                -->
                <rule name="Bundles">
                  <match url="^bundles/([^/]+)/(.+)$" />
                  <action type="Rewrite" url="/bundles/{R:1}?v={R:2}" />
                </rule>

            </rules>
        </rewrite>

  </system.webServer>
</configuration>

 

Realizando una medición antes y otra después de realizar los cambios respecto a los valores por defecto del archivo web.config, encontrarás una gran mejora en la velocidad de carga de tu página web, algo que tus visitantes agradecerán.

Os dejo un enlace al archivo de configuración para que lo podáis descargar cómodamente.

Juan Pedro Catalá
Autor: Juan Pedro Catalá

Únete a la conversación

5 comentarios

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

  1. Un articulo muy interesante con información muy bien explicada y detallada.
    Con respecto al web.config tengo un proyecto de aspx con c# con conexión a sql server
    donde si la consulta a sql tarda demasiado mi pagina revienta con un mensaje de timeout
    he revisando algunas fuentes en internet con posibles soluciones pero ninguna me ha funcionado….

    sabes algún método alternativo para solucionarlo estoy utilizando esta línea
    en mi web.config

    saludos…

    Ernesto Luis.

  2. Genial aportación! Está muy detallado y completo y hay algunas cosas que voy a probar enseguida.

    Tan sólo un par de apuntes:

    – Veo que está muy orientado a ASP.NET WebForms (.aspx), por lo que no toda la configuración es directamente aplicable a cualquier IIS. Si tenemos una aplicación MVC, o simplemente un WordPress sobre IIS, tendremos problemas con este web.config. Estaría bien añadir este «disclaimer».

    – No estoy del todo de acuerdo con la última regla de reescritura para los bundles, ya que una de las reglas de WPO de PageSpeed es «no utilizar querystring en recursos estáticos», porque es posible que algún proxy no lo cachee. Yo lo reescribiría a /bundles/css/sadsadsa.css

    Por lo demás, muy completo e interesante.

    Un saludo!
    Sergi Gisbert