Используем быстрые селекторы для jQuery

Как Вы знаете — в разработке объёмного JS-приложения где используется популярнейшая библиотека jQuery наступает момент когда остро встаёт проблема производительности. Все силы кидаются на амбразуру профайлера, каждый вызов скрупулёзно исследован, каждый функционально нагруженный кусок реализации обнюхан со всех сторон и выправлен. Но беда поступает не с той стороны, откуда её ждут 90% разработчиков. Селекторы — Как много в этом слове.
Давайте разберёмся — как работает эта магия и почему поиск DOM-элементов может стать причиной падения производительности приложения.

Как jQuery разбирает селектор


В самой библиотеке для поиска элементов используется движок Sizzle у которого есть ряд особенностей. Их мы и рассмотрим.

querySelectorAll()

В новых браузерах появилась отменная функция querySelectorAll() и querySelector(), которая умеет производить поиск элементов используя возможности браузера (в частности — используемые при просмотре CSS и назначении свойств элементам). Работает данная функция не во всех браузерах, а только в FF 3.1+, IE8+ (только в стандартном режиме IE8), Opera 9.5+(?) и Safari 3.1+. Так вот Sizzle всегда определяет наличие данной функции и пытается любой поиск выполнить через неё. Однако, тут не без сюрпризов — для успешного использования querySelectorAll() наш селектор должен быть валидным.
Приведу простой пример:
Два приведенных селектора практически ничем не отличаются и вернут одинаковый набор элементов. Однако первый селектор вернёт результат работы querySelectorAll(), а второй — результат обычной фильтрации по элементам.
$('#my_form input[type="hidden"]')
$('#my_form input[type=hidden]')

Разбор селектора и поиск

Если не удалось использовать querySelectorAll(), Sizzle будет пытаться использовать обычные функции браузера getElementById(), getElementsByName(), getElementsByTagName() и getElementByClass(). В большинстве случаев их хватает, но (sic!) в IE < 9 getElementByClassName() поломана и использование селектора по классу приведёт к полному перебору элементов в этом браузере.
В общем случае Sizzle разбирает селектор справа налево. Для иллюстрации данной особенности приведу несколько примеров:
$('.divs .my_class')
Сначала будут найдены элементы .my_class, а потом отфильтрованы только те, которые имеют .divs в родителях. Как мы видим — это довольно затратная операция, причём использование контекста не решает проблемы (о контексте мы поговорим ниже).
Как я уже сказал — в большинстве случаев Sizzle будет разбирать селектор справа налево, но не в случае использования элемента с ID:
$('#divs .my_class')
В таком случае селектор поведёт себя как ожидалось и сразу будет взят элемент #divs для использования в виде контекста.

Контекст

Второй параметр, передаваемый вместе с селектором в функцию $() называется контекстом. Он призван сузить круг поиска элементов. Однако — при разборе контекст пристыкуется к началу селектора, что выигрыша совершенно не даёт. Выигрышная комбинация использования контекста — валидный селектор для querySelectorAll(), так как данная функция может быть применена не только от имени document, но и от элемента. Тогда селектор с контекстом образно трансформируется следующую конструкцию:
$('.divs', document.getElementById('wrapper'));
document.getElementById('wrapper').querySelectorAll('.divs'); // при наличии возможности использовать querySelectorAll()

В данном примере, если невозможно использовать querySelectorAll(), Sizzle будет фильтровать элементы простым перебором.
Ещё одно замечание о контексте (речь не о селекторах) — если вторым параметром в селектор для функции .live() передать объект jQuery — событие будет ловиться на document(!), а если DOM-объект — то всплывать событие будет только до этого элемента. Такие тонкости я стараюсь не запоминать, а использую .delegate().

Фильтры и иерархия элементов

Для поиска вложенных элементов можно воспользоваться следующим селектором:
$('.root > .child')
Как мы знаем — разбор селектора и поиск начнётся со всех .child элементов на странице (при условии, что querySelectorAll() недоступно). Такая операция достаточно затратна и мы можем трансформировать селектор так:
$('.child', $('.root')[0]); // использование контекста не облегчит поиск
$('.root').find('.child'); // а зачем нам перебор всех элементов внутри .root?
$('.root').children('.child'); // самый правильный вариант

Однако, если есть необходимость использовать фильтры по каким-либо атрибутам (:visible, :eq и т.д.) и селектор выглядит так:
$('.some_images:visible')
то фильтр будет применён в последнюю очередь — т.е. мы опять отступаем от правила «справа налево».

Итоги

  • По возможности используйте правильные селекторы, подходящие под querySelectorAll()
  • Заменяйте подчинённости внутри селекторов на подзапросы (.children(...) и т.д.)
  • Уточняйте контекст селектора
  • Фильтруйте как можно меньший готовый набор элементов

Быстрых селекторов Вам в новом году! Всех в наступившим!

По мотивам мастер-класса Ильи Кантора и собственных наблюдений.
Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 31
  • 0
    >> Однако, если есть необходимость использовать фильтры по каким-либо атрибутам (:visible, :eq и т.д.) и селектор выглядит так, то фильтр будет применён в последнюю очередь
    На самом деле $('.el:first') предпочтительнее $('.el'), если нам нужен только первый найденный элемент, даже если он единственный. Как я понимаю, при нахождении первого элемента, дальнейший перебор сокращается. Это немного ускоряет такие куски, если элементов много
    $('.elements').each(function(){
    $(this).attr('rel', $(this).find('.value:first'));
    });

    Еще ускорить можно, заменив $.each() на for(;;){} и убрав двойное $(this)
    • 0
      $(this).find('.value:first')

      А нормально, что ЭТО вернёт объект?
      Вы часто так с атрибутами поступаете?
      • +3
        >> Вы часто так с атрибутами поступаете?
        Нет, к счастью, только когда пытаюсь быстро привести пример и не перечитываю код еще раз =)
        Ну, пусть будет $(this).find('.value:first').html(). Сделайте вид, что там так и написано.
    • +3
      Спасибо, не знал про валидные селекторы. При том, что код я пишу валидный, выбираю элементы не правильно.

      Полезная статья, плюс вам!
      • +1
        В код статьи добавьте, пожалуйста, линк на .querySelectorAll
        и неплохо бы провести бенчмарк
        • 0
          Линк на спеки W3 приведен. В бенчмарке сенса не вижу — ясно что qSA() работает на порядок быстрее, иногда даже быстрее чем getElementByClassName(). Если же сравнивать jQuery и Sizzle с qSA() — оверхед будет, но небольшой (на пару регулярок).
        • 0
          да, чувак! побольше бы таких статей! огромное программистское спасибо! искать такие тонкости невероятно сложно, а тут все на тарелочке!) супер!
          • +2
            кстати, в опере querySelector[All] тоже работает
            • 0
              $('#divs .my_class') — увы, такие конструкции тоже могут ощутимо тормозить, хотя, как написано, и должен сперва искаться id.
              В таких случаях рекомендуют следующую оптимизацию:

              $('#div').find('.my_class')
              • 0
                В Sizzle заложена оптимизация на подобные селекторы, хотя при втором варианте мы обходимся без лишней регулярочки с проверкой
              • +8
                Пару замечаний —

                Исправьте опечатки (фунционально, селетор и т.д.).

                $('#my_form input[type=«hidden»]')
                $('#my_form input[type=hidden]')
                Оба селектора будут использовать qSA, во втором случае sizzle перепишет
                не валидное в валидное.

                getElementByTagName -> getElementsByTagName
                getElementByClass -> getElementsByClassName

                В большинстве случаев их хватает, но (sic!) в IE7 getElementByClass() поломана и использование селектора по классу приведёт к польному перебору элементов в этом браузере.
                getElementsByClassName, был добавлен только в девятом ie (sic!).

                Контекст, не дает выигрыша в производительности с qSA, но он есть без его использования.

                $('.divs', document.getElementById('wrapper'));
                document.getElementById('wrapper').querySelectorAll('.divs'); // при наличии возможности использовать querySelectorAll()
                В спецификации querySelectorAll есть своеобразный баг —
                ejohn.org/blog/thoughts-on-queryselectorall/, поэтому, ваше выражение в jquery, будет выглядить так —

                document.getElementById('wrapper').querySelectorAll('#wrapper .divs');

                $('.child', $('.root')[0]); // использование контекста не облегчит поиск
                Облегчит.

                $('.root').children('.child'); // самый правильный вариант
                Это не самый правильный вариант, это единственно правильный вариант.
                Только оно эквивалентно выражению —
                $('.root > .child')


                • –1
                  про баг с qSA() не знал.
                  $('.root').children('.child'); не эквивалентно $('.root > .child'); — посмотрю сегодня сам код
                  • +1
                    должно быть эквивалетно. .childre() берет только первый уровень детей. если надо копать глубже, есть .find()
                    • 0
                      Давайте разберёмся:
                      Результат селекторов будет эквивалентен, однако путь получения — нет (смотреть тут):
                      — для $('.root > .child');
                      var Expr = Sizzle.selectors = {
                      ...
                      relative: {
                      ...
                      ">": function( checkSet, part ) {
                      ...
                      Sizzle.filter( part, checkSet, true ); // тут обрабатывается магия ">"
                      ...
                      }
                      ...
                      }
                      ...
                      }

                      // А вот метод .children()
                      children: function( elem ) {
                      return jQuery.sibling( elem.firstChild );
                      }
                      sibling: function( n, elem ) {
                      var r = [];
                      for ( ; n; n = n.nextSibling ) {
                      if ( n.nodeType === 1 && n !== elem ) {
                      r.push( n );
                      }
                      }
                      return r;
                      }

                      В результате оно отлично-то возьмёт всех детей первого уровня, но через дополнительные проверки и пару промежуточных функций :)
                      • 0
                        куда делись все пробелы?
                • 0
                  «приведёт к польному перебору „
                • 0
                  Работает данная функция не во всех браузерах, а только в FF 3.1+, IE8+ (только в стандартном режиме IE8), и Safari 3.1+
                  А «Оперу» почему не включили в этот список? Она тоже поддерживает эти вызовы.
                  • 0
                    Включил. Но единственное упоминание я нашёл о версии 9.5, так что это может быть неточно
                  • 0
                    Есть ещё заготовка о событиях в jQuery (более обстоятельная, с выдержками из кода). Coming soon
                    • +1
                      Один в один пересказ мастер-класса Ильи Кантора…
                      javascript.ru/mk
                      Но все равно спасибо =)
                      • +1
                        Не скрою — был там и много полезного для себя вынес. Кстати — сейчас обновлю топик — оставлю ссылку
                      • 0
                        хром потерялся

                        www.quirksmode.org/dom/w3c_core.html
                        • +1
                          >Заменяйте подчинённости внутри селекторов на подзапросы (.children(...) и т.д.)

                          это очень плохо затачивать код под старые браузеры. читаемость падает, скорость для новых браузеров тоже просядет из-за лишних телодвижений.

                          • 0
                            Это оптимизация скорости выполнения. Мой путь: экономить максимум ресурсов на стороне пользователя, потому что заведомо неизвестно на какой машине он будет исполняться.
                          • 0
                            Как мы знаем — разбор селектора и поиск начнётся со всех .child элементов на странице (при условии, что querySelectorAll() недоступно). Такая операция достаточно затратна и мы можем трансформировать селектор так:
                            $('.child', $('.root')[0]); // использование контекста не облегчит поиск
                            $('.root').find('.child'); // а зачем нам перебор всех элементов внутри .root?
                            $('.root').children('.child'); // самый правильный вариант

                            Почему перебор всех элементов .root на странице — менее затратная операция, чем перебор всех элементов .child на странице?
                            • 0
                              теоретически, у каждого .root возможно по нескольку .child, а наоборот — не возможно.
                              • 0
                                Не понял, причем здесь это?
                                • 0
                                  Как причём? Перебрать меньшее количество элементов быстрее, чем перебрать большее.
                                  • 0
                                    То, что .root может содержать несколько .child, совершенно не означает, что элементов .root меньше.

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.