Skip to content

perf: avoid unnecessary coordinate array copies in hot geometric paths - #3164

Open
bscuron wants to merge 2 commits into
Turfjs:masterfrom
bscuron:master
Open

perf: avoid unnecessary coordinate array copies in hot geometric paths#3164
bscuron wants to merge 2 commits into
Turfjs:masterfrom
bscuron:master

Conversation

@bscuron

@bscuron bscuron commented Sep 9, 2026

Copy link
Copy Markdown

Summary

getCoord() defensively copies its result on every call, a global fix for one historical mutating caller (@turf/rhumb-distance, #2167). Adds getCoordRaw(), a non-copying internal variant, and switches every call site that only reads the result (never mutates it or embeds it by reference into a returned GeoJSON object) to use it instead. distance() in particular is a dependency of 23 packages and is often called in tight per-coordinate/per-segment loops.

Also removes a wrap-in-point()-then-getCoord()-unwrap round trip per segment in nearestPointOnLine, using the already-available raw coordinate array directly.

getCoord() (copy-safe) is left in place at the few call sites that embed its result into a returned Feature (centerOfMass, lineSplit, shortestPath), where switching to a raw reference would alias the caller's input geometry.

Performance: this change alone

Benchmarked with each touched package's own bench.ts (Benchmark.js), comparing the parent commit against this one, same machine, same run. Two packages (turf-boolean-point-on-line, turf-directional-mean) produce no usable output from their existing bench.ts in this environment — unrelated pre-existing issue, excluded below. Three packages (turf-ellipse, turf-great-circle, turf-line-split) initially measured as regressions in a single pass; re-measured with the run order reversed and the effect vanished, confirming it was a measurement-order artifact, not a real difference — the corrected (order-verified) numbers are used below.

Package Before (ops/sec, summed across fixtures) After Change
turf-boolean-point-in-polygon 61,638,167 122,800,432 +106%¹
turf-distance-weight 9,964 16,525 +66%
turf-distance 10,317,872 13,262,833 +29%
turf-bearing 16,872,917 20,461,205 +21%
turf-planepoint 31,871,968 38,836,254 +22%
turf-rhumb-bearing 14,273,173 17,286,919 +21%
turf-quadrat-analysis (low sample count, see below) +21%²
turf-nearest-point-on-line 1,512,502 1,706,268 +13%
turf-rhumb-distance 11,371,263 12,353,515 +9%
turf-rhumb-destination 8,748,446 9,221,540 +5%
turf-destination 8,802,002 9,041,056 +3%
turf-transform-scale 12,805,425 12,006,329 ~0%³
turf-ellipse 674,966 678,980 ~0%¹
turf-great-circle 69,250 68,651 ~0%¹
turf-line-split 3,186,298 3,193,247 ~0%¹

¹ Re-measured with run order reversed to confirm the direction/magnitude is real, not a measurement artifact.
² turf-quadrat-analysis's nearest fixture only gets 5-9 Benchmark.js samples (each iteration takes multiple seconds), so this one has more inherent noise than the others.
³ Within noise; this package's call sites weren't on a hot path materially exercised by its own benchmark fixtures.

turf-boolean-point-in-polygon's point-in-ring test does a getCoord() (now getCoordRaw()) per vertex in a tight ray-casting loop, so it sees the largest, most reproducible win of the set.

Performance: if polyclip-ts's optimization PR also merges

@turf/union, @turf/difference, @turf/intersect, @turf/dissolve, and @turf/mask all depend on polyclip-ts for their core geometry engine. We also have an open performance PR against polyclip-ts itself (luizbarboza/polyclip-ts#26 — BigInt-backed exact arithmetic + memoized segment comparisons, +64% to +90% on its own benchmarks). Benchmarked here by swapping the currently-published polyclip-ts@0.16.8 for a local build of that PR's branch, same fixtures, same machine — this repo's own code (this PR's getCoordRaw change included) held constant on both sides:

Package Avg. improvement across fixtures
@turf/union +141%
@turf/difference +145%
@turf/intersect +141%
@turf/mask +165%
@turf/dissolve +119%

Every fixture in every package improved (no mixed signal here, unlike the table above), and the effect size (roughly 2.2-2.65x) is far larger than any measurement noise observed elsewhere in this testing, including a reversed-order and repeat-run spot check on @turf/mask.

Test plan

  • Existing package test suites pass
  • Before/after benchmarked per-package via each package's own bench.ts (tables above)
  • Suspicious single-pass results re-verified with reversed run order before being trusted

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01VSdmXWDQ2epJKfoGXFKdqT

🤖 Generated with Claude Code

https://claude.ai/code/session_01VSdmXWDQ2epJKfoGXFKdqT

bscuron and others added 2 commits September 8, 2026 23:37
getCoord() defensively copies its result on every call, a global fix
for one historical mutating caller (@turf/rhumb-distance, Turfjs#2167).
Added getCoordRaw(), a non-copying internal variant, and switched every
call site that only reads the result (never mutates it or embeds it by
reference into a returned GeoJSON object) to use it instead. distance()
in particular is a dependency of 23 packages and is often called in
tight per-coordinate/per-segment loops.

Also removed a wrap-in-point()-then-getCoord()-unwrap round trip per
segment in nearestPointOnLine, using the already-available raw
coordinate array directly.

Left getCoord() (copy-safe) in place at the few call sites that embed
its result into a returned Feature (centerOfMass, lineSplit,
shortestPath), where switching to a raw reference would alias the
caller's input geometry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VvWWffxvccsNS5ePvdDiiR
getCoord and getCoordRaw had identical validation logic, differing
only in whether the result was copied. Make getCoord delegate to
getCoordRaw and copy once, instead of maintaining two copies of the
same checks. Measured no difference vs the extra call (V8 inlines it).

Also replaces a manual [...getCoordRaw(to)] copy in rhumbDistance
with getCoord(to), which is exactly what getCoord is for.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VSdmXWDQ2epJKfoGXFKdqT
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant