XPath performance: cost, indexes and //

Descendant search is the expensive part of XPath. Knowing which constructs force a full scan tells you where to spend your optimisation effort.

What actually costs time

ConstructWork doneCost on a large document
/a/b/cOne child step per levelLow, proportional to depth
//xVisit every descendant of the contextHigh, proportional to document size
//x[@id='1']Full scan, then filterHigh — no early exit
id('1')Index lookupConstant, when the parser built an index
//x[1]Scan, keep the first per parentHigh, and often not what you meant
count(//x)Scan and countHigh, use a streaming reader instead
//a/following-sibling::aQuadratic in sibling countVery high, avoid
# Measure rather than guess
time xmllint --xpath 'count(//item)' big.xml

# Narrow the context node so // has less to scan
time xmllint --xpath 'count(//feed/item)' big.xml

# A path that never leaves the top level is the cheapest form
time xmllint --xpath 'count(/*/*/item)' big.xml
  • (//x)[1] scans everything then takes one node; //x[1] takes the first matching x child of every parent. Neither is a shortcut for the other.
  • Every additional // in one expression multiplies the traversal.
  • Repeating the same expression instead of storing the result in a variable is the most common waste in XSLT.

Making a query fast

<!-- Keys give you a hash index you control -->
<xsl:key name="byId" match="item" use="@id"/>

<xsl:template match="/">
  <!-- one key call per lookup, instead of scanning every item -->
  <xsl:value-of select="key('byId', '42')/title"/>
</xsl:template>
from lxml import etree
import time

doc = etree.parse("big.xml")
root = doc.getroot()

def timed(label, fn):
    t = time.perf_counter()
    n = len(fn())
    print(label, n, "in", round((time.perf_counter() - t) * 1000), "ms")

# Full descendant scan on every call
timed("//item    ", lambda: doc.xpath("//item"))
# Restrict the context node so the scan is smaller
timed("//b/item  ", lambda: doc.xpath("//b/item"))
# Direct children of the root element
timed("/*/b/item", lambda: doc.xpath("/*/b/item"))
💡
Optimise the query shape before the engine. A single // removed from a hot expression usually beats any library-level tuning, and the change is measurable with a five-line timing harness like the one above.

FAQ

Is id() really constant time?
When the parser recorded ID attributes, which requires a DTD or schema declaration. Without one, id() behaves like any other scan, so wrap your real lookup key in xsl:key or a dictionary instead.
Should I avoid // altogether?
No. Use it for one-off queries and during development. Give it a narrow context node and avoid it inside loops, which is where it becomes quadratic.

XPath from the shell: xmllint and xmlstarlet The XPath data model: nodes, order and values

Last refreshed 2026-09-18.