<p> Мы продолжаем двигаться по нашему <a href="/post/2008/10/Quik-Web-Sites-Compress-Optimization-Content.aspx" target="_blank" title="быстрые веб страницы: уравнение">уравнению</a> производительности приложений в глобальных сетях. И на очереди у нас оптимизация количества файлов, загружаемых браузером. </p> <blockquote> <p> <strong>AppTurns - </strong>число файлов ресурсов, требуемых страницей. Они включают файлы CSS, JS, изображений и любые другие файлы, извлекаемые обозревателем в процессе визуализации страницы. Уравнение учитывает страницу HTML отдельно, добавляя время приема-передачи (round-trip time – RTT) перед выражением AppTurns. </p> </blockquote> <p> Использование множества CSS- и JS-файлов небольшого размера, является хорошей практикой с точки зрения удобства в разработке веб-приложения. Но плохо, с точки зрения производительности вашего приложения. Каждый запрос к серверу требует определенных ресурсов как браузера, так и самого сервера: ожидание в очереди в браузере, время на инициализацию и отправку запроса, передача соответствующих заголовков и cookies, время на обработку запроса и формирования ответа. Время, необходимое для обработки/формирования запроса, называется latency(серенькая область на рисунках ниже). Каждый ресурс, помещенный в отдельный файл, будет бесцеремонно занимать это драгоценное для клиента время. </p> <p> <a href="http://www.podlipensky.com/image.axd?picture=WindowsLiveWriter/60f3384b7291_12876/image_2.png"><img style="border-width: 0px" src="http://www.podlipensky.com/image.axd?picture=WindowsLiveWriter/60f3384b7291_12876/image_thumb.png" border="0" alt="podlipensky.com without combiner" title="podlipensky.com without combiner" width="644" height="181" /></a> </p> <p> Поэтому имеет смысл объединять однотипные файлы ресурсов в один. Таким образом, мы экономим на этот самом latency каждого файла ресурсов. </p> <p> <a href="http://www.podlipensky.com/image.axd?picture=WindowsLiveWriter/60f3384b7291_12876/image_4.png"><img style="border-width: 0px" src="http://www.podlipensky.com/image.axd?picture=WindowsLiveWriter/60f3384b7291_12876/image_thumb_1.png" border="0" alt="podlipensky.com with combiner" title="podlipensky.com with combiner" width="644" height="57" /></a> </p> <p> Так как мы говорим о CSS или JS коде, то объединение будет представлять собой конкатенацию строк. Проще всего написать HttpHandler, который бы смог объединять файлы ресурсов в runtime режиме, оставляя разработчику возможность оперировать множеством файлов. При этом не стоит забывать о таких способах оптимизации как GZIP и кэширование ваших ресурсов. Код такого HttpHandler'a может быть таким: </p> <div style="border: 1px solid gray; margin: 20px 0px 10px; padding: 4px; overflow: auto; font-size: 8pt; width: 97.5%; cursor: text; max-height: 200px; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <div style="border-style: none; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: white"> <span style="color: #606060"> 1:</span> <span style="color: #0000ff">public</span> <span style="color: #0000ff">void</span> ProcessRequest (HttpContext context) { </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <span style="color: #606060"> 2:</span> </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: white"> <span style="color: #606060"> 3:</span> HttpRequest request = context.Request; </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <span style="color: #606060"> 4:</span> </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: white"> <span style="color: #606060"> 5:</span> <span style="color: #008000">// Читаем параметры: имя ключа в конфиге(содержит список файлов для объединения), тип конетнат и версию конента. </span> </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <span style="color: #606060"> 6:</span> <span style="color: #008000">//Все эти поля обязательны, т.к. они используются как ключ для кэширования.</span> </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: white"> <span style="color: #606060"> 7:</span> <span style="color: #0000ff">string</span> setName = request[<span style="color: #006080">"s"</span>] ?? <span style="color: #0000ff">string</span>.Empty; </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <span style="color: #606060"> 8:</span> <span style="color: #0000ff">string</span> contentType = request[<span style="color: #006080">"t"</span>] ?? <span style="color: #0000ff">string</span>.Empty; </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: white"> <span style="color: #606060"> 9:</span> <span style="color: #0000ff">string</span> version = request[<span style="color: #006080">"v"</span>] ?? <span style="color: #0000ff">string</span>.Empty; </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <span style="color: #606060"> 10:</span>  </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: white"> <span style="color: #606060"> 11:</span> <span style="color: #008000">// Определяем, позволяет ли браузер сжатие контента</span> </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <span style="color: #606060"> 12:</span> <span style="color: #0000ff">bool</span> isCompressed = DO_GZIP && <span style="color: #0000ff">this</span>.CanGZip(context.Request); </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: white"> <span style="color: #606060"> 13:</span>  </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <span style="color: #606060"> 14:</span> <span style="color: #008000">// Ответ формируем в UTF8 кодировке</span> </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: white"> <span style="color: #606060"> 15:</span> UTF8Encoding encoding = <span style="color: #0000ff">new</span> UTF8Encoding(); </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <span style="color: #606060"> 16:</span>  </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: white"> <span style="color: #606060"> 17:</span> <span style="color: #008000">// Если ресурс есть в кэше, то берем из кэша.</span> </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <span style="color: #606060"> 18:</span> <span style="color: #008000">// В противном случае, считываем файлы, объединяем и кладем в кэш</span> </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: white"> <span style="color: #606060"> 19:</span> <span style="color: #0000ff">if</span> (!<span style="color: #0000ff">this</span>.WriteFromCache(context, setName, version, isCompressed, contentType)) </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <span style="color: #606060"> 20:</span> { </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: white"> <span style="color: #606060"> 21:</span> <span style="color: #0000ff">using</span> (MemoryStream memoryStream = <span style="color: #0000ff">new</span> MemoryStream(5000)) </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <span style="color: #606060"> 22:</span> { </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: white"> <span style="color: #606060"> 23:</span> <span style="color: #008000">// Определяем тип потока, в который будем писать результат</span> </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <span style="color: #606060"> 24:</span> <span style="color: #0000ff">using</span> (Stream writer = isCompressed ? </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: white"> <span style="color: #606060"> 25:</span> (Stream)(<span style="color: #0000ff">new</span> GZipStream(memoryStream, CompressionMode.Compress)) : </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <span style="color: #606060"> 26:</span> memoryStream) </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: white"> <span style="color: #606060"> 27:</span> { </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <span style="color: #606060"> 28:</span>  </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: white"> <span style="color: #606060"> 29:</span> <span style="color: #008000">// Загружаем файлы, основываясь на ключе в <appSettings></span> </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <span style="color: #606060"> 30:</span> <span style="color: #0000ff">string</span> setDefinition = </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: white"> <span style="color: #606060"> 31:</span> System.Configuration.ConfigurationManager.AppSettings[setName] ?? <span style="color: #006080">""</span>; </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <span style="color: #606060"> 32:</span> <span style="color: #0000ff">string</span>[] fileNames = setDefinition.Split(<span style="color: #0000ff">new</span> <span style="color: #0000ff">char</span>[] { <span style="color: #006080">','</span> }, </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: white"> <span style="color: #606060"> 33:</span> StringSplitOptions.RemoveEmptyEntries); </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <span style="color: #606060"> 34:</span> <span style="color: #0000ff">string</span> delimeter = <span style="color: #006080">"\r\n"</span>; </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: white"> <span style="color: #606060"> 35:</span> <span style="color: #0000ff">foreach</span> (<span style="color: #0000ff">string</span> fileName <span style="color: #0000ff">in</span> fileNames) </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <span style="color: #606060"> 36:</span> { </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: white"> <span style="color: #606060"> 37:</span> <span style="color: #008000">//Добавляем разделитель, чтобы в двух соседних файлах не произошло объединения ключевых слов.</span> </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <span style="color: #606060"> 38:</span> <span style="color: #0000ff">byte</span>[] bytes = encoding.GetBytes(delimeter); </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: white"> <span style="color: #606060"> 39:</span> writer.Write(bytes, 0, bytes.Length); </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <span style="color: #606060"> 40:</span> </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: white"> <span style="color: #606060"> 41:</span> <span style="color: #0000ff">string</span> text = GetFileText(context, fileName.Trim(), encoding); </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <span style="color: #606060"> 42:</span> <span style="color: #0000ff">byte</span>[] fileBytes = encoding.GetBytes(text); </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: white"> <span style="color: #606060"> 43:</span> writer.Write(fileBytes, 0, fileBytes.Length); </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <span style="color: #606060"> 44:</span> } </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: white"> <span style="color: #606060"> 45:</span>  </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <span style="color: #606060"> 46:</span> writer.Close(); </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: white"> <span style="color: #606060"> 47:</span> } </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <span style="color: #606060"> 48:</span>  </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: white"> <span style="color: #606060"> 49:</span> <span style="color: #008000">//Кэшируем результат</span> </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <span style="color: #606060"> 50:</span> <span style="color: #0000ff">byte</span>[] responseBytes = memoryStream.ToArray(); </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: white"> <span style="color: #606060"> 51:</span> context.Cache.Insert(GetCacheKey(setName, version, isCompressed), </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <span style="color: #606060"> 52:</span> responseBytes, <span style="color: #0000ff">null</span>, System.Web.Caching.Cache.NoAbsoluteExpiration, </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: white"> <span style="color: #606060"> 53:</span> CACHE_DURATION); </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <span style="color: #606060"> 54:</span>  </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: white"> <span style="color: #606060"> 55:</span> <span style="color: #008000">// Генерируем ответ</span> </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <span style="color: #606060"> 56:</span> <span style="color: #0000ff">this</span>.WriteBytes(responseBytes, context, isCompressed, contentType); </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: white"> <span style="color: #606060"> 57:</span> } </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: #f4f4f4"> <span style="color: #606060"> 58:</span> } </pre> <pre style="border-style: none; margin: 0em; padding: 0px; overflow: visible; font-size: 8pt; width: 100%; color: black; line-height: 12pt; font-family: consolas,'Courier New',courier,monospace; background-color: white"> <span style="color: #606060"> 59:</span> } </pre> </div> </div> <p> Функция GetFileText читает содержимое файла и возвращает его в виде строки (ввиду простоты функции, я не стал приводить листинг ее кода). </p> <p> Второй путь уменьшить количество компонентов страницы — упростить ее дизайн. А есть ли способ сохранить внешний вид, при этом уменьшив время загрузки? Ниже следует несколько приемов, позволяющих добиться уменьшения количества запросов к серверу, сохраняя функциональность и внешний вид. </p> <p> <a href="http://www.w3.org/TR/html401/struct/objects.html#h-13.6">Image Map</a>‘ы объединяют несколько картинок в одну большую. Общий объем такой картинки примерно равен сумме объемов маленьких картинок, но уменьшение количества запросов к серверу сокращает общее время загрузки страницы. Image Map будет работать, если картинки на странице находятся рядом друг с другом, например в случае полосы навигации. Определение координат для Image Map’ов — занятие довольно утомительное и зачастую приводит к ошибкам (вообще-то для этого есть специальный софт, однако ничего не могу сказать про качество его работы). </p> <p> <a href="http://alistapart.com/articles/sprites">CSS-спрайты</a> являются предпочтительным методом сокращения количества запросов на сервер. Объедините все картинки веб-страницы в одну большую картинку и используйте CSS-свойства для отображения нужного участка картинки. </p> <p> <img src="http://alistapart.com/d/sprites/sprites.gif" alt="" /> </p> <p> Inline-картинки используют <a href="http://tools.ietf.org/html/rfc2397">URL-схему</a> data: для встраивания картинки в саму страницу. Это, однако, увеличит размер HTML-документа. Встраивайте inline-картинки в ваши (кешированные) таблицы стилей — и вы добьетесь уменьшения запросов к серверу, а размер HTML останется прежним. </p> <p> <strong>Полезные ссылки</strong> </p> <p> <a href="/post/2008/10/Quick-web-sites-download-site-content-from-different-domains.aspx" target="_blank" title="Быстрые веб-страницы: Формирование страницы из нескольких источников">Быстрые веб-страницы: Формирование страницы из нескольких источников</a> </p> <p> <a href="/post/2008/10/Quik-Web-Sites-Compress-Optimization-Content.aspx" target="_blank" title="Быстрые веб-страницы: Сжатие и Очистка веб-страниц">Быстрые веб-страницы: Сжатие и Очистка веб-страниц</a> </p> <p> <a href="http://yuiblog.com/blog/2008/07/21/performance-research-part-6/" target="_blank">Performance Research, Part 6: Less is More — Serving Files Faster by Combining Them</a> </p> <p> <a rel="nofollow" href="http://articles.techrepublic.com.com/5100-10878_11-5766810.html" target="_blank">Combine JavaScript with ASP.NET Web forms</a> </p> <p class="post-title"> <a rel="bookmark" href="http://css-tricks.com/css-sprites-what-they-are-why-theyre-cool-and-how-to-use-them/" title="Permanent Link to CSS Sprites: What They Are, Why They’re Cool, and How To Use Them">CSS Sprites: What They Are, Why They’re Cool, and How To Use Them</a> </p> <p>   </p>

Мы продолжаем двигаться по нашему уравнению производительности приложений в глобальных сетях. И на очереди у нас оптимизация количества файлов, загружаемых браузером.

AppTurns - число файлов ресурсов, требуемых страницей. Они включают файлы CSS, JS, изображений и любые другие файлы, извлекаемые обозревателем в процессе визуализации страницы. Уравнение учитывает страницу HTML отдельно, добавляя время приема-передачи (round-trip time – RTT) перед выражением AppTurns.

Использование множества CSS- и JS-файлов небольшого размера, является хорошей практикой с точки зрения удобства в разработке веб-приложения. Но плохо, с точки зрения производительности вашего приложения. Каждый запрос к серверу требует определенных ресурсов как браузера, так и самого сервера: ожидание в очереди в браузере, время на инициализацию и отправку запроса, передача соответствующих заголовков и cookies, время на обработку запроса и формирования ответа. Время, необходимое для обработки/формирования запроса, называется latency(серенькая область на рисунках ниже). Каждый ресурс, помещенный в отдельный файл, будет бесцеремонно занимать это драгоценное для клиента время.

podlipensky.com without combiner

Поэтому имеет смысл объединять однотипные файлы ресурсов в один. Таким образом, мы экономим на этот самом latency каждого файла ресурсов.

podlipensky.com with combiner

Так как мы говорим о CSS или JS коде, то объединение будет представлять собой конкатенацию строк. Проще всего написать HttpHandler, который бы смог объединять файлы ресурсов в runtime режиме, оставляя разработчику возможность оперировать множеством файлов. При этом не стоит забывать о таких способах оптимизации как GZIP и кэширование ваших ресурсов. Код такого HttpHandler'a может быть таким:

   1: public void ProcessRequest (HttpContext context) {
   2:         
   3:         HttpRequest request = context.Request;
   4:         
   5:         // Читаем параметры: имя ключа в конфиге(содержит список файлов для объединения), тип конетнат и версию конента. 
   6:         //Все эти поля обязательны, т.к. они используются как ключ для кэширования.
   7:         string setName = request["s"] ?? string.Empty;
   8:         string contentType = request["t"] ?? string.Empty;
   9:         string version = request["v"] ?? string.Empty;
  10:  
  11:         // Определяем, позволяет ли браузер сжатие контента
  12:         bool isCompressed = DO_GZIP && this.CanGZip(context.Request);
  13:  
  14:         // Ответ формируем в UTF8 кодировке
  15:         UTF8Encoding encoding = new UTF8Encoding();
  16:  
  17:         // Если ресурс есть в кэше, то берем из кэша.
  18:         // В противном случае, считываем файлы, объединяем и кладем в кэш
  19:         if (!this.WriteFromCache(context, setName, version, isCompressed, contentType))
  20:         {
  21:             using (MemoryStream memoryStream = new MemoryStream(5000))
  22:             {
  23:                 // Определяем тип потока, в который будем писать результат
  24:                 using (Stream writer = isCompressed ?
  25:                     (Stream)(new GZipStream(memoryStream, CompressionMode.Compress)) :
  26:                     memoryStream)
  27:                 {
  28:  
  29:                     // Загружаем файлы, основываясь на ключе в <appSettings>
  30:                     string setDefinition = 
  31:                         System.Configuration.ConfigurationManager.AppSettings[setName] ?? "";
  32:                     string[] fileNames = setDefinition.Split(new char[] { ',' }, 
  33:                         StringSplitOptions.RemoveEmptyEntries);
  34:                     string delimeter = "\r\n";
  35:                     foreach (string fileName in fileNames)
  36:                     {   
  37:                         //Добавляем разделитель, чтобы в двух соседних файлах не произошло объединения ключевых слов.
  38:                         byte[] bytes = encoding.GetBytes(delimeter);
  39:                         writer.Write(bytes, 0, bytes.Length);
  40:                         
  41:                         string text = GetFileText(context, fileName.Trim(), encoding);
  42:                         byte[] fileBytes = encoding.GetBytes(text);
  43:                         writer.Write(fileBytes, 0, fileBytes.Length);
  44:                     }
  45:  
  46:                     writer.Close();
  47:                 }
  48:  
  49:                 //Кэшируем результат
  50:                 byte[] responseBytes = memoryStream.ToArray();
  51:                 context.Cache.Insert(GetCacheKey(setName, version, isCompressed),
  52:                     responseBytes, null, System.Web.Caching.Cache.NoAbsoluteExpiration,
  53:                     CACHE_DURATION);
  54:  
  55:                 // Генерируем ответ
  56:                 this.WriteBytes(responseBytes, context, isCompressed, contentType);
  57:             }
  58:         }
  59:     }

Функция GetFileText читает содержимое файла и возвращает его в виде строки (ввиду простоты функции, я не стал приводить листинг ее кода).

Второй путь уменьшить количество компонентов страницы — упростить ее дизайн. А есть ли способ сохранить внешний вид, при этом уменьшив время загрузки? Ниже следует несколько приемов, позволяющих добиться уменьшения количества запросов к серверу, сохраняя функциональность и внешний вид.

Image Map‘ы объединяют несколько картинок в одну большую. Общий объем такой картинки примерно равен сумме объемов маленьких картинок, но уменьшение количества запросов к серверу сокращает общее время загрузки страницы. Image Map будет работать, если картинки на странице находятся рядом друг с другом, например в случае полосы навигации. Определение координат для Image Map’ов — занятие довольно утомительное и зачастую приводит к ошибкам (вообще-то для этого есть специальный софт, однако ничего не могу сказать про качество его работы).

CSS-спрайты являются предпочтительным методом сокращения количества запросов на сервер. Объедините все картинки веб-страницы в одну большую картинку и используйте CSS-свойства для отображения нужного участка картинки.

Inline-картинки используют URL-схему data: для встраивания картинки в саму страницу. Это, однако, увеличит размер HTML-документа. Встраивайте inline-картинки в ваши (кешированные) таблицы стилей — и вы добьетесь уменьшения запросов к серверу, а размер HTML останется прежним.

Полезные ссылки

Быстрые веб-страницы: Формирование страницы из нескольких источников

Быстрые веб-страницы: Сжатие и Очистка веб-страниц

Performance Research, Part 6: Less is More — Serving Files Faster by Combining Them

Combine JavaScript with ASP.NET Web forms

CSS Sprites: What They Are, Why They’re Cool, and How To Use Them