Follow-up to #11900 and #11906.
Background
#11906 ported pnpm's isNew early-return so revisits in the tree walker no longer re-walk transitive subtrees. That collapsed pacquet's dependencies_tree from ~75k to ~11k entries on the astro@^5 fixture and shaved ~3.3s off a fresh-install (1.41× speedup). The remaining gap to pnpm on the same fixture is in peer resolution — resolve_peers still walks every NodeId in the tree from scratch and recomputes the full peer set, even when the same package id has already been processed with an equivalent parent context.
Pacquet's own resolve_peers.rs doc comment calls out the three optimizations the initial slice deliberately deferred. This is the issue to track them landing.
What pnpm has that pacquet doesn't
1. purePkgs fast path
Upstream tracks a Set<PkgIdWithPatchHash> of pure packages — those whose subtree resolves with zero externally-resolved peers and zero missing peers. A pure package's depPath is literally its pkgIdWithPatchHash (no peer suffix), and the walker returns it without recursing into children. From resolvePeers.ts:395-406:
if (
ctx.purePkgs.has(resolvedPackage.pkgIdWithPatchHash) &&
ctx.depGraph[resolvedPackage.pkgIdWithPatchHash as unknown as DepPath].depth <= node.depth &&
Object.keys(resolvedPackage.peerDependencies).length === 0
) {
ctx.pathsByNodeId.set(nodeId, resolvedPackage.pkgIdWithPatchHash as unknown as DepPath)
ctx.pathsByNodeIdPromises.get(nodeId)!.resolve(resolvedPackage.pkgIdWithPatchHash as unknown as DepPath)
return { resolvedPeers: new Map<string, NodeId>(), missingPeers: new Map<string, MissingPeerInfo>() }
}
The set is populated at the end of each call when a node's resolution turns out to be pure (L509-L510). On the next visit of the same pkgIdWithPatchHash, the whole subtree below is skipped.
For peer-light workloads (most npm packages have no peerDependencies), this is the dominant optimization.
2. peersCache
For non-pure packages, upstream caches the resolved-peer combination so a revisit with a compatible parent context reuses the cached depPath instead of recomputing. Definition at resolvePeers.ts:342-348:
interface PeersCacheItem {
depPath: DeferredPromise<DepPath>
resolvedPeers: Map<string, NodeId>
missingPeers: MissingPeers
}
type PeersCache = Map<PkgIdWithPatchHash, PeersCacheItem[]>
Lookup at findHit: for each cached item under the current pkgIdWithPatchHash, check whether the cached resolvedPeers map to the same parent nodes (or to ones with the same pkgIdWithPatchHash — see parentPackagesMatch) as the current parentPkgs context. If yes, reuse the cached depPath.
The cache is populated at the end of each non-pure resolution (L507-L522).
3. Lazy node.children
In upstream, node.children can be a thunk (() => Record<string, NodeId>) that gets called the first time the peer resolver touches the node — see resolvePeers.ts:407-409:
if (typeof node.children === 'function') {
node.children = node.children()
}
Combined with purePkgs, this means children of pure subtrees never get expanded.
This one requires changing pacquet's DependenciesTreeNode::children shape and is the most invasive of the three; the first two land without touching the tree-walker side.
Expected impact
Profiling pacquet@main + #11906 on astro@^5 (1.6k unique packages, depth ~10):
| phase |
before #11906 |
after #11906 |
dependencies_tree size |
~75,000 |
~11,000 |
resolve_peers per pass |
up to ~614ms |
up to ~111ms |
total resolve_peers across 11 hoist iters + final |
~3.8s |
~0.7s |
The 0.7s left is what purePkgs would shrink further. Most of the ~11,000 nodes belong to pure subtrees in the astro graph (esbuild platform tarballs, rollup platform tarballs, @img/sharp-* platform tarballs — these are leaf-ish packages with no peers and no peer-dep transitives).
A back-of-envelope estimate: if ~80% of tree nodes are pure, purePkgs could shave another ~500ms off resolve_peers total. Combined with peersCache hitting on the non-pure remainder, the peer phase should drop to sub-100ms — closing most of the remaining ~6s gap to pnpm (which finishes the same install in ~1s on this fixture).
Acceptance criteria
Lazy node.children (the third optimization above) is out of scope for this issue — it changes the DependenciesTreeNode shape and is worth its own focused PR.
Related
Written by an agent (Claude Code, claude-opus-4-7).
Follow-up to #11900 and #11906.
Background
#11906 ported pnpm's
isNewearly-return so revisits in the tree walker no longer re-walk transitive subtrees. That collapsed pacquet'sdependencies_treefrom ~75k to ~11k entries on theastro@^5fixture and shaved ~3.3s off a fresh-install (1.41× speedup). The remaining gap to pnpm on the same fixture is in peer resolution —resolve_peersstill walks every NodeId in the tree from scratch and recomputes the full peer set, even when the same package id has already been processed with an equivalent parent context.Pacquet's own
resolve_peers.rsdoc comment calls out the three optimizations the initial slice deliberately deferred. This is the issue to track them landing.What pnpm has that pacquet doesn't
1.
purePkgsfast pathUpstream tracks a
Set<PkgIdWithPatchHash>of pure packages — those whose subtree resolves with zero externally-resolved peers and zero missing peers. A pure package'sdepPathis literally itspkgIdWithPatchHash(no peer suffix), and the walker returns it without recursing into children. FromresolvePeers.ts:395-406:The set is populated at the end of each call when a node's resolution turns out to be pure (
L509-L510). On the next visit of the samepkgIdWithPatchHash, the whole subtree below is skipped.For peer-light workloads (most npm packages have no
peerDependencies), this is the dominant optimization.2.
peersCacheFor non-pure packages, upstream caches the resolved-peer combination so a revisit with a compatible parent context reuses the cached
depPathinstead of recomputing. Definition atresolvePeers.ts:342-348:Lookup at
findHit: for each cached item under the currentpkgIdWithPatchHash, check whether the cachedresolvedPeersmap to the same parent nodes (or to ones with the samepkgIdWithPatchHash— seeparentPackagesMatch) as the currentparentPkgscontext. If yes, reuse the cacheddepPath.The cache is populated at the end of each non-pure resolution (
L507-L522).3. Lazy
node.childrenIn upstream,
node.childrencan be a thunk (() => Record<string, NodeId>) that gets called the first time the peer resolver touches the node — seeresolvePeers.ts:407-409:Combined with
purePkgs, this means children of pure subtrees never get expanded.This one requires changing pacquet's
DependenciesTreeNode::childrenshape and is the most invasive of the three; the first two land without touching the tree-walker side.Expected impact
Profiling pacquet@main + #11906 on
astro@^5(1.6k unique packages, depth ~10):dependencies_treesizeresolve_peersper passresolve_peersacross 11 hoist iters + finalThe 0.7s left is what
purePkgswould shrink further. Most of the ~11,000 nodes belong to pure subtrees in theastrograph (esbuild platform tarballs, rollup platform tarballs,@img/sharp-*platform tarballs — these are leaf-ish packages with no peers and no peer-dep transitives).A back-of-envelope estimate: if ~80% of tree nodes are pure,
purePkgscould shave another ~500ms offresolve_peerstotal. Combined withpeersCachehitting on the non-pure remainder, the peer phase should drop to sub-100ms — closing most of the remaining ~6s gap to pnpm (which finishes the same install in ~1s on this fixture).Acceptance criteria
purePkgsset + the fast-path early return inpacquet/crates/resolving-deps-resolver/src/resolve_peers.rs.peersCache+findHit+parentPackagesMatch.astro@^5fixture used to profile perf(pacquet): port pnpm's purePkgs + peersCache for peer resolution #11906 is fine) showing the wall-time delta vs pacquet@main without this PR.resolving-deps-resolvertests pass unchanged.Lazy
node.children(the third optimization above) is out of scope for this issue — it changes theDependenciesTreeNodeshape and is worth its own focused PR.Related
isNewgate (already addresses the tree-explosion half).resolve_peers.rs:14-31doc comment lists these same three as deferred.Written by an agent (Claude Code, claude-opus-4-7).