Для современных веб-страниц сценарии, очевидно, являются критически важным элементом. Их вес растет с точки зрения важности и количества байтов. Например, представьте себе одностраничные приложения и объем кода, который им необходим, чтобы предоставить нам все необходимые функции и анимацию, не нажимая кнопку «Перезагрузить» в нашем браузере. Общий размер сценариев на странице далек от размера видео- и аудиофайлов, но он приближается к размеру изображений.
В этой статье я протестирую производительность двух наиболее посещаемых веб-сайтов, Facebook и Twitter, выделив некоторые из их основных проблем. Затем я сосредоточусь на том, как улучшить время загрузки скриптов страницы, познакомив вас с библиотекой basket.js .
Измерение производительности Facebook и Twitter
Прежде чем рассказать о том, что такое basket.js и какую проблему он решает, давайте начнем с примера из реальной жизни. Давайте используем такой инструмент, как Chrome Developer Tools или F12 Developer Tools, чтобы измерить количество переданных байтов и время загрузки стены Facebook. Чтобы выполнить эту задачу надежным способом, вам необходимо перезагрузить страницу в браузере и, конечно же, очистить кэш. Если вам нужны дополнительные инструкции, этот твит Адди Османи поможет вам .
Сказал, что, каков общий размер сценариев, загруженных для средней стены Facebook (приблизительная оценка), и каково соотношение изображений / сценариев? А что касается проблемы CSS против JavaScript, какой из них будет тяжелее? Догадайся и запиши свои ответы, но … не обманывай!
Давайте посмотрим на результаты:
И здесь вы можете найти их резюме:
- Скрипты : общий размер: 785 КБ, количество запросов: 49, общая сумма времени загрузки: ~ 9 с
- CSS : Общий размер: 125 КБ, Количество запросов: 18, Общая сумма времени загрузки: ~ 2,2 с
- Изображения : Общий размер: 251 КБ, Количество запросов: 68, Общая сумма времени загрузки: ~ 8 с
Помните, что файлы загружаются параллельно (до определенного предела), поэтому время загрузки страницы меньше, чем сумма времени загрузки отдельных файлов.
Давайте посмотрим на другой пример, посмотрев на временную шкалу Twitter:
- Скрипты : общий размер: 417 КБ, количество запросов: 4, общая сумма времени загрузки: ~ 650 мс
- CSS : Общий размер: 114 КБ, Количество запросов: 3, Общая сумма времени загрузки: ~ 200 мс
- Изображения : Общий размер: 647 КБ, Количество запросов: 36, Общая сумма времени загрузки: ~ 3,5 с
Хотя подход Twitter к минификации выглядит по-другому, размер сценариев все еще близок к сумме размера всех загруженных изображений.
В этот момент вы можете подумать: «О чем вы говорите? Это всего лишь 1 МБ, об этом даже не стоит беспокоиться! ». Несомненно, в широкополосном соединении (или даже 4G) задержка загрузки скриптов может быть (почти) незначительной. Однако ситуация не одинакова во всех странах. Во многих из них широкополосное соединение не доступно за пределами городских районов. Например, в Италии в сельской местности вы можете столкнуться с модемом 56K, а мобильная связь последнего поколения стала реальностью только в последнее время. Хотя Италия не охватывает большую часть рынка («всего» ~ 60 миллионов потенциальных пользователей), некоторые крупные страны испытывают те же проблемы. Согласно сообщениям Akamai «Состояние Интернета» , в Индии подавляющее большинство населения не имеет доступа к быстрому соединению. Кроме того, согласно тому же отчету, Бразилия является одной из стран с самой низкой средней скоростью соединения.
Основываясь на этом обсуждении, вы можете понять, что кэширование скриптов — это хорошая идея.
Basket.js решает эту проблему для скриптов, как статически, так и динамически загружаемых, сохраняя их в локальном хранилище браузера . Это также позволяет точно контролировать кеширование и время его истечения.
Вы можете возразить, что кеш браузера уже позаботится об этом, и вы были бы правы. Однако локальное хранилище работает быстрее, и это особенно важно на мобильных устройствах . Мы углубим эту тему в следующих разделах, но тот факт, что Google и Microsoft используют эту технику, может уже дать вам веские основания для прочтения этой статьи.
Что такое Basket.js
Как указано на ее сайте, basket.js — это небольшая библиотека JavaScript, поддерживающая кэширование сценариев localStorage.
Эта цитата очень хорошо описывает цель этого проекта. Как только библиотека загружена в память, она отправляет асинхронные запросы на получение других скриптов, необходимых странице. Он вставляет их в документ и затем кэширует их в локальном хранилище браузера. Таким образом, при следующей загрузке страницы сценарии будут загружаться локально без выполнения какого-либо HTTP-запроса.
Вспоминая приведенные выше примеры на Facebook, это означает, что вы сэкономите 49 HTTP-запросов, почти 800 КБ и общее (суммарное) время загрузки ~ 9 секунд (для широкополосного подключения! Можно ожидать, что это будет намного медленнее для 56к один).
LocalStorage против браузера кэш против индексированной БД
Как упоминалось ранее, исследования, проведенные Google и Microsoft, localStorage
что localStorage
намного быстрее, чем кеш браузера. В SitePoint мы недавно освещали эту тему в статье « Возвращение к локальному хранилищу HTML5» , где Луис Виейра также рассмотрел некоторые ограничения localStorage
. Кроме того, IndexedDB (на удивление) медленнее, чем localStorage
, как для чтения, так и для записи.
Получение точных измерений является довольно сложной задачей, и в настоящее время нет подробных исследований, хотя это является одним из приоритетов для проекта .
Как использовать basket.js
Использовать библиотеку действительно просто. Он предусматривает четыре основных метода:
-
basket.require()
: требовать удаленные скрипты и вставлять их на страницу (с кэшированием или без него) -
basket.get()
: проверитьlocalStorage
наличие скриптов -
basket.remove()
: удалить кэшированный скрипт -
basket.clear()
: удалить все кэшированные скрипты
Требовать скрипты
Чтобы потребовать сценарий, мы можем написать заявление вроде следующего:
basket.require({ url: 'jquery.js' });
Этот метод может использоваться для запроса одного или нескольких сценариев за один вызов. Требуется переменное количество аргументов, один объект для каждого скрипта. Вы также можете передать поля для URL скриптов и несколько опций для каждого скрипта. Звонок всегда вернуть обещание. Это обещание выполняется после загрузки сценария или отклонения по ошибке. Это удобно по нескольким причинам:
- становится легко обрабатывать зависимости с помощью цепочки обещаний, чтобы установить порядок загрузки
- это возможно, когда скрипты не могут быть загружены, и, следовательно, изящно терпят неудачу
- как плюс, вы можете кэшировать файл, не выполняя его при загрузке — вы сможете получить его с помощью
.get()
на более позднем этапе, если вам это действительно нужно
Параметры, которые могут быть переданы в скрипт, позволяют установить:
- псевдоним, чтобы ссылаться на него
- если скрипт должен быть выполнен после загрузки
- количество часов, по истечении которых срок действия скрипта истекает или…
- … если он вообще должен пропустить кеш.
Обработка зависимостей
Если ни один из ваших сценариев не имеет зависимостей, вы можете просто потребовать их всех сразу:
basket.require( { url: 'jquery.js' }, { url: 'underscore.js' }, { url: 'backbone.js' } );
В противном случае, basket.js
, basket.js
на обещания, сделает вашу жизнь проще:
basket .require({ url: 'jquery.js' }) .then(function () { basket.require({ url: 'jquery-ui.js' }); });
Fine Grain Script Cache Expiry Management
Как упомянуто выше, сценарии могут храниться вне кэша индивидуально, или время истечения может быть установлено для каждого из них отдельно.
basket.require( // Expires in 2 hours { url: 'jquery.js', expire: 2 }, // Expires in 3 days { url: 'underscore.js', expire: 72 }, // It's not cached at all { url: 'backbone.js', skipCache: true }, // If you later change this value the older version from cache will be ignored { url: 'd3.js', unique: 'v1.1.0' } );
Очистка кеша вручную
Вы можете удалить один элемент из кэша:
basket .remove('jquery.js') .remove('modernizr');
Или вы можете удалить только элементы с истекшим сроком, все сразу, без явного перечисления их
remove basket.clear(true);
Наконец, также возможно очистить все скрипты для вашей страницы:
remove basket.clear();
Проверять вручную элементы в кеше
Вы даже можете предоставить свою собственную пользовательскую функцию для проверки элементов в кеше и выбора времени, чтобы пометить их как устаревшие. Вы можете перезаписать basket.isValidateItem
с помощью функции, которая возвращает значение true
если кэшированный элемент действителен, и значение false
если скрипт должен быть снова загружен из источника.
Это не перезаписывает существующую проверку на expiry
и unique
параметры, но добавляет поверх нее. Более того, даже если перезапись isValidateItem
является мощной опцией, вряд ли она вам действительно когда-либо понадобится.
Практические занятия: давайте создадим пример
Я использовал basket.js
для рефакторинга загрузки скриптов для TubeHound , заменив RequireJS в качестве менеджера скриптов.
Вот как выглядел главный заголовок скрипта:
requirejs.config({ "baseUrl”: "js/", "paths": { "jquery": "./lib/jquery-2.0.3.min", "Ractive": "./lib/Ractive", "utility": "utility", "fly": "./lib/Ractive-transitions-fly", "fade": "./lib/Ractive-transitions-fade", "bootstrap": "./lib/bootstrap.min", "jquery-ui": "./lib/jquery-ui-1.10.4.custom.min", "jquery-contextmenu": "./lib/jquery.contextmenu" }, "shim": { "jquery": { exports: 'jquery' }, "Ractive": { exports: 'Ractive' }, "utility": { deps: ['jquery'], exports: 'utility' }, "bootstrap": { deps: ['jquery'], exports: 'bootstrap' }, "jquery-ui": { deps: ['jquery'], exports: 'jquery-ui' }, "jquery-contextmenu": { deps: ['jquery'], exports: 'jquery-contextmenu' } } }); require([ 'jquery', 'Ractive', 'utility', 'bootstrap', 'fly', 'jquery-ui', 'jquery-contextmenu', 'fade' ], function ($, Ractive, utility) { ... });
Теперь я удалил все это, кроме объявления функции, без всех ее аргументов. Затем я добавил новый небольшой скрипт с именем loading.js
:
(function () { function requireScriptsDependingOnJQueryAndRactive () { return basket.require( { url: 'js/lib/bootstrap.min.js'}, { url: 'js/lib/Ractive-transitions-fly.js', key: 'fly' }, { url: 'js/lib/Ractive-transitions-fade.js', key: 'fade' }, { url: 'js/lib/jquery-ui-1.10.4.custom.min.js', key: 'jquery-ui' }, { url: 'js/lib/jquery.contextmenu.js', key: 'jquery-contextmenu' }, { url: 'js/utility.min.js', key: 'utility', unique: 1 } ); } basket.require( { url: 'js/lib/jquery-2.0.3.min.js', key: 'jquery' }, { url: 'js/lib/Ractive.js', key: 'Ractive' } ).then(requireScriptsDependingOnJQueryAndRactive) .then(function () { basket.require({ url: 'js/thound.min.js', unique: 1 }); //unique is to make sure we can force a reload, in case of bugs }); }());
Теперь это загружается через <script>
на странице HTML (сразу после basket.js
):
<script src="js/lib/basket.min.js"></script> <script src="js/loader.js"></script>
Я выполнил аналогичный рефакторинг для utility.js
. Раньше RequireJS нуждался в сантехнике:
requirejs.config({ "baseUrl": "js/", "paths": { "jquery": "./lib/jquery-2.0.3.min" }, "shim": { "jquery": { exports: 'jquery' } } }); define([ 'jquery' ], function ($) { "use strict"; ... });
После этого я «экспортирую» модуль, используя глобальную переменную, как показано ниже:
var utility = (function () { "use strict"; ... }());
Производительность
Давайте перейдем к зерну: какое улучшение я получил? Вот это базовая линия, полная перезагрузка существующей страницы:
Потребовалось 6,06 с, чтобы загрузить 904 КБ с 28 запросами. Затем я заново загрузил новую версию страницы и снова измерил:
Так как он загружает страницу с нуля, все скрипты загружаются через HTTP-запросы. Для загрузки 899 КБ с 27 запросами потребовалось 4,01 секунды (requireJS был пропущен и заменен на basket.js).
В этот момент, когда вы снова перезагружаете страницу, все сбрасывается из кэша браузера, но сценарии хранятся в localStorage
: дельта будет измерять фактический выигрыш, обеспечиваемый сценариями кэширования.
Результат: 2,01 с для загрузки 352 КБ, необходимого с 18 запросами. Так что для страницы, которая интенсивно использует JS, у вас есть довольно хорошая экономия.
Наконец, давайте посмотрим окончательное время загрузки для нормального доступа к домашней странице:
Используя кеш браузера и basket.js
, страница может быть загружена за 771 мс, и только 5,3 КБ фактически загружены (17 запросов, в основном обслуживаемых из кеша).
Выводы
Эта библиотека — хорошая идея, с одним недостатком полагаться на неидеальный API данных. Соображения, которые привели к выбору localStorage
, вполне понятны. Он пытается повысить производительность, и опыт показывает, что localStorage
является самым быстрым доступным решением.
С другой стороны, как любит говорить Дональд Кнут, «преждевременная оптимизация — корень всего зла»! Без тщательного и тщательного сравнения производительности трудно взвесить ограничения, вызванные ограничениями квот. К сожалению, проблемы с localStorage
не исчезнут в ближайшее время, по крайней мере, для Chrome, где увеличение квоты потребует некоторой нетривиальной перезаписи .
Хорошая новость заключается в том, что авторы basket.js
рассматривают несколько альтернатив, включая многоуровневое решение , которое попытается использовать лучший постоянный API, доступный в браузере: Service Workers, Cache API (в Chrome) или FileSystem API.
Я был немного удивлен, увидев, что сервисные работники изначально не рассматривались, но, видимо, это скоро изменится. И, что еще лучше, есть ряд новых библиотек, работающих над похожими идеями с разных сторон. Shed , например, выглядит многообещающе: решение еще более широкого спектра, которое делает Service Workers очень простым в использовании.
Несколько проблем, с которыми я мог столкнуться из первых рук (и сгорел) при попытке использовать его в реальном проекте:
- Обратная связь может быть значительно улучшена: трудно сказать, что происходит, когда не удается загрузить ваши сценарии. Если вам повезет, вы можете увидеть какие-то ошибки, но сообщения далеко не значимы. Например, я передавал фактический массив методу
require()
: все, что я получил, это общийTypeError
из кода библиотеки, поэтому мне потребовалось много проб и ошибок, чтобы понять мою ошибку. - Теперь, если вам не повезло: скрипт может вообще не загрузиться, потому что у вас есть опечатка (fi
basker.require
) внутри обратного вызова по цепочке обещаний. Так как ваше сообщение об ошибке проглочено, вам потребуется некоторое время, чтобы понять это. - Если у вас есть зависимости для ваших сценариев, вы теряете понятный декларативный синтаксис, который вы используете с помощью RequireJS, где вы можете перечислить зависимости для каждого из них.
- С другой стороны, все ваши зависимости перечислены и упорядочены в одном месте. И давайте посмотрим правде в глаза, RequireJS является немного многословным и избыточным.
- Как только они будут кэшированы, скрипты, загруженные асинхронно, не будут отображаться на панели « Сеть» инструментов разработки Chrome (или Firefox). Более того, вы не увидите их в списке источников, даже если они загружены из удаленного источника. Это усложняет отладку, но ее можно обойти во время разработки, если вы используете
basket.js
только в производственнойbasket.js
, когда необходима оптимизация.
По правде говоря, этот проект еще не вышел на первую версию, и на данный момент ясно, что это всего лишь эксперимент. И действительно, basket.js
является очень многообещающей идеей, и результаты выглядят действительно хорошо — но я basket.js
что для того, чтобы быть готовым к использованию для разработки сложного проекта — или для производства огромной страницы, требуется небольшой дополнительный шаг. , (Это было бы верно для любого проекта, который не достиг версии 1, из-за возможных изменений в его интерфейсе / структуре).
Вместо этого для небольшого и среднего проекта это может быть хорошим способом сократить время загрузки и разочарование ваших пользователей. Я, например, буду следить за этим, и я буду рад поддержать его принятие, как только проект достигнет зрелости.