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

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