JavaScript

индекс
246,46

Yet Another cSS selector = YASS

После заметки о Peppy я почти обрадовался — вот оно, счастье. Наконец появился движок CSS-селекторов, который делает тоже самое, что и jQuery, только быстрее. Сильно быстрее.

Радоваться было недолго, как только я посмотрел в код, то немного ужаснулся: он не соответствовал моим представлениям об исключительной производительности. Совсем не соответствовал. Точнее, немного удовлетворял, но не до конца. Поэтому сразу встал вопрос: а если быстрее?

Почему нельзя сделать быстрое мини-ядро для CSS-селекторов, которое обеспечит базовую функциональность для работы с DOM (ну, совсем базовую — просто выборку элементов, например)? И, самое главное, чтобы это работало не медленнее (в идеале, даже быстрее), чем нативные вызовы.

Основы быстродействия


Ну, подумали и сделали. После двух бессонных дней ночей удалось набросать простой движок для выбора элементов по CSS1-селекторам (попутно был отброшен XPATH как на порядок более медленная технология и осознано, почему jsForms также работают несколько медленнее, чем встроенные DOM-методы браузера).

Во-вторых, такой код должен и кэшировать выборки (DOM-вызовы дорого обходятся, все нормальные JavaScript-программисты уже их кэшируют — так упростим им задачу и повысим быстродействие).

В-третьих, есть же статистика использования CSS-селекторов в проектах — значит, у нас есть набор элементарных операций, которые должны выполняться быстрее всего (может быть, даже в ущерб более общей производительности).

В-четвертых, библиотека должна возвращать сами элементы, чтобы к ним можно было «прикрутить» любые обертки и нарастить методы. Все обертки ресурсоемки. Если нужно просто 200 раз поменять HTML на странице у заданных элементов, то они не нужны. Достаточно и проверок со стороны браузера на допустимость выполняемых операций.

В-пятых, естественно, библиотека должна опираться на все самое быстрое: быстрые итераторы, быстрые подстановки, анонимные функции, минимум вложенных вызовов и т.д.

Код


В качестве лучшей иллюстрации приведу сам код (он доступен и на тестовой странице YASS), ибо его немного (это же мини-ядро, все-таки, сорри за подсветку, не очень легкочитаемо получилось):
/* открываем анонимную функцию */
(function(){
/* объявляем локальную переменную, затем ее переведем в window. Переменная называется _
-- нет конфиликтов с текущими реализациями подобного функционала через $ */
var _ = function () {
/* кэшируем вызов для document */
  var doc = document,
/* получаем сам селектор в качестве первого аргумента */
    selector = arguments[0],
/* и возможность использования кэша во втором */
    nocache = arguments[1];
/* возвращаем элемент из кэша, если получается*/
  if (_.cache[selector] && !nocache) {
    return _.cache[selector];
  } else {
/* пробуем применить querySelectorAll, работает сейчас только в Safari */
    if (doc['querySelectorAll']) {
      _.nodes = doc['querySelectorAll'](selector);
    } else {
      switch (selector) {
/* обрабатываем несколько элементарных случаев. Например, body */
        case 'body':
          _.nodes = doc.body;
          break;
/* или head для страницы */
        case 'head':
          _.nodes = doc['getElementsByTagName']('head')[0];
          break;
        case 'a':
          _.nodes = doc.links ? doc.links : doc['getElementsByTagName']('a');
          break;
        case 'img':
          _.nodes = doc.images ? doc.images : doc['getElementsByTagName']('img');
          break;
/* самый общий случай, можно еще ускорить */
        default:
/* по отдельности рассматриваем селекторы, разделенные запятыми */
          var groups = selector.replace(/, +/g,",").split(","),
            groups_length = groups.length - 1;
            j = -1;
/* везде используем while, а не for -- так быстрее */
          while (j++ < groups_length) {
/* каждый такой селектор разбиваем на группы, которые содержат узлы тэг-id-класс */
            var singles = groups[j].split(' '),
              singles_length = singles.length - 1,
              i = -1,
              level = 0;
/* в качестве начала поиска нужного узла устанавливаем сам документ */
            _.nodes = doc;
            while (i++ < singles_length) {
/* применяем быстрый разбор регулярного выражения с применением параметров
(тэг-id-класс), подробнее:
webo.in/articles/habrahabr/40-search-not-replace/
*/
              singles[i].replace(/([^\.#]+)?(#([^\.#]+))?(\.([^\.#]+))?/, function(a, tag, b, id, c, klass) {
/* проверяем, задан ли только id (и при этом еще не спустились дальше корня документа) */
                if (tag == '' && klass == '' && !level) {
                  _.nodes = doc[/*@cc_on !@*/0 ? 'all' : 'getElementById'](id);
                } else {
/* теперь проверяем, задан ли только тэг */
                  if (klass == '' && id == '' && !level) {
                    _.nodes = doc['getElementsByTagName'](tag);
/* а потом уже запускаем общий механизм отбора узлов по тэгу, id и классу */
                  } else {
/* массив, в который будет складывать результаты */
                    var newNodes = {},
/* кэшируем текущий размер массива */
                      nodes_length = _.nodes.length,
                      J = -1,
/* кэшируем итератор, в который будем складывать новый размер массива */
                      idx = 0;
/* если в качестве корневого элемента передан 1 элемент, то делаем из него массив
для перебора */
                    if (!nodes_length) {
                      _.nodes = [_.nodes[0]?_.nodes[0]:_.nodes];
                      nodes_length = 1;
                    }
/* опять while, а не for-in или for */
                    while (J++ < nodes_length) {
                      var node = _.nodes[J];
                      if (node) {
/* находим все тэги */
                        var childs = node['getElementsByTagName'](tag ? tag : '*'),
                          childs_length = childs.length,
                          h = -1;
                        while (h++ < childs_length) {
                          var child = childs[h];
/* проверяем их на соответствие заданным id или классу */
                          if (child && (!id || (id && child.id == id)) && (!klass || (klass && child.className.match(klass)))) {
/* и только после этого записываем в результирующий массив */
                            newNodes[idx++] = child;
                          }
                        }
                      }
                    }
/* теперь записываем в текущие узлы все выбранные на данном этапе */
                    _.nodes = newNodes;
          _.nodes.length = idx - 1;
                  }
                }
/* "грязный" хак для обозначения "корня" для текущей выборки --
можем ли использовать "быстрые" выборки по тегу или id или нужен общий подход */
                level++;
              });
/* теперь вбрасываем все узлы от одного селектора в боле глобальный массив --
на тот случай, если у нас задано несколько селекторов через запятую */
              if (groups_length) {
                var nodes_length = _.nodes.length,
                  K = -1,
                  idx = _.sets ? _.sets.length : 0;
        _.sets = _.sets ? _.sets : {};
                while (K++ < nodes_length) {
                  _.sets[idx++] = _.nodes[K];
                }
                _.sets.length = idx;
/* иначе просто копируем текущую выборку элементов в более глобальный массив */
              } else {
                _.sets = _.nodes;
              }
            }
          }
          break;      
      }
    }
/* проверяем, есть ли у нас глобальная выборка. Если нет, то копируем локальную в нее
немного дублирует чуть выше написанный код. Надо отрефакторить */
  _.sets = _.sets ? _.sets : _.nodes;
/* сохраняем результат в кэше, следим за тем, чтобы сохранилась ссылка на элемент */
    _.cache[selector] = _.sets.length>1||_.sets.nodeName ? _.sets : _.sets[0];
/* очищаем все использованные переменные для предотвращения утечек через ссылки на DOM
-- еще нужно проверить, если ли в IE утечки при этом */
  _.sets = _.nodes = null;
/* ну и возвращаем полученный результат */
    return _.cache[selector];
  }
};
/* текущий набор узлов */
_.nodes = null;
/* более глобальный набор узлов, соединяющий несколько селекторов, разделенных запятой */
_.sets = null;
/* кэш для выбранных узлов */
_.cache = {};
/* а вот только теперь записываем как единственную глобальную переменную: window._ */
window._ = _;
})();


Примеры вызовов


Все до безобразия просто:
_('p') — вернет все параграфы на странице
_('p a') — или все ссылки в них
_('p a.blog') — или все ссылки с классом blog
и т.д. Полная CSS1 гамма.

Еще один велосипед?


Может быть, кому-то данный случай покажется тривиальным, но мне лично дальнейшее развитие ситуации видится следующим образом. Либо код можно использовать в образовательных целях, что понять логику конечных автоматов, используемых браузером для распознавания CSS-селекторов (во встроенном движке), и промоделировать некоторые тривиальные или не очень ситуации.

Или же данный код можно будет доработать и включать в основу высокопроизводительных библиотек, которые уже будут на него наворачивать свои методы. Как легко видеть, код замечательно расширяемый или даже модульный. Все, что ему требуется, — это браузер :)

В качестве заключения


В ближайшее время я планирую провести ряд исследований, какова производительность различных методов и подходов в JavaScript — ведь именно за ним будущее (веб-) приложений. И CSS-селекторы пока (и еще долгое время будут, до внедрения в IE querySelectorAll) являются одним из «китов» построения любого приложения. И чем шустрее этот кит получается, тем быстрее работает все остальное.

Текущая версия YASS (читается как yeasss! :) — 0.1. Развиваться будет. Текущий размер кода: около 6 Кб со всеми комментариями, 1,5 — минимизированная версия и 650 байтов — архив с ней. Вдумайтесь: 650 байт пожатого кода, чтобы выбрать любой элемент на странице с помощью CSS1 селекторов. И выбрать почти что не медленнее (разница составляет -10… +30% от нативных методов — за счет кэширования), чем с помощью прямых методов.

Тестирование грядет. Пока что текущая реализация быстрее чем Peppy в полтора-два раза, который и так всех обгоняет.

P.S. В общем, как и ожидалось, yass сильно быстрее текущих библиотек. Только вот тесты проваливает :)
Спасибо lusever за тестовое окружение. Результаты можно посмотреть здесь
+57
3 декабря 2008, 23:50
49

комментарии (72)

+6
DisDis #
Скорее людям нужны возможности фреймворка, чем быстрота. Поэтому Peppy и подобные будут мало востребованы, так как мало плагинов, а значит и быстрого наращивания функциональности. Мне кажется авторам, которые ведут исследования в областях ускорения выбора элементов и т.д. лучше направить свои усилия на ускорение распространенных фреймворков(Prototype, jQuery и т.д.).
+1
sunnybear #
может быть. Я вот лично склоняюсь к тому мнению, что на рынке должно быть разнообразие: хочется готовый фремворк — используй jQuery, YUI, Prototype или Mootools. Хочешь чего-то более базового и быстрого (например, для крупного портала или внутристудийных разработок) — собери систему «по кирпичикам». Чтобы это не развалилось, когда захудалый склад станет небоскребом (видел пару таких примеров, где спасет полный снос текущей клиентской архитектуры).
НЛО прилетело и опубликовало эту надпись здесь
+1
RomanL #
НЕ согласен. У меня дома (1,6ГГц, IE6) хабр (да и многие другие проекты на jQuery) сильно притормаживает при открытии, а тут, глядишь, пошутсрее могли бы бегать :)
НЛО прилетело и опубликовало эту надпись здесь
0
sunnybear #
да, спасибо, есть еще пара мест для оптимизации — типа все, что можно, закэшировать.
И по поводу регулярки еще не смотрел, какая реализация быстрее. Да еще и в каких браузерах :)
НЛО прилетело и опубликовало эту надпись здесь
0
sunnybear #
Не будет: программисту легче указать, какой запрос делать без кэширования (точнее, с его сбросом — чтобы пересчитать динамику), чем пересчитывать кэш на каждый чих…

По поводу ajax — это же мини-ядро :) Сюда еще методы изменения узлов нужны, ajax, события, таймеры, массивы и проч…
+5
tenshi #
ну ты традиционно ускоряешь не в том месте =)

1. split может принимать на вход регулярку.
2. как быть в случае, если нужно выбрать элемент из поддерева?
3. как выбрать, например, все чекбоксы?
4. код не расширяем, не обманывай себя. css2 сюда фиг добавишь
5. если пользователю потребуется закэшировать — он просто создаст локальную переменную — это на порядок быстрее любых ухищрений с селекторами и удобнее их копипаста.
6. почему нет кэширования откомпилированной версии селектора? ах, у тебя же нет компиляции… а в base2 — есть.
0
sunnybear #
1. Спасибо. Да, тут надо проверить, что быстрее — split c регуляркой или replace с функцией.
2. Принимать поддерево в качество параметра можно. Хорошее замечание.
3. CSS2 — да, стоит добавить (выбор по атрибутам и проч.)
4. В данный момент не расширяем (будет точно сыпаться на > ). Но все можно переписать с нуля :)
5. Каждый раз кэшировать? Не проще ли выбирать элемент и не думать о том, закэширован он уже где-то или нет? Кэширование будет обламываться только в случае изменения дерева. Если у нас есть ключевые узлы — да, мы можем их закэшировать. А можем и просто выбирать
6. Что такое «откомпилированная версия»? Чем она отличается от строки и набора узлов?

А насчет «не того места» :) Выложу, наверное, в скором будущем анализ крупной системы на Mootools — там полетела именно сборка мусора и выбор элементов. И терялось на этом все 2-3 секунды в IE при открытии каждой страницы
0
tenshi #
6. тем, что «откомпилированный селектор» — это функция, максимально эффективно реализующая поиск элементов по заданному селектору.

7. конечно, код написанный через задницу проще оптимизировать в одном месте, чем переписывать во многих =) но если код пишется по уму — от «оптимизаций» в селекторах только вред.
0
sunnybear #
хмм, наверное, дерево выбора самой быстрой функции для заданного селектора и есть его «компиляция». Просто с понятием не сталкивался (в частности, если #id задан — то сразу берется document.getElementById, и ничего дальше не предпринимается). Я на это и ориентируюсь. Просто сейчас не самая оптимальная реализация предложена.

7. Как показывает практика, обычно «пишется по уму» другой код, который основан на этих самых селекторах. А когда оказывается, что тормозит ядро, а чтобы его переписать, надо выкинуть 100Кб чужого кода… No comments :)
0
tenshi #
у тебя парсинг селектора происходит при каждом запросе (кроме случаев попадания в кэш). в то время как результат этого парсинга можно закэшировать намертво.

7. это всё от мании реализовывать именно css селекторы. получается медленно и громоздко. у меня где-то валялась реализация не-цсс-селекторов, ориентированная именно на яваскрипт, с компиляцией, модульностью маленьким размером и тп… могу выложить, если интересно.
0
voicer #
выложи, интересно.
0
sunnybear #
а, имеется в виду — промежуточный кэш для каждой части селектора? Это интересно.

выложи, плиз.
+2
tenshi #
pastebin.mozilla-russia.org/93256

функция jpath принимает на вход селектор и возвращает функцию, которая по переданному массиву ( по умолчанию — [document] ) строит новый массив.

там реализованы следующие субселекторы:
* свойство каждого объекта по имени: jpath(' .nodeName ')( document.images )
* элементы по идентификатору из каждого поддерева в массиве: jpath(' #anyId ')([ top, self ])
* элементы-потомки по имени тэга: jpath(' %p ')()
* дочерние элементы с опциональной фильтрацией по имени узла: jpath(' /td ')()
* фильтрация списка по имени класса: jpath(' :favorited ')( elements )
* фильтрация списка по jpath-селектору,
* фильтрация списка по значению: jpath(' [ .checked =«true» ] ')( checkboxes )
0
sunnybear #
а тесты производительности какие-нибудь есть? А то ведь можно исходный селекторо перевести в jpath…
0
tenshi #
нет, конечно =) меня вопросы производительности слабо волнуют. гибкость — превыше всего! ;-)
0
voicer #
Интересное начинание.
Лично мне, правда, идея сильно напоминает base2, но, тем не менее, автор прав — от конкуренции и обилия инструментов все только выиграют. Да и мини-ядро в больших приложениях также лучше фреймворка.
0
voicer #
Да, забыл добавить — успехов в новом проекте :)
+2
ofigenn #
Хотелось бы увидеть долю «оптимизированного» времени в жизненном цикле запроса. Проще говоря, сколько выигрываем вообще, начиная от нажатия на ссылку?
0
sunnybear #
боюсь, время на запрос будет зависеть от специфики библиотеки. Т.е. самое быстрое сейчас — это концепция. Все остальное — просто примочки, которые ее чуть-чуть тюнят.
0
ofigenn #
Это я к тому, что ты написал: «В ближайшее время я планирую провести ряд исследований...»! Если будет интерес, посчитай и этот параметр. Для общей оценки он важен, я думаю.
0
IIIEB4YK #
+2
IIIEB4YK #
Забыл описание для ленивых:
«This is a new pure-JavaScript CSS selector engine that I'm working on.

Comes in at roughly 4x faster in Firefox 3, 3x faster in Opera 9, 1.5x faster in Safari 3 than the other major
JavaScript libraries. It's completely standalone (no library dependencies) and clocks in at 4KB.

Currently this engine is expected to become the new default selector engine of jQuery, MochiKit, Prototype, and Dojo»
0
sunnybear #
Peppy быстрее, чем Sizzle
jamesdonaghue.com/?p=40
0
XaocCPS #
возник вопрос, что предпочтительнее выигрыш в скорости на 10-20% или возможность использовать CSS2- и CSS3-селекторы?
лично для меня предпочтительнее второе, поэтому я жду обновления YASS, которое бы реализовало такую поддержку :)
0
the_buddha #
Ты прав брат, но imho многие ждут обратного, в плане что бы уже существующие мйнстримовые фреймворки ускорялись)) а я так вообще молюсь что окгда ибудь вс ебраузеры будут поддерживать нативные css3 селекторы
0
MTonly #
(в идеале, даже быстрее), чем нативные вызовы
Почему-то, когда вижу слово «нативный», желание читать пропадает. ;-)
0
the_buddha #
а мне просто удвительно, как может быть быстрее чем нативно))
0
sunnybear #
может быть :) причем в наиболее быстрых браузерах — Safari, Opera, Chrome: именно в них элементарные операции «отъедают» меньше, а DOM сильно не пооптимизируешь :)
0
MTonly #
Для непонявших: переводить native как «нативный» — всё равно что вместо «книжный магазин» говорить что-нибудь типа «бучный шоп».
0
tenshi #
а как правильно переводить?
0
MTonly #
Родной, встроенный; зависит от контекста.
0
tenshi #
«родные вызовы», «встроенные вызовы»

фигня-с
0
MTonly #
Вызов встроенного метода.
0
tenshi #
встроенного во что?
0
MTonly #
В JavaScript-интерпретатор браузера, куда ж ещё. ;-)
0
tenshi #
нет, интерпретатор ничего такого не умеет. умеет это отдельная библиотека, реализующая ДОМ. но «метод встроен в библиотеку» — так тоже не говорят.
0
sunnybear #
:) пока тут продолжается дискуссия, библиотеку удалось ускорить еще в два раза :) в ночь выложу версию 0.1.6
+1
MTonly #
Энтузиазм при наличии свободного времени — это замечательно. ;-)
+1
sunnybear #
особенно, без наличия оного :)
0
MTonly #
Со временем это проходит. ;-)
0
sunnybear #
там сложная функция, линейная :)
0
MTonly #
Хорошо, движок JavaScript. ;-)
В любом случае нативный — туда же, куда и лочить вместо блокировать.
0
tenshi #
меня пугают слова длиннее 7 букв…
0
MTonly #
Волков бояться — в лес не ходить. ;-)
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
0
tenshi #
есть такая, 100кб весом.
0
bolk #
Посмотрите вот этот документ: code.msdn.microsoft.com/ie8whitepapers/Release/ProjectReleases.aspx?ReleaseId=574
думаю, он может быть полезен для оптимизации.
0
sunnybear #
спасибо. Там речь идет о querySelectorAll — он уже добавлен в самое начало. Safari 3 уже поддерживает
0
bolk #
IE8b тоже уже поддерживает. Я подробно ваш код не смотрел, глянул краем глаза, поэтому не видел, что вы уже поддерживаете Selector API.
+1
z_z #
Хохо, мерицо скоростью CSS1 vs CSS3+XPath — круто :)
0
deerua #
Спасибо за бессонные ночи ;) То что нужно!
Есть просто проекты, на которых и вовсе не нужен никакой джфреймворк, а вот такого yass не хватает ;)
0
VasilioRuzanni #
Кстати, а что думаете по поводу Sizzle?
0
arty #
doc['querySelectorAll'] проще записывается как doc.querySelectorAll, а разницы никакой, то же самое и для других методов

очень не рекомендую использовать подчёркивание в глобальном объекте, у доллара вначале тоже не было конфликтов
0
sunnybear #
а быстрее ли? предварительные тесты показали, что так чуть быстрее. Руки не доходят каждую фичу в отдельности тестить :)
0
arty #
ммм? я про скорость не говорил: )
0
sunnybear #
а эта библиотечка только ради скорости. По SlickSpeed делает все, что можно, раза в два :)
0
arty #
погоди, ты что ли имеешь в виду, что a['b'] быстрее, чем a.b?
0
sunnybear #
по моему, немного быстрее
0
arty #
а по моим тестам — наоборот. На миллионе прогонов:
84 ms — a.b
134 ms — a['b']
0
sunnybear #
ну, вот и замечательно. Значит, исправлю обратно )
оказалось, что есть еще метод getElementsByClassName :)
Вдобавок не понятно, кто не прав в тестах — я или остальные библиотеки
yass.webo.in/slickspeed/?base2_1-0b2/Peppy_0-1/DOMAssistant_2-7-4/yass_0-1-3

По CSS1-тестам получается в 4 раза быстрее той же Peppy
0
arty #
про неправ я не понял

кстати, в тестах с id-селекторами в опере ошибки — именно с yass
0
sunnybear #
спасибо, посмотрю Opera чуть позже: тяжело несколько веток сразу вести, а потом сливать наиболее продуктивные
0
tenshi #
до округления:
а) .49 .49 .49 .49
б) .51 .51 .51 .51
а быстрее б на 4%

после округления:
а) 0 0 0 0
б) 1 1 1 1
а быстрее б на бесконечное число процентов

0
sunnybear #
да, это понятно. Поэтому тут даже не разы интереснее, а сам факт быстроты — его уж не скроешь никаким округлением. А большое число тестов позволяет свести статистическую ошибку к минимуму.

Про 0-0-0-0 я вообще молчу. Тут даже сравнивать не с чем — просто быстрее, и все :)
0
arty #
и так во всех браузерах
+1
f33l #
интересно: mootools.net/blog/2008/12/04/sizzle/

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