Skip to content

[Optimization] Cache prev values in closure instead of recordPropMetadata #297

Open
@yyx990803

Description

@yyx990803

Currently prop-setting helpers do this:

function setClass(el, value) {
  const prev = recordPropMetadata(el, 'class', value)
  if (value !== prev && (value || prev)) {
    el.className = value
  }
}

function recordPropMetadata(el, key, value) {
  const metadata = getMetadata(el)[0]
  const prev = metadata[key]
  if (prev !== value) metadata[key] = value
  return prev
}

function getMetadata(el) {
  return el.$$metadata || (el.$$metadata = [{}, {}])
}

recordPropMetadata has noticeable overhead on every call:

  • extra index / key access
  • extra array / object allocation

Changes Needed

For template

<div>
   <div :id="foo" :class="bar"></div>
</div>

Current codegen:

const n1 = /* ... */
_renderEffect(() => _setDOMProp(n1, "id", foo))
_renderEffect(() => _setClass(n1, bar))

Should be changed to (storing prev update values in the local closure):

const n1 = /* ... */
let _id, _cls
_renderEffect(() => _setDOMProp(n1, "id", _id, (_id = foo)))
_renderEffect(() => _setClass(n1, _cls, (_cls = bar)))

And in the relevant helpers, prev value should come from the argument instead of element metadata.

Benchmark

A simple benchmark that simulate a typical vapor update function:

<script type="module">
  import { Bench } from 'https://esm.sh/tinybench'

  function recordPropMetadata(el, key, value) {
    const metadata = getMetadata(el)[0]
    const prev = metadata[key]
    if (prev !== value) metadata[key] = value
    return prev
  }

  function getMetadata(el) {
    return el.$$metadata || (el.$$metadata = [{}, {}])
  }

  function setAttr(el, key, value) {
    const oldVal = recordPropMetadata(el, key, value)
    if (value !== oldVal) {
      if (value != null) {
        el.setAttribute(key, value)
      } else {
        el.removeAttribute(key)
      }
    }
  }

  function setAttr2(el, key, oldVal, value) {
    if (value !== oldVal) {
      if (value != null) {
        el.setAttribute(key, value)
      } else {
        el.removeAttribute(key)
      }
    }
  }

  function setClass(el, value) {
    const prev = recordPropMetadata(el, 'class', value)
    if (value !== prev && (value || prev)) {
      el.className = value
    }
  }

  function setClass2(el, prev, value) {
    if (value !== prev && (value || prev)) {
      el.className = value
    }
  }

  const updateOne = (() => {
    const n1 = document.createElement('div')
    const n2 = document.createElement('div')
    const n3 = document.createElement('div')
    const n4 = document.createElement('div')
    return val => {
      setClass(n1, val)
      setAttr(n1, 'id', val)
      setClass(n2, val)
      setAttr(n3, 'id', val)
      setClass(n4, val)
    }
  })()

  const updateTwo = (() => {
    const n1 = document.createElement('div')
    const n2 = document.createElement('div')
    const n3 = document.createElement('div')
    const n4 = document.createElement('div')
    let cls, id, cls2, id2, cls3
    return val => {
      setClass2(n1, cls, (cls = val))
      setAttr2(n1, 'id', id, (id = val))
      setClass2(n2, val, (cls2 = val))
      setAttr2(n3, 'id', val, (id2 = val))
      setClass2(n4, val, (cls3 = val))
    }
  })()

  const bench = new Bench({ name: 'old value handling for set ops', time: 100 })
  let i
  bench
    .add('in closure', () => {
      updateTwo(i++ % 5 ? 'foo' : 'bar')
    })
    .add('in metadata', () => {
      updateOne(i++ % 5 ? 'foo' : 'bar')
    })

  await bench.run()

  console.log(bench.name)
  const output = JSON.stringify(bench.table(), null, 2)
  document.getElementById('output').textContent = output
</script>

<pre id="output"></pre>

Result in Chrome:

[
  {
    "Task name": "in closure",
    "Latency average (ns)": "51.16 ± 6.19%",
    "Latency median (ns)": "0.00",
    "Throughput average (ops/s)": "19537649 ± 0.00%",
    "Throughput median (ops/s)": "19547644",
    "Samples": 1956719
  },
  {
    "Task name": "in metadata",
    "Latency average (ns)": "79.90 ± 6.20%",
    "Latency median (ns)": "0.00",
    "Throughput average (ops/s)": "12505888 ± 0.00%",
    "Throughput median (ops/s)": "12515880",
    "Samples": 1251588
  }
]

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions