From 45d35d506a409ad838c78aae9892dfe712ec8c05 Mon Sep 17 00:00:00 2001 From: jvoisin Date: Sat, 23 May 2026 21:55:51 +0200 Subject: [PATCH] perf: avoid per-node Selection allocation in winnow Not path The winnow() "Not" path routed through grep(), which called newSingleSelection() for every node, allocating a Selection and a 1-element []*html.Node slice on the heap per node, only to immediately unwrap it via s.Get(0) for m.Match(). This commit call m.Match(n) directly and pre-allocate the result slice. It also adds BenchmarkNotMatcher to isolate the winnow path from compileMatcher overhead. Here are my local benchmark results, for BenchmarkNotMatcher with 373
  • nodes, and a pre-compiled matcher: old ns/op new ns/op delta 22750 19350 ~-15% old B/op new B/op delta 9384 3120 -66.7% old allocs new allocs delta 11 2 -81.8% --- bench_filter_test.go | 21 +++++++++++++++++++++ filter.go | 12 ++++++++---- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/bench_filter_test.go b/bench_filter_test.go index 38e39f5..44e5917 100644 --- a/bench_filter_test.go +++ b/bench_filter_test.go @@ -2,6 +2,8 @@ package goquery import ( "testing" + + "github.com/andybalholm/cascadia" ) func BenchmarkFilter(b *testing.B) { @@ -40,6 +42,25 @@ func BenchmarkNot(b *testing.B) { } } +func BenchmarkNotMatcher(b *testing.B) { + var n int + + b.StopTimer() + sel := DocW().Find("li") + m := cascadia.MustCompile(".toclevel-2") + b.StartTimer() + for i := 0; i < b.N; i++ { + if n == 0 { + n = sel.NotMatcher(m).Length() + } else { + sel.NotMatcher(m) + } + } + if n != 371 { + b.Fatalf("want 371, got %d", n) + } +} + func BenchmarkFilterFunction(b *testing.B) { var n int diff --git a/filter.go b/filter.go index 9138ffb..271f050 100644 --- a/filter.go +++ b/filter.go @@ -130,10 +130,14 @@ func winnow(sel *Selection, m Matcher, keep bool) []*html.Node { if keep { return m.Filter(sel.Nodes) } - // Use grep - return grep(sel, func(i int, s *Selection) bool { - return !m.Match(s.Get(0)) - }) + // Not path: call Match directly on each node, no Selection wrapper needed + result := make([]*html.Node, 0, len(sel.Nodes)) + for _, n := range sel.Nodes { + if !m.Match(n) { + result = append(result, n) + } + } + return result } // Filter based on an array of nodes, and the indicator to keep (Filter) or