Repository navigation
fix: avoid quadratic analysis time on large files - #613
Conversation
|
We hit this in production builds and can confirm the fix works on a real app. Setup: Remix 2.16 + Vite on Vercel. What we measured on that file:
With the same change as this PR (a
We can't patch nft inside Vercel's build image, so we're waiting on a release. It would be great to get this reviewed and shipped, and picked up by the Remix and React Router builders. Happy to share more profiling details if useful. |
styfle
left a comment
There was a problem hiding this comment.
Blocking: commits must be signed before this can merge — dcd1e60 is currently unsigned. Please re-sign and force-push.
Non-blocking: the proxy only implements getOwnPropertyDescriptor and get, which is all evaluate reaches for today. If static-eval later uses name in vars, Object.keys(vars), or assigns a binding, those will silently hit the empty target instead of the real bindings. A has trap and a throwing set would be cheap insurance against that.
Reviewed via styfle's devbox
computePureStaticValue copied every global and known top-level binding into a new object on each call, so analysis cost calls x bindings. Look bindings up on demand through a Proxy instead, in the same order. The proxy also answers `in` checks and rejects writes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dcd1e60 to
1adf79f
Compare
|
Thanks @styfle. Re-signed and force-pushed as |
|
CI is red on all four (ubuntu runs It's the
Nothing reaches them today since So let's cover them instead. The proxy is stateless per If you'd rather not restructure, bumping Non-blocking: lifting the proxy out of Reviewed via styfle's devbox |
Build the proxy once per analyze() through an exported factory instead of on every computePureStaticValue call, and test it directly so the has and set traps are covered. The tests pin the old eager copy's behavior: lookup order, shadowed bindings, undefined-valued globals, and no Object.prototype leaks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Went with covering them. Function coverage locally is 96.51% (threshold 96.25%), with the same environment-only failures as before this commit. A mutation that drops the shadow check fails the tests. |
There was a problem hiding this comment.
Looks good 👍
Non-blocking: the shadow rule now exists in two places, getKnownBinding and getDescriptor's binding.shadowDepth === 0 ? binding.value : undefined. They agree today, but a change to one won't reach the other. Passing getKnownBinding into the factory, or routing both through one helper, would keep it in a single spot.
Reviewed via styfle's devbox
|
🎉 This PR is included in version 1.11.1 🎉 The release is available on: Your semantic-release bot 📦🚀 |
computePureStaticValuebuilds a freshvarsobject on every call and copiesevery global and known top-level binding into it. It runs for most call
expressions and variable declarations, so analysis costs calls × bindings.
That grows with the square of the file size. Large bundled files, such as a
server build that inlines its dependencies, pay the most.
evaluateonly readsvarsby name. This PR passes aProxythat looks eachname up in
knownBindingsandglobalBindingswhen it is read, in the sameorder as before:
import.meta, then known bindings (shadowed ones stillresolve to
undefined), then globals.knownBindingschanges only in thewalker, and the walker awaits each call, so a lookup sees the same state as the
old copy.
Benchmark. A synthetic file with N top-level constants, each used once
(Node 26, script below):
mainOn a real 17 MB bundled server file, analysis dropped from 10.2s to 1.4s. On a
real app with 5,707 traced files, the traced file list was identical before
and after.
Benchmark script
Tests.
jest: 1466/1470 pass, the same as onmain. The 4 failures alsofail on
mainlocally:leveldownandsharp-pnpm(native builds), andasset-fs-extrafrom cwd and from root.🤖 Generated with Claude Code