XPath performance benchmark

Most XPath performance advice is folklore. This page measures it: pick a comparison — //item against /catalog/items/item, a cheap predicate before an expensive one, contains() against equality — choose a document of 500, 2,000 or 10,000 records, and run it. Both expressions run in alternating rounds against the same parsed document, and the match counts are checked so a "faster" query that quietly selects something else is flagged rather than celebrated.

Example

XPath on the Recipes sample:

/catalog/items/item[@active = 'true'][price > 900]

The shape the benchmark favours: an anchored path rather than //, and the cheap attribute test placed before the numeric comparison so fewer nodes reach it.

Open it in the Sandbox

What actually costs you time

Across every run, the same three effects dominate, and they dwarf the micro-optimisations people argue about.

  • **Parsing, not querying.** Building the DOM for a large document usually costs more than any single expression. If your code re-parses per query, that is the whole problem — nothing you do to the expression will matter. The benchmark reports the parse time separately for exactly this reason.
  • **// versus an anchored path.** //item asks the engine to consider every node in the document; /catalog/items/item visits three levels. The gap widens with document size, not with expression complexity.
  • **Predicate order.** Predicates are applied left to right, and each one only sees what survived the previous one. Putting the selective, cheap test first ([@active = 'true'] before [price > 900]) means the expensive test runs on a fraction of the nodes.

How the measurement works

The document is generated deterministically and parsed once, so parse cost is not charged to either expression. Each expression is probed once to size the run, then the two alternate across five rounds — alternating matters, because a garbage-collection pause that lands entirely inside one contestant's block would otherwise decide the result.

The reported number is the median of the per-round averages, not the mean, so one outlier round cannot move it. A winner is only declared when the gap exceeds 15%; below that the tool says the two are within noise, which is more honest than a spurious 1.02x.

Only node counts are computed — the results are never materialised into a list or serialised — so what you are comparing is evaluation cost, not rendering cost.

How to read the numbers honestly

These figures describe one engine (fontoxpath for 3.1, the browser's native document.evaluate for 1.0) in one browser on your machine, against a synthetic document. They are useful for ratios and for confirming the direction of an effect. They are not a benchmark of Saxon, libxml2, lxml, or Selenium's XPath, all of which optimise differently — libxml2 in particular has index-aware shortcuts that change the // story.

The ratio between two expressions on the same run is the durable finding. The absolute milliseconds are not: they will differ on another machine, and the 1.0 and 3.1 columns are two different engines rather than two versions of one.

That difference is worth stating plainly, because running the comparisons makes it obvious. XPath 1.0 here is the browser's own document.evaluate, written in C++ and measured in microseconds. XPath 3.1 is fontoxpath, a JavaScript engine, and its cost climbs steeply with the size of the result sequence — a query returning a few thousand nodes takes seconds, not microseconds. That is why the largest document is offered for 1.0 only, and why you should use the 3.1 engine to compare 3.1 constructs against each other rather than to judge whether "XPath is fast".

Rules of thumb worth keeping

If you take four things away from running the comparisons above:

  • Parse once, query many times. Cache the parsed document.
  • Anchor the path when you know where the data lives; save // for when you genuinely don't.
  • Put the cheapest, most selective predicate first.
  • Name the element you mean instead of sweeping //text() or //* and filtering afterwards.

Keep going