Frontend Development 10 min read

Unveiling Sizzle.js: How jQuery’s Fast Selector Engine Works

This article dissects the inner workings of jQuery’s Sizzle selector engine, explaining its tokenization, compilation, matcher generation, and performance optimizations, while offering practical tips for writing faster CSS selectors in modern web development.

Tencent IMWeb Frontend Team
Tencent IMWeb Frontend Team
Tencent IMWeb Frontend Team
Unveiling Sizzle.js: How jQuery’s Fast Selector Engine Works

Remember the once‑famous jQuery library and its claim of having the industry’s fastest DOM selector, Sizzle? By analyzing Sizzle.js source code we can understand the clever design that made it so quick, offering insights useful for modern framework design.

Now we move to the select function and its overall flow

1. Perform lexical analysis to obtain a token list.

2. If a seed set exists, jump straight to the compilation stage.

3. If no seed set and the selector is a single group (no commas), first try to narrow the context: when the first token is an ID selector,

Expr.find["ID"]

finds the context and removes the ID token.

4. Then attempt to find a seed set by scanning tokens from right to left; encountering a combinator (>, +, ~, space) stops the scan, otherwise

Expr.find

methods try to locate matching DOM elements, which become the seed set.

5. Enter the compilation phase, where the goal is to reduce the query range as much as possible before actual matching.

The

Expr.find

object maps selector types to native DOM methods:

<code>{
  "ID": context.getElementById,
  "CLASS": context.getElementsByClassName,
  "NAME": context.getElementsByName,
  "TAG": context.getElementsByTagName
}</code>

Each entry returns a function that validates whether a given element matches the specified type. For example, the CLASS finder receives a class name and a context, returning the matching elements as the seed set.

After obtaining the token list, context, and seed set, Sizzle compiles a nested function from all tokens. This “compile‑once‑execute‑many” approach uses closures so that repeated identical selectors can reuse the compiled function, dramatically improving performance.

The compilation creates two kinds of matcher functions:

1. For combinators (relationship selectors), it generates a function that combines the current selector with the previous one.

2. For non‑combinators, it directly checks whether each seed satisfies the selector.

For example, the selector

div > a

yields two functions: one testing if the parent is a

div

, another testing if the element itself is an

a

. The combined matcher is applied to each seed.

The overall compilation steps are:

Check the cache for a previously compiled selector.

If not tokenized yet, run the tokenizer.

For each group, call

matcherFromTokens

to obtain a matcher; pseudo‑classes go into

setMatchers

, others into

elementMatchers

.

Finally, combine

setMatchers

and

elementMatchers

via

matcherFromGroupMatchers

.

matcherFromTokens

converts a token array into a match function, handling both non‑combinator and combinator tokens. Non‑combinator tokens push their filter functions directly into a

matchers

array, while combinator tokens combine with the previous selector’s filter.

The

elementMatcher

utility turns an array of filter functions into a single function that runs each filter sequentially.

The

Expr.filter

object contains specific filter functions for different selector types such as ID, CLASS, TAG, ATTR, CHILD, and PSEUDO. Each returns a function that checks a DOM element against the corresponding criterion.

<code>Expr.filter["ID"] = function(id) {
  return function(elem) { return elem.id === id; };
};

Expr.filter["TAG"] = function(tag) {
  return function(elem) { return elem.nodeName.toLowerCase() === tag; };
};</code>

In summary, the Sizzle engine processes a selector by tokenizing it, narrowing the context and seed set, compiling a series of matcher functions, and finally filtering the seed elements through these matchers. The following optimization tips arise from this design:

Start selectors with an ID to quickly narrow the root node.

Place tag names before class names because

getElementsByTagName

is fast.

Avoid the universal selector

*

; specify a concrete seed element.

Prefer parent‑child (>) combinators over descendant (space) selectors to reduce search scope.

Cache compiled jQuery objects to trade space for time.

performanceJavaScriptjQuerySizzleDOM selector
Tencent IMWeb Frontend Team
Written by

Tencent IMWeb Frontend Team

IMWeb Frontend Community gathering frontend development enthusiasts. Follow us for refined live courses by top experts, cutting‑edge technical posts, and to sharpen your frontend skills.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.