skip to content

What does using a JavaScript Proxy cost at run time compared with a plain object, and how do you decide whether that cost matters for a given piece of code?

level: seniorimportance: should knowfreq 30%

answer

  1. a handler call per operation
  2. no inline cache, no shape prediction
  3. count the accesses, not the multiplier
  4. nested wrapping compounds the cost
  5. unwrap to raw inside hot loops

basics

~20 s

Every intercepted operation on a proxy becomes a call into a handler function, so engines cannot use the inline caches that make plain property reads nearly free. Expect a large relative slowdown per access and judge it by how many accesses the code performs.

solid answer

~50 s

The cost is per operation, and it is a relative cost, not a fixed one. A plain property read on a stable object shape is one of the cheapest things an engine does — the shape is known, the offset is cached, and the read often gets inlined away. On a proxy, every read has to call your `get` handler, which usually calls `Reflect.get` on top, and the result cannot be predicted from the object's shape, so the fast path is gone. In practice that is several times to an order of magnitude slower per access, though still nanoseconds. So the deciding question is *how many accesses*: a config object read a few dozen times, or UI state touched at human interaction rates, will never show up in a profile. A tight loop over hundreds of thousands of elements, or per-frame numeric work, absolutely will. Measure on representative data, and if it hurts, unwrap to the raw object inside the hot loop.

go deeper

for a junior

Know the basic fact: reading a property through a Proxy runs a JavaScript function first, so it cannot be as cheap as reading a property directly.

for a middle

Explain why the engine's fast path disappears — the result is decided by handler code rather than by the object's shape, so no cached slot offset and no inlining — and note that enumeration traps cost per key.

for a senior

Show that you decide by access count and by profile, not by reputation: name workloads where it is irrelevant and where it is fatal, and give a concrete mitigation such as unwrapping to the raw object inside the loop.

for a principal

Own the architectural line: which data lives inside the observed graph at all. Keep bulk numeric data out, mandate lazy wrapping with a target-to-proxy cache, and require a measured budget before a proxy layer spreads across a codebase.

## Why proxied access is slower Modern engines make plain property access fast by exploiting the fact that objects created the same way share a shape. Once the engine has seen `user.name` a few times with the same shape, it caches the slot offset and can read the field directly, and in hot code the read may be inlined into machine code with a shape guard. A proxy destroys every one of those assumptions. The result of `p.name` is whatever an arbitrary JavaScript function returns; it cannot be derived from the object's shape, cannot be cached as an offset, and cannot be inlined away. Each read is at minimum a call into your handler plus, in the common implementation, a `Reflect.get` that performs the ordinary lookup you were trying to avoid paying for once already. Writes, `in`, `delete` and key enumeration pay the same shape of cost. Second-order costs are easy to miss: - **Allocation inside traps.** A `get` trap that returns `value.bind(target)`, or that wraps a nested object in a new proxy on every read, allocates on every access and creates garbage-collection pressure. Cache in a `WeakMap` instead. - **Enumeration traps.** `ownKeys` combined with `getOwnPropertyDescriptor` runs per key for operations like `Object.keys` or spread, so spreading a large proxied object is markedly more expensive than spreading a plain one. - **Deep wrapping.** If reading a nested object hands back a proxy, the cost compounds down the path: `state.a.b.c` is three trap calls, not one read. ## How to decide Do not reason about the multiplier; reason about the count. **Almost certainly fine:** configuration and feature-flag objects, dependency-injection containers, dynamic API clients where each property access is followed by a network request, validation on form-field writes, debug-time access logging, and UI state that changes when a human clicks or types. All of these perform tens to thousands of operations, and the trap overhead disappears next to layout, network or rendering work. **Almost certainly not fine:** loops over large arrays or object graphs, per-frame geometry or physics maths, parsers and serializers walking every node, and anything reading the same property inside a hot inner loop. Here the property access *is* the work, and multiplying it hurts directly. **Needs a measurement:** medium-sized reactive tables and lists, where the cost depends on how much of the data is actually read per render. ## Measuring honestly Benchmark the real shape of your access pattern rather than a synthetic `for` loop, because engines optimise degenerate loops in ways that flatter or defame the proxy. Compare a proxied and an unproxied run of the same workload, over data of realistic size, and look at the profile: if trap functions are not visible in it, the overhead is not your problem. Beware micro-benchmarks that hoist the read out of the loop, and beware ones so small the timer resolution dominates. ## Mitigations that keep the design - **Unwrap in hot paths.** Reactive libraries expose a raw accessor (Vue's `toRaw`); read it once outside the loop and iterate the plain object. - **Wrap lazily and cache.** Create nested proxies only when the nested object is first read, and store them in a `WeakMap` keyed by target so repeated reads reuse one proxy — which also keeps `===` stable. - **Wrap shallowly.** If only the top-level keys need observing, do not descend at all. - **Keep bulk numeric data outside.** Typed arrays and large buffers should stay raw; proxy the small object that points at them. - **Hoist the read.** `const {items} = state` once, then loop over `items`, instead of touching `state.items` per iteration. ## What to say in an interview The strong answer refuses to quote a universal multiplier and instead names the mechanism (no inline caching, a call per operation), the deciding variable (operation count), and one concrete mitigation you have actually applied. Claiming proxies are 'basically free' and claiming they are 'far too slow to use' are both wrong; the honest position is that they are cheap enough for coordination-shaped code and too expensive for data-shaped code.

  • Which trap tends to be the most expensive in practice, and why?
    Key enumeration. `Object.keys`, spread and `JSON.stringify` invoke `ownKeys` and then `getOwnPropertyDescriptor` once per key, so a single spread of a large proxied object turns into a burst of trap calls plus the descriptor objects they allocate. Object-per-key work is what makes proxied serialization noticeably slower than the raw equivalent.
  • A profile shows your reactive state is hot. What is the first change you make?
    Read the raw object once outside the hot region and work on that, so the loop performs plain property access. Next, check for accidental deep wrapping — a `get` trap creating a fresh proxy per read — and cache nested proxies in a `WeakMap`. Only after that would I consider redesigning the store.
  • Why is a synthetic loop benchmark a poor way to judge proxy overhead?
    Because engines optimise degenerate loops aggressively: the plain-object version may be hoisted or eliminated entirely while the proxied version cannot be, exaggerating the gap; and a loop that touches one property misses the enumeration and allocation costs real code pays. Benchmark the actual access pattern on realistic data.

saying these in an interview costs you the question

  • Claims proxy access is as fast as a plain read
  • Says proxies disable the JIT for the whole script
  • Quotes a fixed universal slowdown multiplier
  • Creates a new nested proxy on every read
  • Judges the cost without measuring the real workload

context