
Все начиналось холодным февральским вечером. Клиент написал о странной проблеме при ускорении сайта: слайд-шоу множилось и блокировало поведение сайта, страницы были недоступны. Через два дня после выяснения всех подробностей я узнал, почему Mootools и jQuery категорически нельзя использовать совместно. И подтвердился в мысли, что и «алкоголь — зло», и «eval — зло».
Но обо всем по порядку.
Выясняем корень бед
На момент сейчас в браузерах есть достаточное количество инструментов профилирования (даже, можно сказать, это количество несколько избыточное), позволяющих зафиксировать проблемное место, воспроизвести ошибку и ее устранить. Это, в первую очередь:
- Сетевая панель в инструментах разработчика: она отображает все запросы, легитимно переданные браузером, со всеми заголовками и ответами. Если речь не идет о каком-то шифровании трафика и нужна стандартная информация о запросах (заголовки, состояние, размер), то ее более чем хватает.
- Консоль (ошибок) в инструментах разработчика. В случае возникновения проблемы можно посмотреть как причину ошибки, так и стек обратных вызовов. А также повторить поведение браузера при выполнении кода.
- DOM-дерево и исходный код сайта. Не всегда бывает понятно, какие свойства были заданы, а какие — динамически применились к HTML-элементам. Но всегда можно посмотреть информацию «из первых рук» — от браузера.
Если бы речь шла о простой трассировке ошибке — то на этом статью можно было бы заканчивать. Но ошибка была непростая, а рекурсивная. И вкладка браузера «падала» через несколько секунд после загрузки страницы, оставляя в консоли ошибок примерно такой след:
Стек вызовов
c.Request.Class.send @ mootools-core.js:182 i.extend.$owner @ mootools-core.js:50 Element.implement.load @ mootools-core.js:187 st.event.trigger @ jquery.js:2989 (anonymous function) @ jquery.js:3639 st.extend.each @ jquery.js:642 st.fn.st.each @ jquery.js:263 st.fn.extend.trigger @ jquery.js:3638 st.fn.(anonymous function) @ jquery.js:3662 st.fn.load @ jquery.js:7498 (anonymous function) @ jquery.bxslider.js:11 st.extend.each @ jquery.js:642 st.fn.st.each @ jquery.js:263 (anonymous function) @ jquery.bxslider.js:11 st.extend.each @ jquery.js:642 st.fn.st.each @ jquery.js:263 loadElements @ jquery.bxslider.js:11 setup @ jquery.bxslider.js:11 init @ jquery.bxslider.js:6 $.fn.bxSlider @ jquery.bxslider.js:52 (anonymous function) @ VM375:2 f @ jquery.js:1026 p.fireWith @ jquery.js:1138 st.extend.ready @ jquery.js:427 xt @ jquery.js:97
На сайте это отображалось в повторяющемся слайдере (множественные стрелочки справа и слева — это несколько копий слайдера, наложенных друг на друга из-за ошибки):

Лирическое отступление
Стоит сказать, что и MooTools, и jQuery приходили на сайт в сжатом виде. Однако, к jQuery шла карта исходников (source map), что существенно облегчило поиск крайнего (=виноватого).
Карта исходников — чрезвычайно полезная вещь в отладке сжатого кода, выпущена уже третья спецификация, сократившая объем карты в несколько раз. Если вы слышите про карту исходников впервые, то советую обратить на нее внимание. Но двигаемся дальше.
Копаем вглубь
В любой проблеме важно выделить минимальные условия, все еще приводящие к возникновению проблемы (но без дополнительных данных), и эти условия последовательно проанализировать. Как оказалось, минимальным условием для стабильного воспроизведения проблемы было наличие встроенного (inline) JavaScript-кода в HTML-странице. Механизмы кэширования в браузере и у интернет-провайдеров могут помешать сделать простые выводы, поэтому при работе с публичными незашифрованными HTML-страницами необходимо четко формулировать и несколько раз перепроверять условия, чтобы быть в них уверенным наверняка.
Но почему включение JavaScript-кода в HTML приводило к рекурсивному его выполнению в браузере? Яндекс и Google ничего о таких ситуациях пока не знает. Нужно ему помочь.
Гипотезы: мало не бывает

Разобраться помогает стек вызовов (листинг выше). Bxslider (
Written while drinking Belgian ales and listening to jazz
— бельгийский эль точно помешал автору спрогнозировать некоторые нестандартные варианты развития событий) на каждом объекте (в нашем случае — картинке) вызывал свойство load, которое обрабатывалось через jQuery примерно следующим образом:Обработка события jQuery
jQuery.event.triggered = type; try { elem[ type ](); } catch ( e ) { // IE<9 dies on focus/blur to hidden element (#1486,#12518) // only reproducible on winXP IE8 native, not IE9 in IE8 mode }
Вроде все четко: jQuery вызывает нативный метод у элемента, как только со всей остальной оберткой закончили. В данном случае, это
img["load"]()
. Что должно приводить к завершению события load у изображения, оно должно положиться в кэш браузера, и все должны быть счастливы. Но с такой ситуацией не согласна библиотека MooTools:Обработка load MooTools
Element.Properties.load = { set: function ( a ) { var b = this.get("load").cancel(); b.setOptions(a); return this; }, get: function() { var a = this.retrieve("load"); if ( !a ) { a = new Request.HTML( {data: this, link: "cancel", update:this, method:"get"} ); this.store("load", a); } return a } };
MooTools метод
load
понимает по-своему. И в случае отсутствия информации об объекте, объект загружается через new Request.HTML
. Вроде тоже нормально: давайте еще раз загрузим картинку, если о ней нет информации у MooTools (ведь картинка уже загрузилась в браузерный кэш, это просто операции в памяти компьютера локального пользователя). Но jQuery, когда вызывает этот метод у изображения, почему-то забывает передать параметры, в частности, URL. Наверное, jQuery не знает, что после него еще будет работать MooTools, которому эти параметры ой как нужны будут. И MooTools без параметров загружает «пустой» URL (текущую страницу).Тоже вроде допустимый расклад: при загрузке страницы браузер загрузит ее еще 5 раз с сервера (просто HTML-документ), если в слайдере загружается 5 картинок (так и происходило на сайте). Если страница в серверном кэше, то на производительности это (почти) никак не отражается (и в ресурсах на сетевой панели инструментов разработчика найти эти «лишние» вызовы, перемешанные с картинками и счетчиками, тоже непросто).
Но проблема в том, что по умолчанию MooTools выполняет
eval
всех скриптов в загруженном HTML-документе. И это уже хуже: мы можем пережить выполнение кода счетчиков по нескольку раз на сайте. А если начинает выполняться обработчик DOMReady, который загружает слайдер, который вызывает загрузку изображений, которые вызывают загрузку HTML-страницы, на которой исполняется весь inline-код, который запускает обработчик DOMReady… Ну, вы поняли.Резюме
Не использовать совместно несколько JavaScript-фреймворков. Никогда (для Joomla! есть даже плагин, вырезающий MooTools из системы). Если вдруг возникнет желание — перечитать данную статью. Описанная проблема оказалась «на поверхности» и была быстро выявлена. Но возможно ситуации, когда совместное поведение фреймворков будет зависеть от сети (задержек и порядка прохождения запросов) и используемого браузера. И тогда найти причину проблемы и ее исправить возможным совсем не кажется.
Использовать
eval
только в тех случаях, когда вы контролируете исполняемый код. Если нет контроля, нет eval.Алкоголь — зло, эль — зло. Трезвый образ жизни — наше все.
Инструменты разработчика в браузерах реально могут дать вам всю необходимую информацию. Нужно уметь ими пользоваться.