From 8ca87a1f4a6d7ff3b4d49d290c57398eef89f506 Mon Sep 17 00:00:00 2001 From: jvoisin Date: Mon, 1 Jun 2026 00:23:42 +0200 Subject: [PATCH] perf: presize result slice in getChildrenWithSiblingType The collect-all sibling traversals (Siblings, NextAll, PrevAll, Contents and their filtered variants) grew the result slice via append with no capacity hint, paying repeated grow-and-double reallocations. This commit runs the existing iterator once to count the matching siblings, then presize the result slice before collecting. The Until cases are left alone, as their predicate would run twice, and the single-result Next/Prev cases need no allocation. benchstat (count=10): allocs/op Siblings -57%, SiblingsFiltered -54%, NextAll -60%, NextAllFiltered -56%, PrevAll -55%, PrevAllFiltered -53%, Contents -10%, ChildrenFiltered -17% (geomean -35%) B/op geomean -6% (-9% to -11% on the All paths) sec/op PrevAll -44%, Next -29%, Contents -27% (geomean -20%) --- traversal.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/traversal.go b/traversal.go index c495eea..09b0b66 100644 --- a/traversal.go +++ b/traversal.go @@ -657,6 +657,20 @@ func getChildrenWithSiblingType(parent *html.Node, st siblingType, skipNode *htm } } + // For the collect-all cases, count the results first so result can be + // presized, avoiding append's repeated reallocations. + switch st { + case siblingAll, siblingAllIncludingNonElements, siblingPrevAll, siblingNextAll: + n := 0 + for c := iter(nil); c != nil; c = iter(c) { + n++ + } + if n == 0 { + return nil + } + result = make([]*html.Node, 0, n) + } + for c := iter(nil); c != nil; c = iter(c) { // If this is an ...Until case, test before append (returns true // if the until condition is reached)